#[cfg(test)] mod proposal_tests { use crate::permission::{Policy, PolicyDetails, PolicyType, RoleTarget}; use crate::*; use common_data::constants::{DEFAULT_PROPOSAL_EXPIRY_TIME_NS, NANOSECONDS_PER_WEEK}; use near_sdk::test_utils::{accounts, VMContextBuilder}; use near_sdk::{testing_env, AccountId}; fn get_context(predecessor: AccountId) -> VMContextBuilder { let mut builder = VMContextBuilder::new(); builder.predecessor_account_id(predecessor); builder } fn setup_contract() -> Contract { let context = get_context(accounts(0)); testing_env!(context.build()); Contract::new( ChangeControl::default(), Some(accounts(0)), U128(NANOSECONDS_PER_WEEK * 4), vec![accounts(0), accounts(1), accounts(2)], 2, U128(NANOSECONDS_PER_WEEK * 2), ) } #[test] fn role_breaching() { let mut contract = setup_contract(); let employee = accounts(1); // Assign the employee as owner contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "owner", "target": {"AccountId": employee.to_string()} }), ); let first_proposal_id = contract.get_latest_proposal_id(); // Proposal should have been auto-executed and removed from storage assert!(contract.get_proposal(first_proposal_id).is_none()); // Verify the role was granted through side effects assert!(contract.internal_has_role(&employee, "owner")); // =========== Scenario Start ============= // Employee switch from security department to finance department // Security department head revoke the owner role from the employee contract.propose_execution( "revoke_role".to_string(), serde_json::json!({ "role_id": "owner", "target": {"AccountId": employee.to_string()} }), ); let second_proposal_id = contract.get_latest_proposal_id(); // Proposal should have been auto-executed and removed from storage assert!(contract.get_proposal(second_proposal_id).is_none()); // Verify the role was revoked through side effects assert!(!contract.internal_has_role(&employee, "owner")); // =========== Scenario Description ============= // Finance department head grant teller role to the employee contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "target": {"AccountId": employee.to_string()} }), ); let third_proposal_id = contract.get_latest_proposal_id(); // Proposal should have been auto-executed and removed from storage assert!(contract.get_proposal(third_proposal_id).is_none()); // Verify the role was granted through side effects assert!(contract.internal_has_role(&employee, "teller")); // The employee should not have owner role, but the test will fail at the following line assert!(!contract.internal_has_role(&employee, "owner")); } #[test] fn test_propose_execution_single_vote() { let mut contract = setup_contract(); // Create a proposal that should auto-execute (threshold=1, proposer has owner role) contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "target": {"AccountId": accounts(1).to_string()} }), ); let proposal_id = contract.get_latest_proposal_id(); // Proposal should have been auto-executed and removed from storage assert!(contract.get_proposal(proposal_id).is_none()); // Verify the role was granted through side effects assert!(contract.internal_has_role(&accounts(1), "teller")); } #[test] fn test_propose_execution_multi_vote() { let mut contract = setup_contract(); // First, update a policy to require 2 votes let updated_policy = Policy { id: "grant_role".to_string(), description: Some("Grant role with 2 vote requirement".to_string()), required_role: "owner".to_string(), required_vote_count: 2, policy_type: PolicyType::KernelConfiguration, policy_details: PolicyDetails::KernelConfiguration, activation_time: near_sdk::json_types::U128(0), proposal_expiry_time_nanosec: U128(DEFAULT_PROPOSAL_EXPIRY_TIME_NS), required_pending_actions: vec![], }; contract .kernel_state .policies .insert("grant_role".to_string(), updated_policy); // Grant owner role to another account contract.internal_grant_role("owner".to_string(), RoleTarget::AccountId(accounts(2))); // Create a proposal contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "target": {"AccountId": accounts(1).to_string()} }), ); let proposal_id = contract.get_latest_proposal_id(); // Proposal should be pending (not auto-executed) but proposer already voted let proposal = contract.get_proposal(proposal_id).unwrap(); let proposal_votes = contract.get_proposal_votes(proposal_id); assert_eq!(proposal_votes.len(), 1); assert_eq!(proposal.execution_threshold, 2); // Second vote (from accounts(2)) - should trigger execution let context = get_context(accounts(2)); testing_env!(context.build()); contract.vote_on_proposal(proposal_id); // Proposal should have been auto-executed and removed from storage assert!(contract.get_proposal(proposal_id).is_none()); // Verify the role was granted through side effects assert!(contract.internal_has_role(&accounts(1), "teller")); } #[test] fn test_vote_on_proposal_duplicate_vote() { let mut contract = setup_contract(); // Setup multi-vote policy let updated_policy = Policy { id: "grant_role".to_string(), description: Some("Grant role with 2 vote requirement".to_string()), required_role: "owner".to_string(), required_vote_count: 2, policy_type: PolicyType::KernelConfiguration, policy_details: PolicyDetails::KernelConfiguration, activation_time: near_sdk::json_types::U128(0), proposal_expiry_time_nanosec: U128(DEFAULT_PROPOSAL_EXPIRY_TIME_NS), required_pending_actions: vec![], }; contract .kernel_state .policies .insert("grant_role".to_string(), updated_policy); contract.internal_grant_role("owner".to_string(), RoleTarget::AccountId(accounts(2))); // Create a proposal contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "account_id": accounts(1).to_string() }), ); let proposal_id = contract.get_latest_proposal_id(); // Proposer already voted during propose_execution, try to vote again - should panic let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { contract.vote_on_proposal(proposal_id); })); assert!(result.is_err()); } #[test] fn test_vote_on_proposal_role_check() { let mut contract = setup_contract(); // Setup multi-vote policy let updated_policy = Policy { id: "grant_role".to_string(), description: Some("Grant role with 2 vote requirement".to_string()), required_role: "owner".to_string(), required_vote_count: 2, policy_type: PolicyType::KernelConfiguration, policy_details: PolicyDetails::KernelConfiguration, activation_time: near_sdk::json_types::U128(0), proposal_expiry_time_nanosec: U128(DEFAULT_PROPOSAL_EXPIRY_TIME_NS), required_pending_actions: vec![], }; contract .kernel_state .policies .insert("grant_role".to_string(), updated_policy); // Create a proposal contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "account_id": accounts(1).to_string() }), ); let proposal_id = contract.get_latest_proposal_id(); // Try to vote with account that doesn't have owner role - should panic let context = get_context(accounts(3)); // accounts(3) doesn't have owner role testing_env!(context.build()); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { contract.vote_on_proposal(proposal_id); })); assert!(result.is_err()); } #[test] fn test_cancel_proposal() { let mut contract = setup_contract(); // Setup multi-vote policy let updated_policy = Policy { id: "grant_role".to_string(), description: Some("Grant role with 2 vote requirement".to_string()), required_role: "owner".to_string(), required_vote_count: 2, policy_type: PolicyType::KernelConfiguration, policy_details: PolicyDetails::KernelConfiguration, activation_time: near_sdk::json_types::U128(0), proposal_expiry_time_nanosec: U128(DEFAULT_PROPOSAL_EXPIRY_TIME_NS), required_pending_actions: vec![], }; contract .kernel_state .policies .insert("grant_role".to_string(), updated_policy); // Create a proposal contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "account_id": accounts(1).to_string() }), ); let proposal_id = contract.get_latest_proposal_id(); // Cancel the proposal (as proposer) contract.cancel_proposal(proposal_id); // Cancelled proposals are immediately removed from storage assert!(contract.get_proposal(proposal_id).is_none()); } #[test] fn test_get_proposal_votes() { let mut contract = setup_contract(); // Setup multi-vote policy let updated_policy = Policy { id: "grant_role".to_string(), description: Some("Grant role with 2 vote requirement".to_string()), required_role: "owner".to_string(), required_vote_count: 2, policy_type: PolicyType::KernelConfiguration, policy_details: PolicyDetails::KernelConfiguration, activation_time: near_sdk::json_types::U128(0), proposal_expiry_time_nanosec: U128(DEFAULT_PROPOSAL_EXPIRY_TIME_NS), required_pending_actions: vec![], }; contract .kernel_state .policies .insert("grant_role".to_string(), updated_policy); contract.internal_grant_role("owner".to_string(), RoleTarget::AccountId(accounts(2))); // Create a proposal contract.propose_execution( "grant_role".to_string(), serde_json::json!({ "role_id": "teller", "target": {"AccountId": accounts(1).to_string()} }), ); let proposal_id = contract.get_latest_proposal_id(); // Check votes after first vote (proposer) - should have 1 vote let votes = contract.get_proposal_votes(proposal_id); assert_eq!(votes.len(), 1); assert_eq!(votes[0].voter, accounts(0)); // Proposer's vote // Now have accounts(2) vote - this will trigger execution and cleanup let context = get_context(accounts(2)); testing_env!(context.build()); contract.vote_on_proposal(proposal_id); // After execution, proposal and votes are removed from storage assert!(contract.get_proposal(proposal_id).is_none()); assert_eq!(contract.get_proposal_votes(proposal_id).len(), 0); // Verify the role was granted through side effects assert!(contract.internal_has_role(&accounts(1), "teller")); } }