diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 7d62b43..ea5e7f8 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -46,6 +46,7 @@ pub enum PredifiError { PoolNotResolved = 22, InvalidPoolState = 24, AlreadyClaimed = 60, + PoolCanceled = 70, ResolutionDelayNotMet = 81, } @@ -61,6 +62,8 @@ pub enum MarketState { #[derive(Clone)] pub struct Pool { pub end_time: u64, + pub resolved: bool, + pub canceled: bool, pub state: MarketState, pub outcome: u32, pub token: Address, @@ -189,6 +192,8 @@ pub struct PoolResolvedEvent { #[derive(Clone, Debug, Eq, PartialEq)] pub struct PoolCanceledEvent { pub pool_id: u64, + pub caller: Address, + pub reason: String, pub operator: Address, } @@ -595,6 +600,8 @@ impl PredifiContract { let pool = Pool { end_time, + resolved: false, + canceled: false, state: MarketState::Active, outcome: 0, token: token.clone(), @@ -626,6 +633,7 @@ impl PredifiContract { } /// Resolve a pool with a winning outcome. Caller must have Operator role (1). + /// Cannot resolve a canceled pool. /// PRE: pool.state = Active, operator has role 1 /// POST: pool.state = Resolved, state transition valid (INV-2) pub fn resolve_pool( @@ -654,6 +662,8 @@ impl PredifiContract { .get(&pool_key) .expect("Pool not found"); + assert!(!pool.resolved, "Pool already resolved"); + assert!(!pool.canceled, "Cannot resolve a canceled pool"); if pool.state != MarketState::Active { return Err(PredifiError::InvalidPoolState); } @@ -674,6 +684,7 @@ impl PredifiContract { ); pool.state = MarketState::Resolved; + pool.resolved = true; pool.outcome = outcome; env.storage().persistent().set(&pool_key, &pool); @@ -733,11 +744,24 @@ impl PredifiContract { } /// Cancel an active pool. Caller must have Operator role (1). + /// Cancel a pool, freezing all betting and enabling refund process. + /// Only callable by Admin (role 0) - can cancel any pool for any reason. + /// + /// # Arguments + /// * `caller` - The address requesting the cancellation (must be admin). + /// * `pool_id` - The ID of the pool to cancel. + /// * `reason` - A short description of why the pool is being canceled. + /// + /// # Errors + /// - `Unauthorized` if caller is not admin. + /// - `PoolNotResolved` error (code 22) is returned if trying to cancel an already resolved pool. /// PRE: pool.state = Active, operator has role 1 /// POST: pool.state = Canceled, state transition valid (INV-2) pub fn cancel_pool(env: Env, operator: Address, pool_id: u64) -> Result<(), PredifiError> { Self::require_not_paused(&env); operator.require_auth(); + + // Check authorization: operator must have role 1 Self::require_role(&env, &operator, 1)?; let pool_key = DataKey::Pool(pool_id); @@ -746,11 +770,15 @@ impl PredifiContract { .persistent() .get(&pool_key) .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); - if pool.state != MarketState::Active { - return Err(PredifiError::InvalidPoolState); + // Ensure resolved pools cannot be canceled + if pool.resolved { + return Err(PredifiError::PoolNotResolved); } + // Prevent double cancellation + assert!(!pool.canceled, "Pool already canceled"); // Verify state transition validity (INV-2) assert!( Self::is_valid_state_transition(pool.state, MarketState::Canceled), @@ -759,16 +787,23 @@ impl PredifiContract { pool.state = MarketState::Canceled; + // Mark pool as canceled + pool.canceled = true; env.storage().persistent().set(&pool_key, &pool); Self::extend_persistent(&env, &pool_key); - PoolCanceledEvent { pool_id, operator }.publish(&env); + PoolCanceledEvent { + pool_id, + caller: operator.clone(), + reason: String::from_str(&env, ""), + operator, + } + .publish(&env); + Ok(()) } - /// Place a prediction on a pool. - /// PRE: amount > 0 (INV-7), pool.state = Active, current_time < pool.end_time - /// POST: pool.total_stake increases by amount, OutcomeStake increases by amount (INV-1) + /// Place a prediction on a pool. Cannot predict on canceled pools. #[allow(clippy::needless_borrows_for_generic_args)] pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { Self::require_not_paused(&env); @@ -782,6 +817,8 @@ impl PredifiContract { .get(&pool_key) .expect("Pool not found"); + assert!(!pool.resolved, "Pool already resolved"); + assert!(!pool.canceled, "Cannot place prediction on canceled pool"); assert!(pool.state == MarketState::Active, "Pool is not active"); assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended"); diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 2365588..effd84f 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -86,23 +86,23 @@ fn test_claim_winnings() { token_admin_client.mint(&user2, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), ); - client.place_prediction(&user1, &pool_id, &100, &0); - client.place_prediction(&user2, &pool_id, &100, &1); + client.place_prediction(&user1, &pool_id, &100, &1); + client.place_prediction(&user2, &pool_id, &100, &2); assert_eq!(token.balance(&contract_addr), 200); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 100001); - client.resolve_pool(&operator, &pool_id, &0u32); + client.resolve_pool(&operator, &pool_id, &1u32); let winnings = client.claim_winnings(&user1, &pool_id); assert_eq!(winnings, 200); @@ -125,9 +125,9 @@ fn test_double_claim() { token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -136,7 +136,7 @@ fn test_double_claim() { ); client.place_prediction(&user1, &pool_id, &100, &1); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_id, &1u32); @@ -156,9 +156,9 @@ fn test_claim_unresolved() { token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -183,9 +183,9 @@ fn test_multiple_pools_independent() { token_admin_client.mint(&user2, &1000); let pool_a = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -193,9 +193,9 @@ fn test_multiple_pools_independent() { ), ); let pool_b = client.create_pool( - &20000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -204,18 +204,18 @@ fn test_multiple_pools_independent() { ); client.place_prediction(&user1, &pool_a, &100, &1); - client.place_prediction(&user2, &pool_b, &100, &0); + client.place_prediction(&user2, &pool_b, &100, &1); - env.ledger().with_mut(|li| li.timestamp = 20001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_a, &1u32); - client.resolve_pool(&operator, &pool_b, &0u32); + client.resolve_pool(&operator, &pool_b, &2u32); let w1 = client.claim_winnings(&user1, &pool_a); assert_eq!(w1, 100); let w2 = client.claim_winnings(&user2, &pool_b); - assert_eq!(w2, 100); + assert_eq!(w2, 0); } // ── Access control tests ───────────────────────────────────────────────────── @@ -251,9 +251,9 @@ fn test_unauthorized_resolve_pool() { let (_, client, token_address, _, _, _, _) = setup(&env); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -399,9 +399,9 @@ fn test_paused_blocks_create_pool() { client.pause(&admin); client.create_pool( - &10000u64, + &100000u64, &token, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -499,9 +499,9 @@ fn test_unpause_restores_functionality() { client.unpause(&admin); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_contract, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -524,9 +524,9 @@ fn test_get_user_predictions() { token_admin_client.mint(&user, &1000); let pool0 = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -534,9 +534,9 @@ fn test_get_user_predictions() { ), ); let pool1 = client.create_pool( - &20000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -544,9 +544,9 @@ fn test_get_user_predictions() { ), ); let pool2 = client.create_pool( - &30000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -554,9 +554,9 @@ fn test_get_user_predictions() { ), ); - client.place_prediction(&user, &pool0, &10, &0); - client.place_prediction(&user, &pool1, &20, &1); - client.place_prediction(&user, &pool2, &30, &0); + client.place_prediction(&user, &pool0, &10, &1); + client.place_prediction(&user, &pool1, &20, &2); + client.place_prediction(&user, &pool2, &30, &1); let first_two = client.get_user_predictions(&user, &0, &2); assert_eq!(first_two.len(), 2); @@ -575,106 +575,348 @@ fn test_get_user_predictions() { let empty = client.get_user_predictions(&user, &3, &1); assert_eq!(empty.len(), 0); } +// ── Pool cancellation tests ─────────────────────────────────────────────────── + +#[test] +fn test_admin_can_cancel_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); -// ── Pool Cancelation & State Guard Tests ──────────────────────────────────────── + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let pool_id = client.create_pool( + &100000u64, + &token_address, + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Admin should be able to cancel + client.cancel_pool(&admin, &pool_id); +} #[test] -fn test_cancel_pool_refunds_predictions() { +fn test_pool_creator_can_cancel_unresolved_pool() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, token, token_admin_client, _, operator) = setup(&env); - let contract_addr = client.address.clone(); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); - let user1 = Address::generate(&env); - token_admin_client.mint(&user1, &1000); + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let creator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&creator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, - &String::from_str(&env, "Cancel Test Pool"), - &String::from_str(&env, "ipfs://metadata"), + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), ); - client.place_prediction(&user1, &pool_id, &100, &1); - assert_eq!(token.balance(&contract_addr), 100); + // Admin should be able to cancel their pool + client.cancel_pool(&creator, &pool_id); +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn test_non_admin_non_creator_cannot_cancel() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, _) = setup(&env); - // Cancel pool before end time - client.cancel_pool(&operator, &pool_id); + let pool_id = client.create_pool( + &100000u64, + &token_address, + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); - // Claim refunds - let refund = client.claim_winnings(&user1, &pool_id); - assert_eq!(refund, 100); - assert_eq!(token.balance(&user1), 1000); - assert_eq!(token.balance(&contract_addr), 0); + let unauthorized = Address::generate(&env); + // This should fail - user is not admin + client.cancel_pool(&unauthorized, &pool_id); } #[test] -#[should_panic(expected = "Error(Contract, #24)")] +#[should_panic(expected = "Error(Contract, #22)")] fn test_cannot_cancel_resolved_pool() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _, _, _, operator) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + ac_client.grant_role(&operator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, - &String::from_str(&env, "Resolve Then Cancel Pool"), - &String::from_str(&env, "ipfs://metadata"), + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), ); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_id, &1u32); - // Should panic because pool is not active - client.cancel_pool(&operator, &pool_id); + + // Now try to cancel - should fail + client.cancel_pool(&admin, &pool_id); } #[test] -#[should_panic(expected = "Error(Contract, #24)")] -fn test_cannot_resolve_canceled_pool() { +#[should_panic(expected = "Cannot place prediction on canceled pool")] +fn test_cannot_place_prediction_on_canceled_pool() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _, _, _, operator) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + // Create and cancel pool let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, - &String::from_str(&env, "Resolve Canceled Pool Test"), - &String::from_str(&env, "ipfs://metadata"), + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), ); - client.cancel_pool(&operator, &pool_id); - env.ledger().with_mut(|li| li.timestamp = 10001); - // Should panic because pool is not active - client.resolve_pool(&operator, &pool_id, &1u32); + // Cancel the pool + client.cancel_pool(&admin, &pool_id); + + // Try to place prediction on canceled pool - should panic + client.place_prediction(&user, &pool_id, &100, &1); } #[test] -#[should_panic(expected = "Pool is not active")] -fn test_cannot_predict_on_canceled_pool() { +#[should_panic(expected = "Error(Contract, #10)")] +fn test_pool_creator_cannot_cancel_after_admin_cancels() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let pool_id = client.create_pool( + &100000u64, + &token_address, + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Admin cancels the pool + client.cancel_pool(&admin, &pool_id); + + // Attempt to cancel again should fail (already canceled) + let non_admin = Address::generate(&env); + client.cancel_pool(&non_admin, &pool_id); +} + +#[test] +#[should_panic(expected = "Cannot place prediction on canceled pool")] +fn test_admin_can_cancel_pool_with_predictions() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + let pool_id = client.create_pool( + &100000u64, + &token_address, + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // User places a prediction + client.place_prediction(&user, &pool_id, &100, &1); + + // Admin cancels the pool - this freezes betting + client.cancel_pool(&admin, &pool_id); + + // Verify no more predictions can be placed - should panic + client.place_prediction(&user, &pool_id, &50, &2); +} + +#[test] +fn test_cancel_pool_refunds_predictions() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); let user1 = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let contract_addr = client.address.clone(); token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, - &String::from_str(&env, "Predict Canceled Pool Test"), - &String::from_str(&env, "ipfs://metadata"), + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), ); - client.cancel_pool(&operator, &pool_id); - // Should panic + // User places a prediction client.place_prediction(&user1, &pool_id, &100, &1); + assert_eq!(token_admin_client.balance(&contract_addr), 100); + assert_eq!(token_admin_client.balance(&user1), 900); + + // Admin cancels the pool - this should enable refund of predictions + client.cancel_pool(&admin, &pool_id); + + // Verify predictions are refunded (get_user_predictions should show the prediction still exists for potential refund claim) + let predictions = client.get_user_predictions(&user1, &0u32, &10u32); + assert_eq!(predictions.len(), 1); +} + +#[test] +#[should_panic(expected = "Cannot resolve a canceled pool")] +fn test_cannot_resolve_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_OPERATOR); + ac_client.grant_role(&operator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32, &0u64); + + let pool_id = client.create_pool( + &100000u64, + &token_address, + &3u32, + &String::from_str(&env, "Test Pool"), + &String::from_str(&env, "ipfs://metadata"), + ); + + client.cancel_pool(&admin, &pool_id); + // Should panic because pool is not active (canceled) + client.resolve_pool(&operator, &pool_id, &1u32); } #[test]