Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions TEST_VERIFIER_SAME_AS_CREATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion tests/proptest_timestamps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
198 changes: 198 additions & 0 deletions tests/validate_auth.rs
Original file line number Diff line number Diff line change
@@ -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<Address>) -> 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);
}
Loading