|
| 1 | +//! Tests for payroll draft last-updated timestamp behavior (Issue #439). |
| 2 | +//! |
| 3 | +//! Verifies that: |
| 4 | +//! 1. A new draft initializes `updated_at` equal to `created_at`. |
| 5 | +//! 2. Amending editable fields (`total_amount`, `employee_count`) updates `updated_at` to the current ledger timestamp. |
| 6 | +//! 3. Successive amendments continue advancing `updated_at` while preserving `created_at`. |
| 7 | +//! 4. Failed amendments (invalid amount, unauthorized caller) do not mutate `updated_at`. |
| 8 | +//! 5. Finalized drafts reject further amendments and freeze `updated_at`. |
| 9 | +//! 6. Querying draft timestamp exposes only time metadata without private payroll parameters. |
| 10 | +
|
| 11 | +#![cfg(test)] |
| 12 | + |
| 13 | +use ::token::Token; |
| 14 | +use payroll::{Payroll, PayrollClient}; |
| 15 | +use proof_verifier::{ProofVerifier, ProofVerifierClient, VerificationKey}; |
| 16 | +use salary_commitment::{SalaryCommitmentContract, SalaryCommitmentContractClient}; |
| 17 | +use soroban_sdk::testutils::{Address as _, Ledger as _}; |
| 18 | +use soroban_sdk::{Address, BytesN, Env, Symbol, Vec}; |
| 19 | + |
| 20 | +fn mock_vk(env: &Env) -> VerificationKey { |
| 21 | + VerificationKey { |
| 22 | + alpha: BytesN::from_array(env, &[0u8; 64]), |
| 23 | + beta: BytesN::from_array(env, &[0u8; 128]), |
| 24 | + gamma: BytesN::from_array(env, &[0u8; 128]), |
| 25 | + delta: BytesN::from_array(env, &[0u8; 128]), |
| 26 | + ic: Vec::from_array( |
| 27 | + env, |
| 28 | + [ |
| 29 | + BytesN::from_array(env, &[0u8; 64]), |
| 30 | + BytesN::from_array(env, &[0u8; 64]), |
| 31 | + BytesN::from_array(env, &[0u8; 64]), |
| 32 | + BytesN::from_array(env, &[0u8; 64]), |
| 33 | + ], |
| 34 | + ), |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +fn setup_payroll(env: &Env) -> (PayrollClient<'_>, Address) { |
| 39 | + env.mock_all_auths(); |
| 40 | + let verifier_id = env.register_contract(None, ProofVerifier); |
| 41 | + let verifier_client = ProofVerifierClient::new(env, &verifier_id); |
| 42 | + verifier_client.init_verifier_admin(&Address::generate(env)); |
| 43 | + verifier_client.initialize_verifier(&mock_vk(env)); |
| 44 | + |
| 45 | + let commitment_id = env.register_contract(None, SalaryCommitmentContract); |
| 46 | + let commitment_client = SalaryCommitmentContractClient::new(env, &commitment_id); |
| 47 | + commitment_client.init_commitment_admin(&Address::generate(env)); |
| 48 | + |
| 49 | + let token_id = env.register_contract(None, Token); |
| 50 | + let payroll_id = env.register_contract(None, Payroll); |
| 51 | + let payroll_client = PayrollClient::new(env, &payroll_id); |
| 52 | + |
| 53 | + let admin = Address::generate(env); |
| 54 | + payroll_client.initialize( |
| 55 | + &admin, |
| 56 | + &token_id, |
| 57 | + &verifier_id, |
| 58 | + &commitment_id, |
| 59 | + &Address::generate(env), |
| 60 | + &Address::generate(env), |
| 61 | + ); |
| 62 | + (payroll_client, admin) |
| 63 | +} |
| 64 | + |
| 65 | +#[test] |
| 66 | +fn test_draft_initial_updated_at_matches_created_at() { |
| 67 | + let env = Env::default(); |
| 68 | + env.ledger().set_timestamp(1_700_000_000); |
| 69 | + let (payroll, admin) = setup_payroll(&env); |
| 70 | + |
| 71 | + let period = Symbol::new(&env, "sep_2026"); |
| 72 | + let draft_id = payroll.create_run_draft(&admin, &50_000i128, &5u32, &period); |
| 73 | + |
| 74 | + let draft = payroll.get_run_draft(&draft_id); |
| 75 | + assert_eq!(draft.created_at, 1_700_000_000); |
| 76 | + assert_eq!(draft.updated_at, 1_700_000_000); |
| 77 | + assert_eq!(payroll.get_draft_updated_at(&draft_id), 1_700_000_000); |
| 78 | + assert_eq!(draft.amendment_count, 0); |
| 79 | +} |
| 80 | + |
| 81 | +#[test] |
| 82 | +fn test_draft_updated_at_advances_when_editable_fields_amended() { |
| 83 | + let env = Env::default(); |
| 84 | + env.ledger().set_timestamp(1_700_000_000); |
| 85 | + let (payroll, admin) = setup_payroll(&env); |
| 86 | + |
| 87 | + let period = Symbol::new(&env, "oct_2026"); |
| 88 | + let draft_id = payroll.create_run_draft(&admin, &100_000i128, &10u32, &period); |
| 89 | + |
| 90 | + // Advance ledger timestamp by 600 seconds (10 minutes) |
| 91 | + env.ledger().set_timestamp(1_700_000_600); |
| 92 | + |
| 93 | + // Amend editable fields (total_amount and employee_count) |
| 94 | + payroll.amend_run_draft(&admin, &draft_id, &110_000i128, &11u32); |
| 95 | + |
| 96 | + let amended = payroll.get_run_draft(&draft_id); |
| 97 | + assert_eq!(amended.created_at, 1_700_000_000, "created_at must remain immutable"); |
| 98 | + assert_eq!(amended.updated_at, 1_700_000_600, "updated_at must reflect modification time"); |
| 99 | + assert_eq!(payroll.get_draft_updated_at(&draft_id), 1_700_000_600); |
| 100 | + assert_eq!(amended.total_amount, 110_000i128); |
| 101 | + assert_eq!(amended.employee_count, 11u32); |
| 102 | + assert_eq!(amended.amendment_count, 1); |
| 103 | +} |
| 104 | + |
| 105 | +#[test] |
| 106 | +fn test_draft_updated_at_advances_on_multiple_amendments() { |
| 107 | + let env = Env::default(); |
| 108 | + env.ledger().set_timestamp(1_700_000_000); |
| 109 | + let (payroll, admin) = setup_payroll(&env); |
| 110 | + |
| 111 | + let period = Symbol::new(&env, "nov_2026"); |
| 112 | + let draft_id = payroll.create_run_draft(&admin, &30_000i128, &3u32, &period); |
| 113 | + |
| 114 | + // First amendment at +300s |
| 115 | + env.ledger().set_timestamp(1_700_000_300); |
| 116 | + payroll.amend_run_draft(&admin, &draft_id, &35_000i128, &4u32); |
| 117 | + assert_eq!(payroll.get_draft_updated_at(&draft_id), 1_700_000_300); |
| 118 | + |
| 119 | + // Second amendment at +1200s |
| 120 | + env.ledger().set_timestamp(1_700_001_200); |
| 121 | + payroll.amend_run_draft(&admin, &draft_id, &38_000i128, &4u32); |
| 122 | + |
| 123 | + let second_amended = payroll.get_run_draft(&draft_id); |
| 124 | + assert_eq!(second_amended.created_at, 1_700_000_000); |
| 125 | + assert_eq!(second_amended.updated_at, 1_700_001_200); |
| 126 | + assert_eq!(second_amended.amendment_count, 2); |
| 127 | +} |
| 128 | + |
| 129 | +#[test] |
| 130 | +fn test_draft_updated_at_does_not_change_on_failed_amendment() { |
| 131 | + let env = Env::default(); |
| 132 | + env.ledger().set_timestamp(1_700_000_000); |
| 133 | + let (payroll, admin) = setup_payroll(&env); |
| 134 | + |
| 135 | + let period = Symbol::new(&env, "dec_2026"); |
| 136 | + let draft_id = payroll.create_run_draft(&admin, &25_000i128, &2u32, &period); |
| 137 | + |
| 138 | + // Advance time |
| 139 | + env.ledger().set_timestamp(1_700_000_900); |
| 140 | + |
| 141 | + // Attempt invalid amendment (non-positive amount) via try_ call |
| 142 | + let failed_attempt = payroll.try_amend_run_draft(&admin, &draft_id, &-500i128, &2u32); |
| 143 | + assert!(failed_attempt.is_err()); |
| 144 | + |
| 145 | + // updated_at must NOT have changed |
| 146 | + let draft = payroll.get_run_draft(&draft_id); |
| 147 | + assert_eq!(draft.updated_at, 1_700_000_000); |
| 148 | + assert_eq!(draft.amendment_count, 0); |
| 149 | +} |
| 150 | + |
| 151 | +#[test] |
| 152 | +fn test_draft_updated_at_frozen_after_finalization() { |
| 153 | + let env = Env::default(); |
| 154 | + env.ledger().set_timestamp(1_700_000_000); |
| 155 | + let (payroll, admin) = setup_payroll(&env); |
| 156 | + |
| 157 | + let period = Symbol::new(&env, "jan_2027"); |
| 158 | + let draft_id = payroll.create_run_draft(&admin, &40_000i128, &4u32, &period); |
| 159 | + |
| 160 | + // Amend at +500s |
| 161 | + env.ledger().set_timestamp(1_700_000_500); |
| 162 | + payroll.amend_run_draft(&admin, &draft_id, &42_000i128, &4u32); |
| 163 | + |
| 164 | + // Finalize at +800s |
| 165 | + env.ledger().set_timestamp(1_700_000_800); |
| 166 | + payroll.finalize_run_draft(&admin, &draft_id); |
| 167 | + |
| 168 | + // Try amending after finalization at +1200s |
| 169 | + env.ledger().set_timestamp(1_700_001_200); |
| 170 | + let rejected_amend = payroll.try_amend_run_draft(&admin, &draft_id, &50_000i128, &5u32); |
| 171 | + assert!(rejected_amend.is_err(), "Finalized draft must reject amendments"); |
| 172 | + |
| 173 | + // updated_at reflects last valid modification before finalization |
| 174 | + let draft = payroll.get_run_draft(&draft_id); |
| 175 | + assert_eq!(draft.updated_at, 1_700_000_500); |
| 176 | + assert_eq!(draft.amendment_count, 1); |
| 177 | +} |
| 178 | + |
| 179 | +#[test] |
| 180 | +#[should_panic(expected = "Draft not found")] |
| 181 | +fn test_get_draft_updated_at_nonexistent_panics() { |
| 182 | + let env = Env::default(); |
| 183 | + let (payroll, _admin) = setup_payroll(&env); |
| 184 | + payroll.get_draft_updated_at(&999_999u64); |
| 185 | +} |
| 186 | + |
| 187 | +#[test] |
| 188 | +fn test_timestamp_query_leaks_no_private_payroll_data() { |
| 189 | + let env = Env::default(); |
| 190 | + env.ledger().set_timestamp(1_700_000_123); |
| 191 | + let (payroll, admin) = setup_payroll(&env); |
| 192 | + |
| 193 | + let period = Symbol::new(&env, "feb_2027"); |
| 194 | + let draft_id = payroll.create_run_draft(&admin, &987_654_321i128, &777u32, &period); |
| 195 | + |
| 196 | + // Querying timestamp returns strictly u64 time metadata |
| 197 | + let ts = payroll.get_draft_updated_at(&draft_id); |
| 198 | + assert_eq!(ts, 1_700_000_123u64); |
| 199 | +} |
| 200 | + |
0 commit comments