| Test Name | Type | Lines | Purpose |
|---|---|---|---|
test_repeated_cancel_after_removal_consistent |
Unit | 959–984 | Basic idempotent cancel validation |
load_test_repeated_cancel_multiple_attempts |
Load | 991–1024 | Repeated cancels (N=5) consistency check |
test_cancel_then_execute_payment_consistent_error |
Integration | 1028–1054 | Cross-operation state validation |
test_repeated_cancel_multi_pair_no_cross_contamination |
Integration | 1058–1137 | Storage isolation and key handling |
test_repeated_cancel_no_extra_events |
Verification | 1141–1171 | Event stream purity |
prop_repeated_cancel_is_deterministic |
Property-Based | 1176–1198 | Mathematical proof across input space |
For any subscription that exists and is successfully cancelled:
1. First cancel() returns Ok(())
2. Second cancel() returns Err(NoActiveSubscription)
3. Third+ cancel() returns Err(NoActiveSubscription)
4. Subscription remains permanently removed
5. No state corruption occurs
6. No spurious events are emitted
| Scenario | Test(s) | Validation |
|---|---|---|
| Single retry | Test 1 | Basic idempotence |
| Multiple retries | Test 2 | Sustained determinism |
| Cross-operation | Test 3 | State consistency |
| Multi-pair | Test 4 | Isolation guarantee |
| Event cleanliness | Test 5 | Off-chain compatibility |
| Property-based | Test 6 | All valid inputs |
# Run all contract tests
cd SorobanPay/contracts/subscription
cargo test --lib
# Run only edge case tests
cargo test repeated_cancel
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_repeated_cancel_after_removal_consistent -- --exactAll tests use the test helper struct T:
struct T {
env: Env, // Soroban environment
client: SubscriptionProtocolClient,
subscriber: Address,
merchant: Address,
token: Address,
contract_id: Address,
}
// Key methods:
t.client.subscribe(...) // Create subscription
t.client.try_cancel(...) // Attempt cancel (returns Result)
t.client.execute_payment(...) // Execute payment
t.has_sub() // Check if subscription exists
t.get_sub() // Get subscription data
t.sub_bal() // Get subscriber token balance
t.mer_bal() // Get merchant token balance
t.advance(secs) // Advance ledger timeOperation Result Events
─────────────────────────────────────────────────────
subscribe() Ok(()) 1 event
↓
cancel() attempt #1 Ok(()) 1 event (cancel)
↓
cancel() attempt #2 Err(NoActiveSubscription) 0 events
↓
cancel() attempt #3 Err(NoActiveSubscription) 0 events
↓
... (all subsequent) Err(NoActiveSubscription) 0 events
After first cancel: Subscription removed from storage
After retry attempt: No tokens transferred
No state mutations
Subscription remains removed
assert!(result1.is_ok(), "first cancel must succeed");
assert!(matches!(result2, Err(Ok(ContractError::NoActiveSubscription))),
"second cancel must return NoActiveSubscription");for attempt in 2..=N {
assert!(matches!(result, Err(Ok(ContractError::NoActiveSubscription))));
}t.advance(ivl + 1); // Even after time passes
let result = t.client.try_execute_payment(...);
assert!(matches!(result, Err(Ok(ContractError::NoActiveSubscription))));// Cancel one pair
assert!(client.try_cancel(&sub1, &mer1).is_ok());
// Other pairs unaffected
assert!(has_subscription(&sub1, &mer2));
assert!(has_subscription(&sub2, &mer1));let events_before = t.env.events().all().len();
let _ = t.client.try_cancel(...); // fails
let events_after = t.env.events().all().len();
assert_eq!(events_before, events_after); // NO new eventsproptest!(|(amount in 1_i128..=100_000_i128,
interval in 86_400_u64..=31_536_000_u64)| {
// Test body runs 256 times with random values
prop_assert!(...);
});for (let attempt = 1; attempt <= MAX; attempt++) {
try {
await contract.cancel(subscriber, merchant);
// SUCCESS - whether first cancel or idempotent retry
return markAsCancelled();
} catch (error) {
if (error === "NoActiveSubscription") {
// Already cancelled - idempotent success
return markAsCancelled();
}
// Transient error - retry
if (attempt < MAX) await sleep(backoff());
else throw;
}
}-- Safe to assume: each subscription has at most 1 cancel event
-- If subscription_id not found in payments table and no cancel event,
-- it was either never created or TTL expired
SELECT * FROM cancel_events
WHERE subscription_id = $1; -- Returns 0 or 1 row- Detailed Explanation:
docs/edge-case-repeated-cancel.md - Implementation Summary:
IMPLEMENTATION_SUMMARY.md - Contract Code:
contracts/subscription/src/lib.rs(cancel function) - Error Codes:
contracts/subscription/src/error.rs
- ✅ All 6 tests compile cleanly
- ✅ Zero diagnostics/warnings
- ✅ Tests follow project conventions
- ✅ Each test is independent
- ✅ Tests use meaningful assertions
- ✅ Documentation is comprehensive
- ✅ Integration patterns are clear
Adding a new test?
- Add to the "Edge Case" section (lines 950+)
- Use the
Thelper struct for setup - Follow naming:
test_*(unit),load_test_*(stress),prop_*(property-based) - Add docstring explaining "what" and "why"
- Run
cargo testto verify
Modifying cancel() logic?
- Ensure first success case still works
- Ensure second+ attempts still return
NoActiveSubscription - Run full test suite:
cargo test --lib - Check that no new events are emitted on failures