Skip to content

Latest commit

 

History

History
239 lines (190 loc) · 6.7 KB

File metadata and controls

239 lines (190 loc) · 6.7 KB

Quick Reference: Edge Case Tests

New Tests Added to contracts/subscription/src/test.rs

Location: Lines 950–1198 (249 lines total)

Test Summary

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

What Gets Tested

Core Contract Guarantee

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

Coverage Matrix

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

How to Run Tests

# 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 -- --exact

Test Helpers Used

All 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 time

Expected Behavior

Cancel Sequence

Operation               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

Balances & State

After first cancel:    Subscription removed from storage
After retry attempt:   No tokens transferred
                       No state mutations
                       Subscription remains removed

Key Assertions in Tests

Test 1: Basic Idempotence

assert!(result1.is_ok(), "first cancel must succeed");
assert!(matches!(result2, Err(Ok(ContractError::NoActiveSubscription))), 
        "second cancel must return NoActiveSubscription");

Test 2: Multiple Retries

for attempt in 2..=N {
    assert!(matches!(result, Err(Ok(ContractError::NoActiveSubscription))));
}

Test 3: Cross-Operation

t.advance(ivl + 1);  // Even after time passes
let result = t.client.try_execute_payment(...);
assert!(matches!(result, Err(Ok(ContractError::NoActiveSubscription))));

Test 4: Isolation

// Cancel one pair
assert!(client.try_cancel(&sub1, &mer1).is_ok());

// Other pairs unaffected
assert!(has_subscription(&sub1, &mer2));
assert!(has_subscription(&sub2, &mer1));

Test 5: Event Purity

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 events

Test 6: Property-Based

proptest!(|(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!(...);
});

Off-Chain Integration Patterns

Pattern 1: Safe Retry Loop

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;
    }
}

Pattern 2: Event Stream Processing

-- 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

Related Documentation

  • 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

Verification Checklist

  • ✅ 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

Quick Start for Developers

Adding a new test?

  1. Add to the "Edge Case" section (lines 950+)
  2. Use the T helper struct for setup
  3. Follow naming: test_* (unit), load_test_* (stress), prop_* (property-based)
  4. Add docstring explaining "what" and "why"
  5. Run cargo test to verify

Modifying cancel() logic?

  1. Ensure first success case still works
  2. Ensure second+ attempts still return NoActiveSubscription
  3. Run full test suite: cargo test --lib
  4. Check that no new events are emitted on failures