Skip to content
Merged
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
26 changes: 13 additions & 13 deletions app/contract/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/contract/contracts/Folder/src/coverage_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ fn test_demo_expiry_and_refund_under_10_lines() {
/// Privacy toggle and balance privacy using TestContext in ≤ 10 lines.
#[test]
fn test_demo_privacy_toggle_under_10_lines() {
let ctx = TestContext::new(); // 1
let ctx = TestContext::with_admin(); // 1 — set_privacy requires initialization
assert!(!ctx.client.get_privacy(&ctx.alice)); // 2
ctx.client.set_privacy(&ctx.alice, &true); // 3
assert!(ctx.client.get_privacy(&ctx.alice)); // 4
Expand Down
33 changes: 20 additions & 13 deletions app/contract/contracts/Folder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,24 @@ impl RustAcademyContract {

/// Enable or disable privacy for an account.
///
/// Access: **owner** — `owner` must authorize the call. Gated by the
/// [`PauseFlag::SetPrivacy`] feature flag. The auth check is enforced
/// inside [`privacy::set_privacy`].
///
/// # Arguments
/// * `env` - The contract environment
/// * `owner` - The account address to configure
/// * `owner` - The account address to configure (must authorize)
/// * `enabled` - `true` to enable privacy, `false` to disable
///
/// # Errors
/// * `ContractPaused` - Contract is currently paused
/// * `Unauthorized` - Contract is not initialized
/// * `OperationPaused` - The `SetPrivacy` feature is paused
/// * `PrivacyAlreadySet` - Privacy state is already at the requested value
pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> {
// guard_initialized ensures the contract is properly set up before any
// state is mutated. Owner auth is enforced inside privacy::set_privacy.
admin::guard_initialized(&env)?;
if is_feature_paused(&env, PauseFlag::SetPrivacy) {
if storage::is_feature_paused(&env, PauseFlag::SetPrivacy) {
return Err(RustAcademyError::OperationPaused);
}
privacy::set_privacy(&env, owner, enabled)
Expand Down Expand Up @@ -359,13 +366,20 @@ impl RustAcademyContract {
/// Returns the new counter value. Parameters `_from`, `_to`, `_amount` are reserved for
/// future use; the implementation only increments the counter.
///
/// Access: requires initialized contract. Gated by `guard_initialized` to prevent
/// counter manipulation on an uninitialized deployment.
///
/// # Arguments
/// * `env` - The contract environment
/// * `_from` - Reserved (depositor address for future use)
/// * `_to` - Reserved (recipient address for future use)
/// * `_amount` - Reserved (amount for future use)
pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> u64 {
increment_escrow_counter(&env)
///
/// # Errors
/// * `Unauthorized` - Contract has not been initialized
pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> Result<u64, RustAcademyError> {
admin::guard_initialized(&env)?;
Ok(increment_escrow_counter(&env))
}

/// Health check for deployment and monitoring.
Expand Down Expand Up @@ -572,7 +586,7 @@ impl RustAcademyContract {
env: Env,
stealth_address: BytesN<32>,
) -> Result<(), RustAcademyError> {
admin::require_initialized(&env)?;
admin::guard_initialized(&env)?;
stealth::cleanup_stealth_escrow(&env, stealth_address)
}

Expand Down Expand Up @@ -873,13 +887,6 @@ impl RustAcademyContract {
admin::set_paused(&env, caller, new_state)
}

/// Check if the function is currently paused.
///
/// Returns `true` if paused, `false` otherwise.
pub fn is_feature_paused(env: &Env, flag: PauseFlag) -> bool {
storage::is_feature_paused(env, flag)
}

/// Pause a function in the contract (**Admin only**).
///
/// When paused, the particular operations is blocked. Caller must equal the stored admin.
Expand Down
201 changes: 201 additions & 0 deletions app/contract/contracts/Folder/src/role_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,205 @@ fn test_gated_privacy_and_commitment_work_when_unpaused() {
let _ = ctx
.client
.create_amount_commitment(&ctx.alice, &1_000i128, &salt);
}

// ============================================================================
// Issue #53 — standardized guard helper coverage
// ============================================================================

/// `set_privacy` requires the owner to authorize — succeeds when called by the owner.
#[test]
fn test_set_privacy_requires_owner_auth() {
let ctx = TestContext::with_admin();
// In tests all auths are mocked; calling set_privacy with alice means alice
// authorizes her own address, which must succeed.
let res = ctx.client.try_set_privacy(&ctx.alice, &true);
assert!(res.is_ok(), "owner should be allowed to set their own privacy");
}

/// `set_privacy` is rejected when the contract has not been initialized.
#[test]
fn test_set_privacy_blocked_when_uninitialized() {
// TestContext::new() does NOT call initialize()
let ctx = TestContext::new();
let res = ctx.client.try_set_privacy(&ctx.alice, &true);
assert!(
res.is_err(),
"set_privacy must be blocked before contract is initialized"
);
}

/// `create_escrow` is gated by `guard_initialized` and fails before initialization.
#[test]
fn test_create_escrow_blocked_when_uninitialized() {
let ctx = TestContext::new();
let res = ctx.client.try_create_escrow(&ctx.alice, &ctx.bob, &100u64);
assert!(
res.is_err(),
"create_escrow must fail when the contract is not initialized"
);
}

/// `create_escrow` succeeds after the contract is initialized and increments the counter.
#[test]
fn test_create_escrow_works_when_initialized() {
let ctx = TestContext::with_admin();
let counter = ctx.client.create_escrow(&ctx.alice, &ctx.bob, &0u64);
assert_eq!(counter, 1u64, "first create_escrow call should return counter = 1");
}

/// `cleanup_stealth_escrow` uses `guard_initialized` — blocked before init.
#[test]
fn test_cleanup_stealth_escrow_blocked_when_uninitialized() {
let ctx = TestContext::new();
let dummy: soroban_sdk::BytesN<32> = soroban_sdk::BytesN::from_array(&ctx.env, &[0u8; 32]);
let res = ctx.client.try_cleanup_stealth_escrow(&dummy);
assert!(
res.is_err(),
"cleanup_stealth_escrow must fail when contract is not initialized"
);
}

/// Deposit is blocked by the global pause flag (tests `guard_deposit`).
#[test]
fn test_deposit_blocked_when_globally_paused() {
let ctx = TestContext::with_admin();
ctx.client.set_paused(&ctx.admin, &true);

ctx.mint(&ctx.alice, 1000);
let res = ctx.client.try_deposit(
&ctx.token,
&1000i128,
&ctx.alice,
&ctx.salt(b"paused"),
&0u64,
&None,
);
assert!(
res.is_err(),
"deposit must be blocked when the contract is globally paused"
);
}

/// Deposit is blocked by the feature-level pause flag (tests `guard_deposit` feature gate).
#[test]
fn test_deposit_blocked_when_deposit_feature_paused() {
let ctx = TestContext::with_admin();
ctx.client
.pause_features(&ctx.admin, &(storage::PauseFlag::Deposit as u64));

ctx.mint(&ctx.alice, 1000);
let res = ctx.client.try_deposit(
&ctx.token,
&1000i128,
&ctx.alice,
&ctx.salt(b"feat_paused"),
&0u64,
&None,
);
assert!(
res.is_err(),
"deposit must be blocked when the Deposit feature flag is paused"
);
}

/// Deposit is blocked in emergency mode (tests that `guard_deposit` includes emergency check).
#[test]
fn test_deposit_blocked_in_emergency_mode() {
let ctx = TestContext::with_admin();
ctx.client.activate_emergency_mode(&ctx.admin);

ctx.mint(&ctx.alice, 1000);
let res = ctx.client.try_deposit(
&ctx.token,
&1000i128,
&ctx.alice,
&ctx.salt(b"emergency_deposit"),
&0u64,
&None,
);
assert!(
res.is_err(),
"deposit must be blocked in emergency mode"
);
}

/// Refund is blocked when the global pause flag is set (tests `guard_refund`).
#[test]
fn test_refund_blocked_when_globally_paused() {
let ctx = TestContext::with_admin();

ctx.mint(&ctx.alice, 500);
let commitment = ctx.client.deposit(
&ctx.token,
&500i128,
&ctx.alice,
&ctx.salt(b"refund_pause"),
&1u64, // 1 second timeout
&None,
);

// Advance time past expiry
ctx.advance_time(100);

// Now pause
ctx.client.set_paused(&ctx.admin, &true);

let res = ctx.client.try_refund(&commitment, &ctx.alice);
assert!(
res.is_err(),
"refund must be blocked when the contract is globally paused"
);
}

/// Dispute operations are blocked when the contract is globally paused (tests `guard_dispute`).
#[test]
fn test_dispute_blocked_when_globally_paused() {
let ctx = TestContext::with_admin();
let arbiter = Address::generate(&ctx.env);

ctx.mint(&ctx.alice, 1000);
let commitment = ctx.client.deposit(
&ctx.token,
&1000i128,
&ctx.alice,
&ctx.salt(b"dispute_pause"),
&0u64,
&Some(arbiter.clone()),
);

ctx.client.set_paused(&ctx.admin, &true);

let res = ctx.client.try_dispute(&commitment);
assert!(
res.is_err(),
"dispute must be blocked when the contract is globally paused"
);
}

/// Admin configuration calls are blocked in emergency mode (tests `guard_admin_config`).
#[test]
fn test_set_paused_blocked_in_emergency_mode() {
let ctx = TestContext::with_admin();
ctx.client.activate_emergency_mode(&ctx.admin);

let res = ctx.client.try_set_paused(&ctx.admin, &false);
assert!(
res.is_err(),
"set_paused must be blocked once emergency mode is active"
);
}

/// `guard_initialized` returns Unauthorized when the contract is not yet initialized.
#[test]
fn test_guard_initialized_blocks_uninitialized_ops() {
let ctx = TestContext::new();

// cleanup_escrow uses guard_initialized
let dummy: soroban_sdk::BytesN<32> = soroban_sdk::BytesN::from_array(&ctx.env, &[1u8; 32]);
let res = ctx.client.try_cleanup_escrow(&dummy);
assert!(
res.is_err(),
"cleanup_escrow must fail on an uninitialized contract"
);
}
2 changes: 2 additions & 0 deletions app/contract/contracts/Folder/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,8 @@ fn test_commitment_cycle() {
#[test]
fn test_create_escrow() {
let (env, client) = setup();
let admin = Address::generate(&env);
client.initialize(&admin);
let from = Address::generate(&env);
let to = Address::generate(&env);
let amount = 1_000;
Expand Down
Loading