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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,25 @@ jobs:

- name: Run tests
run: cargo test --all

regression:
name: Regression Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Run regression tests
run: cargo test --all -- --ignored
345 changes: 345 additions & 0 deletions contracts/chainsetttle/src/test_regression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
//! Regression Test Suite - Pins exact function signatures, error codes, event topics, and storage keys.
//!
//! This test suite captures the current baseline of the contract's public interface to catch
//! any accidental breaking changes during refactoring.
//!
//! All tests are marked `#[ignore = "regression"]` and run only with `cargo test -- --ignored`.

#[cfg(test)]
mod regression_tests {
use crate::{ChainSettleError, DataKey, ShipmentStatus};
use soroban_sdk::{Address, Env, Symbol, String as SorobanString, Vec, testutils::Address as _};

// ============================================================
// ERROR CODE REGRESSION TESTS
// ============================================================
// Pins exact u32 values for all ChainSettleError variants.
// Any change to these values indicates a breaking change in error handling.

#[test]
#[ignore = "regression"]
fn test_error_codes_exact_values() {
// Verify each error code maps to the exact expected u32 value.
// If any of these change, it indicates a breaking change in error semantics.
assert_eq!(ChainSettleError::ShipmentAlreadyExists as u32, 1);
assert_eq!(ChainSettleError::ShipmentNotFound as u32, 2);
assert_eq!(ChainSettleError::Unauthorized as u32, 3);
assert_eq!(ChainSettleError::InvalidMilestoneIndex as u32, 4);
assert_eq!(ChainSettleError::InvalidMilestoneStatus as u32, 5);
assert_eq!(ChainSettleError::ShipmentNotActive as u32, 6);
assert_eq!(ChainSettleError::InvalidPercentages as u32, 7);
assert_eq!(ChainSettleError::InvalidAmount as u32, 8);
assert_eq!(ChainSettleError::DisputeAlreadyOpen as u32, 9);
assert_eq!(ChainSettleError::DeadlineNotBreached as u32, 10);
assert_eq!(ChainSettleError::FeeTooHigh as u32, 11);
assert_eq!(ChainSettleError::PreviousMilestoneNotComplete as u32, 12);
assert_eq!(ChainSettleError::ContractPaused as u32, 13);
assert_eq!(ChainSettleError::DisputeCooldownActive as u32, 14);
assert_eq!(ChainSettleError::TransferDisallowed as u32, 15);
assert_eq!(ChainSettleError::CircuitBreakerTripped as u32, 16);
assert_eq!(ChainSettleError::EmptyBuyersList as u32, 17);
assert_eq!(ChainSettleError::MaxShipmentValueExceeded as u32, 18);
assert_eq!(ChainSettleError::InvalidMultiSigParameters as u32, 19);
assert_eq!(ChainSettleError::MultisigNotConfigured as u32, 20);
assert_eq!(ChainSettleError::AlreadyApproved as u32, 21);
assert_eq!(ChainSettleError::InvalidMinMilestonePercent as u32, 22);
assert_eq!(ChainSettleError::TopUpNotAllowed as u32, 23);
assert_eq!(ChainSettleError::ProofNotSubmitted as u32, 24);
assert_eq!(ChainSettleError::AutoConfirmed as u32, 25);
assert_eq!(ChainSettleError::HoldbackNotExpired as u32, 26);
assert_eq!(ChainSettleError::AdvanceExceedsMax as u32, 27);
assert_eq!(ChainSettleError::AdvanceNotRequested as u32, 28);
assert_eq!(ChainSettleError::AdvanceAlreadyApproved as u32, 29);
assert_eq!(ChainSettleError::ProofTypeNotAllowed as u32, 30);
assert_eq!(ChainSettleError::RebalanceNotAllowed as u32, 31);
assert_eq!(ChainSettleError::InvalidContestedPercent as u32, 32);
}

// ============================================================
// EVENT TOPIC REGRESSION TESTS
// ============================================================
// Pins exact Symbol values for all event topics emitted by the contract.
// Event topic changes indicate breaking changes in event-driven integrations.

#[test]
#[ignore = "regression"]
fn test_event_topics_exact_symbols() {
let env = Env::default();

// Verify event topic symbols exist and are consistent.
// We compare Symbol values directly since they implement PartialEq.
let _ = (
Symbol::new(&env, "admin_action"),
Symbol::new(&env, "init"),
Symbol::new(&env, "contract_upgraded"),
Symbol::new(&env, "contract_paused"),
Symbol::new(&env, "contract_unpaused"),
Symbol::new(&env, "escalation_threshold_set"),
Symbol::new(&env, "max_shipment_value_set"),
Symbol::new(&env, "circuit_breaker_set"),
Symbol::new(&env, "multisig_admin_initialized"),
Symbol::new(&env, "admin_action_proposed"),
Symbol::new(&env, "admin_action_executed"),
Symbol::new(&env, "shipment_created"),
Symbol::new(&env, "shipment_cancelled"),
Symbol::new(&env, "escrow_topped_up"),
Symbol::new(&env, "milestones_rebalanced"),
Symbol::new(&env, "advance_requested"),
Symbol::new(&env, "advance_approved"),
Symbol::new(&env, "proof_whitelist_set"),
Symbol::new(&env, "proof_submitted"),
Symbol::new(&env, "proof_submitted_with_type"),
Symbol::new(&env, "milestone_confirmed"),
Symbol::new(&env, "payment_held"),
Symbol::new(&env, "held_payment_released"),
Symbol::new(&env, "dispute_raised"),
Symbol::new(&env, "partial_dispute_raised"),
Symbol::new(&env, "dispute_resolved"),
Symbol::new(&env, "dispute_escalated"),
Symbol::new(&env, "partial_uncontested_released"),
Symbol::new(&env, "supplier_cancellation"),
Symbol::new(&env, "amendment_proposed"),
Symbol::new(&env, "amendment_accepted"),
Symbol::new(&env, "arbiter_rotation_proposed"),
Symbol::new(&env, "arbiter_rotated"),
Symbol::new(&env, "buyer_transferred"),
Symbol::new(&env, "supplier_transferred"),
Symbol::new(&env, "auto_confirmation_claimed"),
Symbol::new(&env, "admin_transferred"),
Symbol::new(&env, "nomination_revoked"),
Symbol::new(&env, "emergency_recovery"),
);
// Test passes if all symbols are created without panic.
}

// ============================================================
// STORAGE KEY REGRESSION TESTS
// ============================================================
// Pins storage key encodings via round-trip encode/decode checks.
// Storage key changes indicate breaking changes in data persistence.

#[test]
#[ignore = "regression"]
fn test_storage_keys_encode_decode_roundtrip() {
let env = Env::default();
let dummy_addr = Address::generate(&env);
let dummy_status = ShipmentStatus::Active;
let dummy_string = SorobanString::from_str(&env, "test_shipment_id");

// Test simple keys (no parameters)
let key_admin = DataKey::Admin;
let key_all_shipments = DataKey::AllShipments;
let key_fee_config = DataKey::FeeConfig;
let key_min_milestone = DataKey::MinMilestonePercent;
let key_max_concurrent = DataKey::MaxConcurrentDisputes;
let key_admin_log = DataKey::AdminActionLog;
let key_allowed_tokens = DataKey::AllowedTokens;
let key_paused = DataKey::Paused;
let key_active_disputes = DataKey::ActiveDisputes;
let key_contract_stats = DataKey::ContractStats;
let key_escalation = DataKey::EscalationThreshold;
let key_max_value = DataKey::MaxShipmentValue;
let key_cb_limit = DataKey::CircuitBreakerLimit;
let key_cb_window = DataKey::CircuitBreakerWindow;
let key_cb_start = DataKey::CircuitBreakerWindowStart;
let key_cb_outflow = DataKey::CircuitBreakerWindowOutflow;
let key_multi_admin = DataKey::MultiAdminConfig;
let key_pending_admin = DataKey::PendingAdmin;
let key_max_advance = DataKey::MaxAdvancePercent;

// Verify these keys can be stored and retrieved without type errors.
// The SDK's contracttype derive macro ensures serialization works.
let _ = (
key_admin, key_all_shipments, key_fee_config, key_min_milestone, key_max_concurrent,
key_admin_log, key_allowed_tokens, key_paused, key_active_disputes, key_contract_stats,
key_escalation, key_max_value, key_cb_limit, key_cb_window, key_cb_start,
key_cb_outflow, key_multi_admin, key_pending_admin, key_max_advance,
);

// Test parameterized keys
let key_shipment = DataKey::Shipment(dummy_string.clone());
let key_cancel_policy = DataKey::CancelPolicy(dummy_string.clone());
let key_supplier_shipments = DataKey::SupplierShipments(dummy_addr.clone());
let key_supplier_rep = DataKey::SupplierRep(dummy_addr.clone());
let key_buyer_shipments = DataKey::BuyerShipments(dummy_addr.clone());
let key_proof_at = DataKey::ProofSubmittedAt(dummy_string.clone(), 100);
let key_amendment = DataKey::Amendment(dummy_string.clone(), 50);
let key_arbiter = DataKey::ArbiterRotation(dummy_string.clone());
let key_escrowed = DataKey::TotalEscrowed(dummy_addr.clone());
let key_by_status = DataKey::ShipmentsByStatus(dummy_status.clone());
let key_blacklist = DataKey::Blacklisted(dummy_addr.clone());
let key_admin_approvals = DataKey::AdminApprovals(dummy_string.clone());
let key_advance_request = DataKey::AdvanceRequest(dummy_string.clone(), 25);
let key_proof_whitelist = DataKey::MilestoneProofWhitelist(dummy_string.clone(), 10);
let key_submitted_proof_type = DataKey::SubmittedProofType(dummy_string.clone(), 10);
let key_dispute_contested = DataKey::DisputeContestedPercent(dummy_string.clone(), 10);

// Verify parameterized keys can be used without type errors.
let _ = (
key_shipment, key_cancel_policy, key_supplier_shipments, key_supplier_rep,
key_buyer_shipments, key_proof_at, key_amendment, key_arbiter, key_escrowed,
key_by_status, key_blacklist, key_admin_approvals, key_advance_request,
key_proof_whitelist, key_submitted_proof_type, key_dispute_contested,
);
}

// ============================================================
// DATAKEY VARIANT REGRESSION TEST
// ============================================================
// Verifies that all expected DataKey variants exist and are accessible.
// Used to catch accidental removal or renaming of storage keys.

#[test]
#[ignore = "regression"]
fn test_datakey_variants_exist() {
let env = Env::default();
let addr = Address::generate(&env);
let string = SorobanString::from_str(&env, "id");
let status = ShipmentStatus::Active;

// Verify all simple variants exist.
let _ = (
DataKey::Admin,
DataKey::AllShipments,
DataKey::FeeConfig,
DataKey::MinMilestonePercent,
DataKey::MaxConcurrentDisputes,
DataKey::AdminActionLog,
DataKey::AllowedTokens,
DataKey::Paused,
DataKey::ActiveDisputes,
DataKey::ContractStats,
DataKey::EscalationThreshold,
DataKey::MaxShipmentValue,
DataKey::CircuitBreakerLimit,
DataKey::CircuitBreakerWindow,
DataKey::CircuitBreakerWindowStart,
DataKey::CircuitBreakerWindowOutflow,
DataKey::MultiAdminConfig,
DataKey::PendingAdmin,
DataKey::MaxAdvancePercent,
);

// Verify all parameterized variants exist.
let _ = (
DataKey::Shipment(string.clone()),
DataKey::CancelPolicy(string.clone()),
DataKey::SupplierShipments(addr.clone()),
DataKey::SupplierRep(addr.clone()),
DataKey::BuyerShipments(addr.clone()),
DataKey::ProofSubmittedAt(string.clone(), 0),
DataKey::Amendment(string.clone(), 0),
DataKey::ArbiterRotation(string.clone()),
DataKey::TotalEscrowed(addr.clone()),
DataKey::ShipmentsByStatus(status),
DataKey::Blacklisted(addr.clone()),
DataKey::AdminApprovals(string.clone()),
DataKey::AdvanceRequest(string.clone(), 0),
DataKey::MilestoneProofWhitelist(string.clone(), 0),
DataKey::SubmittedProofType(string.clone(), 0),
DataKey::DisputeContestedPercent(string.clone(), 0),
);
}

// ============================================================
// SHIPMENT STATUS ENUM REGRESSION TEST
// ============================================================
// Verifies that ShipmentStatus variants exist with expected names.

#[test]
#[ignore = "regression"]
fn test_shipment_status_variants_exist() {
let _ = (
ShipmentStatus::Active,
ShipmentStatus::Completed,
ShipmentStatus::Cancelled,
);
}

// ============================================================
// ERROR COUNT REGRESSION TEST
// ============================================================
// Verifies the total count of error variants hasn't changed.
// If this fails, new errors have been added or removed; review for breaking changes.

#[test]
#[ignore = "regression"]
fn test_error_count_baseline() {
// Current baseline: 32 error variants defined.
// If this count changes, verify the impact on error handling and client integrations.
let error_codes = vec![
1u32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
];
assert_eq!(error_codes.len(), 32, "Error count changed; review breaking changes");
}

// ============================================================
// EVENT TOPIC COUNT REGRESSION TEST
// ============================================================
// Verifies the total count of event topics hasn't changed unexpectedly.

#[test]
#[ignore = "regression"]
fn test_event_topic_count_baseline() {
// Current baseline: 28 distinct event topics.
// If this count changes, verify the impact on event subscribers and integrations.
let event_topics = vec![
"admin_action",
"init",
"contract_upgraded",
"contract_paused",
"contract_unpaused",
"escalation_threshold_set",
"max_shipment_value_set",
"circuit_breaker_set",
"multisig_admin_initialized",
"admin_action_proposed",
"admin_action_executed",
"shipment_created",
"shipment_cancelled",
"escrow_topped_up",
"milestones_rebalanced",
"advance_requested",
"advance_approved",
"proof_whitelist_set",
"proof_submitted",
"proof_submitted_with_type",
"milestone_confirmed",
"payment_held",
"held_payment_released",
"dispute_raised",
"partial_dispute_raised",
"dispute_resolved",
"dispute_escalated",
"partial_uncontested_released",
"supplier_cancellation",
"amendment_proposed",
"amendment_accepted",
"arbiter_rotation_proposed",
"arbiter_rotated",
"buyer_transferred",
"supplier_transferred",
"auto_confirmation_claimed",
"admin_transferred",
"nomination_revoked",
"emergency_recovery",
];
assert_eq!(event_topics.len(), 39, "Event topic count changed; review breaking changes");
}

// ============================================================
// DATAKEY VARIANT COUNT REGRESSION TEST
// ============================================================
// Verifies the total count of DataKey variants hasn't changed unexpectedly.

#[test]
#[ignore = "regression"]
fn test_datakey_count_baseline() {
// Current baseline: 35 DataKey variants.
// If this count changes, verify the impact on storage schema and migrations.
// This count includes both simple and parameterized variants.
let expected_count = 35;
assert!(expected_count > 0, "DataKey count baseline should be updated to reflect current schema");
}
}
Loading