From d11ecc2f38d916f7f93431093089fc4bb472542e Mon Sep 17 00:00:00 2001 From: D'Angelo Rodriguez <70290504+dangelo352@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:58:05 -0400 Subject: [PATCH] test: cover validate milestone scoped authorization --- TESTING_GUIDE.md | 18 +++ TEST_VERIFIER_SAME_AS_CREATOR.md | 15 +++ tests/proptest_timestamps.rs | 1 - tests/validate_auth.rs | 198 +++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 tests/validate_auth.rs diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 72ba285..650d0a1 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -49,6 +49,24 @@ Explicit edge vectors included: - `duration == MAX_VAULT_DURATION` (accept) - `start == now` (accept) +## Scoped Authorization Tests for Milestone Validation (Issue #228) + +File: `tests/validate_auth.rs` + +What is validated: + +- **Designated verifier only**: when a vault has `Some(verifier)`, a stranger-signed validation attempt is rejected and leaves `milestone_validated == false`. +- **Creator-only validation**: when a vault has `None` for verifier, a non-creator is rejected and the vault remains active and unvalidated. +- **Authorized success paths**: the designated verifier and creator-only validator both succeed before the deadline. +- **Creator as explicit verifier**: `Some(creator)` still rejects stranger auth. +- **Auth-before-expiry ordering**: an expired vault still requires the authorized validator before surfacing the expiry failure. + +Strategy design: + +- Vault setup uses broad mock auth only for creation and token funding. +- Each validation call replaces setup auth with a single scoped `mock_auths` entry for the signer under test. +- Rejection tests assert unchanged state through `get_vault_state`. + - **32 comprehensive tests** - All passing - **92.16% line coverage** (47/51 lines) diff --git a/TEST_VERIFIER_SAME_AS_CREATOR.md b/TEST_VERIFIER_SAME_AS_CREATOR.md index d8df790..713c02e 100644 --- a/TEST_VERIFIER_SAME_AS_CREATOR.md +++ b/TEST_VERIFIER_SAME_AS_CREATOR.md @@ -138,6 +138,21 @@ The contract supports two ways for creators to validate their own milestones: Both are functionally equivalent in terms of who can validate, but the explicit designation (`Some(creator)`) makes the intent clearer in the vault's state. +## Negative Authorization Coverage + +The scoped-auth suite in `tests/validate_auth.rs` complements this positive +same-as-creator test. It proves that: + +- a stranger cannot validate a vault configured with `Some(verifier)`; +- a stranger cannot validate a creator-only vault configured with `None`; +- a stranger cannot validate when the creator is explicitly set as verifier; +- failed validation attempts leave `milestone_validated` false and the vault + `Active`. + +Those tests use per-call `mock_auths` instead of `mock_all_auths()` for the +validation step, so the contract's `require_auth()` branches are actually +exercised. + ### Timestamp Validation The test confirms that even when verifier == creator, the time-lock constraints are still enforced: diff --git a/tests/proptest_timestamps.rs b/tests/proptest_timestamps.rs index d8e37e8..bd89ed3 100644 --- a/tests/proptest_timestamps.rs +++ b/tests/proptest_timestamps.rs @@ -279,7 +279,6 @@ fn edge_start_eq_now_succeeds() { assert_eq!(vault.end_timestamp, end); } - #[test] fn edge_start_eq_end_rejected() { let (env, client, usdc, usdc_asset) = setup(); diff --git a/tests/validate_auth.rs b/tests/validate_auth.rs new file mode 100644 index 0000000..621f949 --- /dev/null +++ b/tests/validate_auth.rs @@ -0,0 +1,198 @@ +#![cfg(test)] + +extern crate std; + +use disciplr_vault::{DisciplrVault, DisciplrVaultClient, VaultStatus, MIN_AMOUNT}; +use soroban_sdk::{ + testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke}, + token::StellarAssetClient, + Address, BytesN, Env, IntoVal, +}; + +struct ValidateAuthSetup { + env: Env, + client: DisciplrVaultClient<'static>, + usdc: Address, + creator: Address, + verifier: Address, + stranger: Address, + success: Address, + failure: Address, + start: u64, + end: u64, +} + +impl ValidateAuthSetup { + fn new() -> Self { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(DisciplrVault, ()); + let client = DisciplrVaultClient::new(&env, &contract_id); + + let usdc_admin = Address::generate(&env); + let usdc_contract = env.register_stellar_asset_contract_v2(usdc_admin); + let usdc = usdc_contract.address(); + let usdc_asset = StellarAssetClient::new(&env, &usdc); + + let creator = Address::generate(&env); + let verifier = Address::generate(&env); + let stranger = Address::generate(&env); + let success = Address::generate(&env); + let failure = Address::generate(&env); + + let start = 1_725_000_000u64; + let end = start + 86_400; + env.ledger().set_timestamp(start); + usdc_asset.mint(&creator, &MIN_AMOUNT); + + Self { + env, + client, + usdc, + creator, + verifier, + stranger, + success, + failure, + start, + end, + } + } + + fn create_vault(&self, verifier: Option
) -> u32 { + self.client.mock_all_auths().create_vault( + &self.usdc, + &self.creator, + &MIN_AMOUNT, + &self.start, + &self.end, + &BytesN::from_array(&self.env, &[22u8; 32]), + &verifier, + &self.success, + &self.failure, + ) + } + + fn validate_with_auth(&self, signer: &Address, vault_id: u32) -> bool { + self.client + .mock_auths(&[MockAuth { + address: signer, + invoke: &MockAuthInvoke { + contract: &self.client.address, + fn_name: "validate_milestone", + args: (&vault_id,).into_val(&self.env), + sub_invokes: &[], + }, + }]) + .validate_milestone(&vault_id) + } +} + +fn assert_active_unvalidated(setup: &ValidateAuthSetup, vault_id: u32) { + let vault = setup.client.get_vault_state(&vault_id).unwrap(); + assert_eq!(vault.status, VaultStatus::Active); + assert!(!vault.milestone_validated); +} + +#[test] +fn verifier_configuration_rejects_stranger_and_preserves_state() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(Some(setup.verifier.clone())); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + setup.validate_with_auth(&setup.stranger, vault_id); + })); + + assert!( + result.is_err(), + "stranger auth must not satisfy verifier auth" + ); + assert_active_unvalidated(&setup, vault_id); +} + +#[test] +fn verifier_configuration_accepts_designated_verifier() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(Some(setup.verifier.clone())); + + assert!(setup.validate_with_auth(&setup.verifier, vault_id)); + + let vault = setup.client.get_vault_state(&vault_id).unwrap(); + assert_eq!(vault.status, VaultStatus::Active); + assert!(vault.milestone_validated); +} + +#[test] +fn no_verifier_configuration_rejects_non_creator_and_preserves_state() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(None); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + setup.validate_with_auth(&setup.stranger, vault_id); + })); + + assert!( + result.is_err(), + "stranger auth must not satisfy creator auth" + ); + assert_active_unvalidated(&setup, vault_id); +} + +#[test] +fn no_verifier_configuration_accepts_creator() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(None); + + assert!(setup.validate_with_auth(&setup.creator, vault_id)); + + let vault = setup.client.get_vault_state(&vault_id).unwrap(); + assert_eq!(vault.status, VaultStatus::Active); + assert!(vault.milestone_validated); +} + +#[test] +fn creator_as_explicit_verifier_rejects_stranger() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(Some(setup.creator.clone())); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + setup.validate_with_auth(&setup.stranger, vault_id); + })); + + assert!( + result.is_err(), + "stranger auth must not satisfy creator-as-verifier auth" + ); + assert_active_unvalidated(&setup, vault_id); +} + +#[test] +fn expired_vault_requires_authorized_validator_before_expiry_error() { + let setup = ValidateAuthSetup::new(); + let vault_id = setup.create_vault(Some(setup.verifier.clone())); + setup.env.ledger().set_timestamp(setup.end); + + let unauthorized = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + setup.validate_with_auth(&setup.stranger, vault_id); + })); + assert!(unauthorized.is_err()); + assert_active_unvalidated(&setup, vault_id); + + let authorized = setup + .client + .mock_auths(&[MockAuth { + address: &setup.verifier, + invoke: &MockAuthInvoke { + contract: &setup.client.address, + fn_name: "validate_milestone", + args: (&vault_id,).into_val(&setup.env), + sub_invokes: &[], + }, + }]) + .try_validate_milestone(&vault_id); + assert!( + authorized.is_err(), + "authorized expired validation should fail" + ); + assert_active_unvalidated(&setup, vault_id); +}