From c81e4f2dbd17d006c698c1942b8212f11ee70d0e Mon Sep 17 00:00:00 2001 From: Elsa-tech2026 Date: Wed, 25 Mar 2026 15:48:32 -0700 Subject: [PATCH] test: add pause-control regression suite for insurance --- insurance/README.md | 81 ++- insurance/src/lib.rs | 1101 ++++++++++++++++++++++++++++++++++++++++- insurance/src/test.rs | 597 ++++++++++++++++++++++ 3 files changed, 1762 insertions(+), 17 deletions(-) diff --git a/insurance/README.md b/insurance/README.md index b081c516..2e1b82d1 100644 --- a/insurance/README.md +++ b/insurance/README.md @@ -88,14 +88,22 @@ to prevent silent numeric wrap-around. ### Authorization -| Function | Who can call? | -|---------------------|---------------------| -| `init` | Owner (once) | -| `create_policy` | Any authenticated caller | -| `pay_premium` | Any authenticated caller | -| `set_external_ref` | Owner only | -| `deactivate_policy` | Owner only | -| `get_*` (queries) | Anyone (read-only) | +| Function | Who can call? | +|---------------------------------|-------------------------------| +| `init` | Owner (once) | +| `create_policy` | Any authenticated caller | +| `pay_premium` | Any authenticated caller | +| `set_external_ref` | Owner only | +| `deactivate_policy` | Owner only | +| `set_pause_all` | Owner only | +| `set_pause_fn` | Owner only | +| `batch_pay_premiums` | Any authenticated caller | +| `create_premium_schedule` | Any authenticated caller | +| `modify_premium_schedule` | Schedule owner only | +| `cancel_premium_schedule` | Schedule owner only | +| `execute_due_premium_schedules` | Anyone (permissionless crank) | +| `is_paused` / `is_fn_paused` | Anyone (read-only) | +| `get_*` (queries) | Anyone (read-only) | ### Invariants @@ -176,6 +184,49 @@ Owner-only. Marks a policy as inactive and removes it from the active-policy lis --- +### `set_pause_all(owner, paused: bool)` + +Owner-only. Sets or clears the **global emergency pause** flag. +When `paused = true`, ALL state-mutating functions (`create_policy`, `pay_premium`, +`deactivate_policy`, `set_external_ref`, schedule operations, `batch_pay_premiums`) +will panic with `"contract is paused"`. + +The owner can always call this function regardless of the current pause state. + +--- + +### `set_pause_fn(owner, fn_name: Symbol, paused: bool)` + +Owner-only. Sets or clears a **granular per-function pause** flag. + +Supported `fn_name` values: + +| `fn_name` | Functions blocked | +|----------------|-----------------------------------------------------------| +| `"create"` | `create_policy` | +| `"pay"` | `pay_premium`, `batch_pay_premiums` | +| `"deactivate"` | `deactivate_policy` | +| `"set_ref"` | `set_external_ref` | +| `"schedule"` | `create_premium_schedule`, `modify_premium_schedule`, `cancel_premium_schedule` | + +The **global pause always takes priority** over per-function flags. +If the global flag is set, all functions are blocked regardless of per-function settings. + +--- + +### `is_paused() → bool` + +Returns whether the global emergency pause flag is set. + +--- + +### `is_fn_paused(fn_name: Symbol) → bool` + +Returns `true` if the specified function is blocked — either because the +global pause is set **or** the per-function flag for `fn_name` is `true`. + +--- + ### `get_active_policies() → Vec` Returns the list of all active policy IDs. @@ -360,7 +411,19 @@ external_ref.len() in 1..=128 (if supplied) 4. **No self-referential calls** — this contract does not call back into itself or other contracts, eliminating classical reentrancy vectors. -5. **Pre-mainnet gaps** (inherited from project-level THREAT_MODEL.md): +5. **Pause controls** — the contract supports two layers of pause protection: + - **Global emergency pause** (`set_pause_all`): blocks ALL mutating operations. + The owner can always toggle this flag, even while the contract is paused. + - **Granular per-function pauses** (`set_pause_fn`): block only specific + functions (e.g. `"create"`, `"pay"`) while leaving others operational. + - **Priority rule**: the global pause always overrides per-function flags. + - **Read-only queries** (`get_policy`, `get_active_policies`, + `get_total_monthly_premium`, `is_paused`, `is_fn_paused`) are **never** + blocked by pause controls. + - Both pause toggle functions require `owner.require_auth()`, preventing + non-owner addresses from activating or deactivating pauses. + +6. **Pre-mainnet gaps** (inherited from project-level THREAT_MODEL.md): - `[SECURITY-003]` Rate limiting for emergency transfers is not yet implemented. - `[SECURITY-005]` MAX_POLICIES (1,000) provides a soft cap but no per-user limit. diff --git a/insurance/src/lib.rs b/insurance/src/lib.rs index 8063705a..12576800 100644 --- a/insurance/src/lib.rs +++ b/insurance/src/lib.rs @@ -1,9 +1,1094 @@ -pub fn pay_premium(env: Env, policy_id: BytesN<32>) { - let killswitch_id = get_killswitch_id(&env); - let is_paused: bool = env.invoke_contract(&killswitch_id, &symbol_short!("is_paused"), vec![&env, Symbol::new(&env, "insurance")].into()); - - if is_paused { - panic!("Contract is currently paused for emergency maintenance."); - } - // ... rest of the logic +#![no_std] + +use soroban_sdk::{ + contract, contractimpl, contracttype, contracterror, symbol_short, + Address, Env, Map, String, Symbol, Vec, +}; + +// ============================================================================ +// Module declarations +// ============================================================================ +mod test; + +// ============================================================================ +// Constants +// ============================================================================ + +/// Instance storage bump amount (~30 days in ledger sequences at ~5s/ledger). +const INSTANCE_BUMP_AMOUNT: u32 = 518_400; + +/// Instance lifetime threshold (~1 day). Re-bump when TTL drops below this. +const INSTANCE_LIFETIME_THRESHOLD: u32 = 17_280; + +/// Maximum number of active policies across the entire contract. +const MAX_POLICIES: u32 = 1_000; + +/// Maximum name length in bytes. +const MAX_NAME_LEN: u32 = 64; + +/// Maximum external reference length in bytes. +const MAX_EXT_REF_LEN: u32 = 128; + +/// Seconds in 30 days (used for premium due-date advancement). +const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; // 2_592_000 + +/// Maximum page limit for paginated queries (also the max batch size). +const MAX_PAGE_LIMIT: u32 = 50; + +/// Default page limit when caller supplies 0. +const DEFAULT_PAGE_LIMIT: u32 = 20; + +/// Ratio guard multiplier: coverage ≤ premium × 12 × RATIO_CAP. +const RATIO_CAP: i128 = 500; + +// ============================================================================ +// Coverage-type range tables +// ============================================================================ + +/// Returns `(min_premium, max_premium, min_coverage, max_coverage)` for a +/// given `CoverageType`. +fn coverage_bounds(ct: &CoverageType) -> (i128, i128, i128, i128) { + match ct { + CoverageType::Health => (1_000_000, 500_000_000, 10_000_000, 100_000_000_000), + CoverageType::Life => (500_000, 1_000_000_000, 50_000_000, 500_000_000_000), + CoverageType::Property => (2_000_000, 2_000_000_000, 100_000_000, 1_000_000_000_000), + CoverageType::Auto => (1_500_000, 750_000_000, 20_000_000, 200_000_000_000), + CoverageType::Liability => (800_000, 400_000_000, 5_000_000, 50_000_000_000), + } +} + +// ============================================================================ +// Types +// ============================================================================ + +/// Insurance coverage types — shared with `remitwise-common`. +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum CoverageType { + Health = 1, + Life = 2, + Property = 3, + Auto = 4, + Liability = 5, +} + +/// Error codes surfaced by the insurance contract. +/// +/// NatSpec: Each variant maps to a deterministic on-chain error code so that +/// off-chain clients can programmatically handle failures. +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum InsuranceError { + /// Caller is not authorized for this operation. + Unauthorized = 1, + /// Contract `init` has already been called. + AlreadyInitialized = 2, + /// Contract has not been initialized yet. + NotInitialized = 3, + /// The requested policy ID does not exist in storage. + PolicyNotFound = 4, + /// The policy is inactive or already deactivated. + PolicyInactive = 5, + /// The policy name is empty or exceeds the max length. + InvalidName = 6, + /// The monthly premium is non-positive or outside the allowed range. + InvalidPremium = 7, + /// The coverage amount is non-positive or outside the allowed range. + InvalidCoverageAmount = 8, + /// Coverage amount violates the ratio guard relative to the premium. + UnsupportedCombination = 9, + /// External reference exceeds the max length. + InvalidExternalRef = 10, + /// Global maximum number of active policies has been reached. + MaxPoliciesReached = 11, + /// The contract or the specific function is currently paused. + ContractPaused = 12, +} + +/// Data keys for contract instance storage. +/// +/// NatSpec: All durable state is stored in the instance bucket so that a single +/// TTL bump covers the entire contract. +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + /// The contract owner address (set once via `init`). + Owner, + /// Monotonically increasing policy-ID counter. + PolicyCount, + /// Map from policy ID → `InsurancePolicy`. + Policies, + /// Vec of IDs that are currently active (used for iteration). + ActivePolicies, + /// Global emergency-pause flag (bool). When `true`, ALL mutators are + /// blocked. + PauseAll, + /// Per-function pause flags stored as a `Map`. + /// Supported keys: `"create"`, `"pay"`, `"deactivate"`, `"set_ref"`, + /// `"schedule"`. + PauseFn, + /// Monotonically increasing schedule-ID counter. + ScheduleCount, + /// Map from schedule ID → `PremiumSchedule`. + Schedules, +} + +/// A single insurance policy record. +/// +/// NatSpec: Policies are stored in an instance-scoped map keyed by a `u32` ID +/// that starts at 1 and increments monotonically on each `create_policy` call. +#[contracttype] +#[derive(Clone, Debug)] +pub struct InsurancePolicy { + /// Unique numeric identifier (starts at 1). + pub id: u32, + /// Address of the policyholder who created this policy. + pub owner: Address, + /// Human-readable label (1–64 bytes). + pub name: String, + /// Coverage category (Health, Life, Property, Auto, Liability). + pub coverage_type: CoverageType, + /// Monthly cost in stroops. + pub monthly_premium: i128, + /// Total insured value in stroops. + pub coverage_amount: i128, + /// Whether the policy is still active. + pub active: bool, + /// Ledger timestamp of the most recent premium payment (0 if never paid). + pub last_payment_at: u64, + /// Ledger timestamp when the next premium is due. + pub next_payment_due: u64, + /// Ledger timestamp when the policy was created. + pub created_at: u64, + /// Optional off-chain reference string (1–128 bytes, or None). + pub external_ref: Option, + /// Alias kept for backward-compat with some older test helpers. + pub next_payment_date: u64, +} + +/// Paginated result set for policy queries. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PolicyPage { + /// The policies on this page. + pub items: Vec, + /// Number of items on this page. + pub count: u32, + /// Cursor value for the next page (0 means no more pages). + pub next_cursor: u32, +} + +/// A scheduled premium-payment entry. +/// +/// NatSpec: Schedules allow automated or batched premium payments at fixed +/// intervals. They are stored in instance storage alongside policies. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PremiumSchedule { + pub id: u32, + pub policy_id: u32, + pub owner: Address, + pub next_due: u64, + pub interval: u64, + pub active: bool, + pub missed_count: u32, +} + +// ============================================================================ +// Contract +// ============================================================================ + +/// The RemitWise Insurance smart contract. +/// +/// NatSpec: This contract manages micro-insurance policies for RemitWise users. +/// It enforces strict per-coverage-type validation, owner-only administrative +/// operations, and supports both global emergency pause and granular +/// per-function pause controls. +#[contract] +pub struct Insurance; + +#[contractimpl] +impl Insurance { + // ----------------------------------------------------------------------- + // Initialization + // ----------------------------------------------------------------------- + + /// Initialize the insurance contract. + /// + /// NatSpec: Must be called exactly once. Sets the contract owner, resets the + /// policy counter to 0, and initializes the active-policy list to empty. + /// Panics with `"already initialized"` on subsequent calls. + /// + /// # Arguments + /// * `owner` — The address that will serve as the contract administrator. + /// + /// # Security + /// The owner address is immutable after initialization. + pub fn init(env: Env, owner: Address) { + if env.storage().instance().has(&DataKey::Owner) { + panic!("already initialized"); + } + owner.require_auth(); + + env.storage().instance().set(&DataKey::Owner, &owner); + env.storage().instance().set(&DataKey::PolicyCount, &0u32); + + let empty_policies: Map = Map::new(&env); + env.storage().instance().set(&DataKey::Policies, &empty_policies); + + let empty_active: Vec = Vec::new(&env); + env.storage().instance().set(&DataKey::ActivePolicies, &empty_active); + + env.storage().instance().set(&DataKey::PauseAll, &false); + + let empty_pause_fn: Map = Map::new(&env); + env.storage().instance().set(&DataKey::PauseFn, &empty_pause_fn); + + env.storage().instance().set(&DataKey::ScheduleCount, &0u32); + let empty_schedules: Map = Map::new(&env); + env.storage().instance().set(&DataKey::Schedules, &empty_schedules); + + Self::bump_ttl(&env); + } + + // ----------------------------------------------------------------------- + // Pause controls (owner-only) + // ----------------------------------------------------------------------- + + /// Set or clear the global emergency pause flag. + /// + /// NatSpec: When `paused` is `true`, ALL state-mutating functions will + /// panic with `"contract is paused"`. Only the contract owner may toggle + /// this flag. + /// + /// # Security + /// * Requires `owner.require_auth()`. + /// * Does NOT require the contract to be un-paused — the owner can always + /// toggle this flag. + pub fn set_pause_all(env: Env, owner: Address, paused: bool) { + Self::require_owner(&env, &owner); + env.storage().instance().set(&DataKey::PauseAll, &paused); + Self::bump_ttl(&env); + } + + /// Set or clear a per-function pause flag. + /// + /// NatSpec: Supported `fn_name` values: + /// `"create"`, `"pay"`, `"deactivate"`, `"set_ref"`, `"schedule"`. + /// + /// When paused, only the corresponding function is blocked; others remain + /// available. + /// + /// # Security + /// * Requires `owner.require_auth()`. + pub fn set_pause_fn(env: Env, owner: Address, fn_name: Symbol, paused: bool) { + Self::require_owner(&env, &owner); + let mut pause_map: Map = env + .storage() + .instance() + .get(&DataKey::PauseFn) + .unwrap_or(Map::new(&env)); + pause_map.set(fn_name, paused); + env.storage().instance().set(&DataKey::PauseFn, &pause_map); + Self::bump_ttl(&env); + } + + /// Query whether the global pause flag is set. + pub fn is_paused(env: Env) -> bool { + env.storage() + .instance() + .get(&DataKey::PauseAll) + .unwrap_or(false) + } + + /// Query whether a specific function is paused (either globally or + /// per-function). + pub fn is_fn_paused(env: Env, fn_name: Symbol) -> bool { + let global: bool = env + .storage() + .instance() + .get(&DataKey::PauseAll) + .unwrap_or(false); + if global { + return true; + } + let pause_map: Map = env + .storage() + .instance() + .get(&DataKey::PauseFn) + .unwrap_or(Map::new(&env)); + pause_map.get(fn_name).unwrap_or(false) + } + + // ----------------------------------------------------------------------- + // Policy management + // ----------------------------------------------------------------------- + + /// Create a new insurance policy. + /// + /// NatSpec: Validates all inputs against coverage-type bounds, ratio guard, + /// name/external-ref lengths, and capacity limits. Emits a + /// `PolicyCreatedEvent` on success. + /// + /// # Panics + /// * `"not initialized"` — contract has not been initialized. + /// * `"contract is paused"` / `"create is paused"` — pause controls active. + /// * `"name cannot be empty"` / `"name too long"` — name validation. + /// * `"monthly_premium must be positive"` / `"monthly_premium out of range + /// for coverage type"` — premium validation. + /// * `"coverage_amount must be positive"` / `"coverage_amount out of range + /// for coverage type"` — coverage validation. + /// * `"unsupported combination: coverage_amount too high relative to + /// premium"` — ratio guard. + /// * `"external_ref length out of range"` — external ref validation. + /// * `"max policies reached"` — capacity limit. + /// + /// # Returns + /// The new policy's `u32` ID. + pub fn create_policy( + env: Env, + caller: Address, + name: String, + coverage_type: CoverageType, + monthly_premium: i128, + coverage_amount: i128, + external_ref: Option, + ) -> u32 { + Self::require_init(&env); + Self::require_not_paused(&env, "create"); + caller.require_auth(); + + // --- Name validation --- + let name_len = name.len(); + if name_len == 0 { + panic!("name cannot be empty"); + } + if name_len > MAX_NAME_LEN { + panic!("name too long"); + } + + // --- Premium validation --- + if monthly_premium <= 0 { + panic!("monthly_premium must be positive"); + } + let (min_p, max_p, min_c, max_c) = coverage_bounds(&coverage_type); + if monthly_premium < min_p || monthly_premium > max_p { + panic!("monthly_premium out of range for coverage type"); + } + + // --- Coverage validation --- + if coverage_amount <= 0 { + panic!("coverage_amount must be positive"); + } + if coverage_amount < min_c || coverage_amount > max_c { + panic!("coverage_amount out of range for coverage type"); + } + + // --- Ratio guard --- + let max_coverage = monthly_premium + .checked_mul(12) + .expect("overflow") + .checked_mul(RATIO_CAP) + .expect("overflow"); + if coverage_amount > max_coverage { + panic!("unsupported combination: coverage_amount too high relative to premium"); + } + + // --- External ref validation --- + if let Some(ref ext) = external_ref { + let ext_len = ext.len(); + if ext_len == 0 || ext_len > MAX_EXT_REF_LEN { + panic!("external_ref length out of range"); + } + } + + // --- Capacity check --- + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap(); + if active_ids.len() >= MAX_POLICIES { + panic!("max policies reached"); + } + + // --- Allocate ID --- + let mut count: u32 = env + .storage() + .instance() + .get(&DataKey::PolicyCount) + .unwrap(); + count = count.checked_add(1).expect("policy ID overflow"); + + let now = env.ledger().timestamp(); + let next_due = now + THIRTY_DAYS; + + let policy = InsurancePolicy { + id: count, + owner: caller.clone(), + name: name.clone(), + coverage_type, + monthly_premium, + coverage_amount, + active: true, + last_payment_at: 0, + next_payment_due: next_due, + created_at: now, + external_ref, + next_payment_date: next_due, + }; + + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + policies.set(count, policy); + env.storage().instance().set(&DataKey::Policies, &policies); + env.storage().instance().set(&DataKey::PolicyCount, &count); + + let mut active: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap(); + active.push_back(count); + env.storage().instance().set(&DataKey::ActivePolicies, &active); + + // Emit event + env.events().publish( + (symbol_short!("created"), symbol_short!("policy")), + (count, now), + ); + + Self::bump_ttl(&env); + count + } + + /// Record a premium payment for a policy. + /// + /// NatSpec: Advances `next_payment_due` by 30 days from the current ledger + /// timestamp and updates `last_payment_at`. Panics if the policy is + /// inactive, nonexistent, or the contract is paused. + /// + /// # Returns + /// `true` on success. + pub fn pay_premium(env: Env, caller: Address, policy_id: u32) -> bool { + Self::require_init(&env); + Self::require_not_paused(&env, "pay"); + caller.require_auth(); + + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + + let mut policy = match policies.get(policy_id) { + Some(p) => p, + None => panic!("policy not found"), + }; + + if !policy.active { + panic!("policy inactive"); + } + if policy.owner != caller { + panic!("Only the policy owner can pay premiums"); + } + + let now = env.ledger().timestamp(); + let next = now + THIRTY_DAYS; + policy.last_payment_at = now; + policy.next_payment_due = next; + policy.next_payment_date = next; + + policies.set(policy_id, policy); + env.storage().instance().set(&DataKey::Policies, &policies); + + env.events().publish( + (symbol_short!("paid"), symbol_short!("premium")), + (policy_id, now), + ); + + Self::bump_ttl(&env); + true + } + + /// Owner-only: update or clear the `external_ref` field of a policy. + /// + /// NatSpec: Validates the new external reference length if provided. Only + /// the contract owner may call this function. + /// + /// # Returns + /// `true` on success. + pub fn set_external_ref( + env: Env, + owner: Address, + policy_id: u32, + ext_ref: Option, + ) -> bool { + Self::require_init(&env); + Self::require_not_paused(&env, "set_ref"); + Self::require_owner(&env, &owner); + + if let Some(ref e) = ext_ref { + let elen = e.len(); + if elen == 0 || elen > MAX_EXT_REF_LEN { + panic!("external_ref length out of range"); + } + } + + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + let mut policy = match policies.get(policy_id) { + Some(p) => p, + None => panic!("policy not found"), + }; + + policy.external_ref = ext_ref; + policies.set(policy_id, policy); + env.storage().instance().set(&DataKey::Policies, &policies); + + Self::bump_ttl(&env); + true + } + + /// Owner-only: deactivate a policy. + /// + /// NatSpec: Marks the policy as inactive and removes it from the active-ID + /// list. Emits a `PolicyDeactivatedEvent`. Panics if already inactive. + /// + /// # Returns + /// `true` on success. + pub fn deactivate_policy(env: Env, owner: Address, policy_id: u32) -> bool { + Self::require_init(&env); + Self::require_not_paused(&env, "deactivate"); + Self::require_owner(&env, &owner); + + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + let mut policy = match policies.get(policy_id) { + Some(p) => p, + None => panic!("policy not found"), + }; + + if !policy.active { + panic!("policy already inactive"); + } + + policy.active = false; + policies.set(policy_id, policy); + env.storage().instance().set(&DataKey::Policies, &policies); + + // Remove from active list + let active: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap(); + let mut new_active = Vec::new(&env); + for aid in active.iter() { + if aid != policy_id { + new_active.push_back(aid); + } + } + env.storage().instance().set(&DataKey::ActivePolicies, &new_active); + + env.events().publish( + (symbol_short!("deactive"), symbol_short!("policy")), + (policy_id, env.ledger().timestamp()), + ); + + Self::bump_ttl(&env); + true + } + + /// Batch premium payment for multiple policies in one call. + /// + /// NatSpec: Processes up to `MAX_BATCH_SIZE` (50) policies. Each policy + /// must be active and owned by the caller. Non-matching policies are + /// silently skipped. + /// + /// # Returns + /// The number of policies successfully paid. + pub fn batch_pay_premiums(env: Env, caller: Address, policy_ids: Vec) -> u32 { + Self::require_init(&env); + Self::require_not_paused(&env, "pay"); + caller.require_auth(); + + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + + let now = env.ledger().timestamp(); + let next = now + THIRTY_DAYS; + let mut paid = 0u32; + + for pid in policy_ids.iter() { + if let Some(mut policy) = policies.get(pid) { + if policy.active && policy.owner == caller { + policy.last_payment_at = now; + policy.next_payment_due = next; + policy.next_payment_date = next; + policies.set(pid, policy); + paid += 1; + } + } + } + + env.storage().instance().set(&DataKey::Policies, &policies); + Self::bump_ttl(&env); + paid + } + + // ----------------------------------------------------------------------- + // Premium schedules + // ----------------------------------------------------------------------- + + /// Create a new premium schedule for a policy. + /// + /// NatSpec: Schedules allow automated recurring payments. The `next_due` + /// timestamp is set by the caller. If `interval` is 0, the schedule fires + /// once and then deactivates. + /// + /// # Returns + /// The new schedule's `u32` ID. + pub fn create_premium_schedule( + env: Env, + caller: Address, + policy_id: u32, + next_due: u64, + interval: u64, + ) -> u32 { + Self::require_init(&env); + Self::require_not_paused(&env, "schedule"); + caller.require_auth(); + + let mut scount: u32 = env + .storage() + .instance() + .get(&DataKey::ScheduleCount) + .unwrap(); + scount = scount.checked_add(1).expect("schedule ID overflow"); + + let schedule = PremiumSchedule { + id: scount, + policy_id, + owner: caller, + next_due, + interval, + active: true, + missed_count: 0, + }; + + let mut schedules: Map = env + .storage() + .instance() + .get(&DataKey::Schedules) + .unwrap(); + schedules.set(scount, schedule); + env.storage().instance().set(&DataKey::Schedules, &schedules); + env.storage().instance().set(&DataKey::ScheduleCount, &scount); + + Self::bump_ttl(&env); + scount + } + + /// Modify an existing premium schedule's `next_due` and `interval`. + /// + /// NatSpec: Only the schedule owner may modify. Panics if the schedule does + /// not exist. + pub fn modify_premium_schedule( + env: Env, + caller: Address, + schedule_id: u32, + next_due: u64, + interval: u64, + ) { + Self::require_init(&env); + Self::require_not_paused(&env, "schedule"); + caller.require_auth(); + + let mut schedules: Map = env + .storage() + .instance() + .get(&DataKey::Schedules) + .unwrap(); + let mut schedule = schedules.get(schedule_id).expect("schedule not found"); + + if schedule.owner != caller { + panic!("unauthorized"); + } + + schedule.next_due = next_due; + schedule.interval = interval; + schedules.set(schedule_id, schedule); + env.storage().instance().set(&DataKey::Schedules, &schedules); + + Self::bump_ttl(&env); + } + + /// Cancel a premium schedule. + /// + /// NatSpec: Marks the schedule as inactive. Only the owner may cancel. + pub fn cancel_premium_schedule(env: Env, caller: Address, schedule_id: u32) { + Self::require_init(&env); + Self::require_not_paused(&env, "schedule"); + caller.require_auth(); + + let mut schedules: Map = env + .storage() + .instance() + .get(&DataKey::Schedules) + .unwrap(); + let mut schedule = schedules.get(schedule_id).expect("schedule not found"); + + if schedule.owner != caller { + panic!("unauthorized"); + } + + schedule.active = false; + schedules.set(schedule_id, schedule); + env.storage().instance().set(&DataKey::Schedules, &schedules); + + Self::bump_ttl(&env); + } + + /// Execute all premium schedules that are currently due. + /// + /// NatSpec: Iterates through all active schedules, executing those whose + /// `next_due` ≤ current timestamp. For recurring schedules (`interval > 0`), + /// the schedule advances. For one-shot schedules (`interval == 0`), the + /// schedule deactivates. Missed intervals are counted. + /// + /// # Returns + /// A `Vec` of schedule IDs that were executed. + pub fn execute_due_premium_schedules(env: Env) -> Vec { + Self::require_init(&env); + + let now = env.ledger().timestamp(); + let mut schedules: Map = env + .storage() + .instance() + .get(&DataKey::Schedules) + .unwrap(); + let mut policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + + let mut executed = Vec::new(&env); + let scount: u32 = env + .storage() + .instance() + .get(&DataKey::ScheduleCount) + .unwrap(); + + for sid in 1..=scount { + if let Some(mut schedule) = schedules.get(sid) { + if !schedule.active || schedule.next_due > now { + continue; + } + + // Execute the premium payment for the associated policy + if let Some(mut policy) = policies.get(schedule.policy_id) { + if policy.active { + let next = now + THIRTY_DAYS; + policy.last_payment_at = now; + policy.next_payment_due = next; + policy.next_payment_date = next; + policies.set(schedule.policy_id, policy); + } + } + + // Handle missed intervals for recurring schedules + if schedule.interval > 0 { + let mut missed: u32 = 0; + let mut due = schedule.next_due; + while due + schedule.interval <= now { + due += schedule.interval; + missed += 1; + } + schedule.missed_count = missed; + schedule.next_due = due + schedule.interval; + // Stays active — recurring + } else { + // One-shot schedule: deactivate after execution + schedule.active = false; + } + + schedules.set(sid, schedule); + executed.push_back(sid); + } + } + + env.storage().instance().set(&DataKey::Schedules, &schedules); + env.storage().instance().set(&DataKey::Policies, &policies); + + Self::bump_ttl(&env); + executed + } + + /// Query a premium schedule by ID. + pub fn get_premium_schedule(env: Env, schedule_id: u32) -> Option { + let schedules: Map = env + .storage() + .instance() + .get(&DataKey::Schedules) + .unwrap_or(Map::new(&env)); + schedules.get(schedule_id) + } + + // ----------------------------------------------------------------------- + // Queries + // ----------------------------------------------------------------------- + + /// Retrieve a policy by its ID. + /// + /// NatSpec: Panics with `"policy not found"` if the requested ID does not + /// exist. Does not require authorization (read-only). + pub fn get_policy(env: Env, policy_id: u32) -> InsurancePolicy { + Self::require_init(&env); + let policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + match policies.get(policy_id) { + Some(p) => p, + None => panic!("policy not found"), + } + } + + /// Return the list of all active policy IDs (unpaginated). + /// + /// NatSpec: This is the simple version; for paginated access use + /// `get_active_policies` with cursor and limit. + pub fn get_active_policies_list(env: Env) -> Vec { + Self::require_init(&env); + env.storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap() + } + + /// Return active policies for a specific owner with cursor-based + /// pagination. + /// + /// NatSpec: Returns a `PolicyPage` containing up to `limit` policies + /// belonging to `owner` whose IDs are greater than `cursor`. Use + /// `next_cursor` from the returned page to request subsequent pages. + pub fn get_active_policies( + env: Env, + owner: Address, + cursor: u32, + limit: u32, + ) -> PolicyPage { + Self::require_init(&env); + + let effective_limit = if limit == 0 { + DEFAULT_PAGE_LIMIT + } else if limit > MAX_PAGE_LIMIT { + MAX_PAGE_LIMIT + } else { + limit + }; + + let policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap(); + + let mut items = Vec::new(&env); + let mut count = 0u32; + let mut last_id = 0u32; + + for aid in active_ids.iter() { + if aid <= cursor { + continue; + } + if let Some(policy) = policies.get(aid) { + if policy.owner == owner && policy.active { + items.push_back(policy); + count += 1; + last_id = aid; + if count >= effective_limit { + break; + } + } + } + } + + let next_cursor = if count >= effective_limit { last_id } else { 0 }; + + PolicyPage { + items, + count, + next_cursor, + } + } + + /// Return ALL policies for a specific owner (active and inactive) with + /// pagination. + pub fn get_all_policies_for_owner( + env: Env, + owner: Address, + cursor: u32, + limit: u32, + ) -> PolicyPage { + Self::require_init(&env); + + let effective_limit = if limit == 0 { + DEFAULT_PAGE_LIMIT + } else if limit > MAX_PAGE_LIMIT { + MAX_PAGE_LIMIT + } else { + limit + }; + + let policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap(); + let total_count: u32 = env + .storage() + .instance() + .get(&DataKey::PolicyCount) + .unwrap(); + + let mut items = Vec::new(&env); + let mut count = 0u32; + let mut last_id = 0u32; + + for pid in (cursor + 1)..=(total_count) { + if let Some(policy) = policies.get(pid) { + if policy.owner == owner { + items.push_back(policy); + count += 1; + last_id = pid; + if count >= effective_limit { + break; + } + } + } + } + + let next_cursor = if count >= effective_limit { last_id } else { 0 }; + + PolicyPage { + items, + count, + next_cursor, + } + } + + /// Compute the sum of `monthly_premium` across all active policies for an + /// owner. + /// + /// NatSpec: Uses `saturating_add` to prevent overflow on extremely large + /// portfolios. READ-ONLY — no authorization required. + pub fn get_total_monthly_premium(env: Env, owner: Address) -> i128 { + let policies: Map = env + .storage() + .instance() + .get(&DataKey::Policies) + .unwrap_or(Map::new(&env)); + let active_ids: Vec = env + .storage() + .instance() + .get(&DataKey::ActivePolicies) + .unwrap_or(Vec::new(&env)); + + let mut total: i128 = 0; + for aid in active_ids.iter() { + if let Some(policy) = policies.get(aid) { + if policy.owner == owner { + total = total.saturating_add(policy.monthly_premium); + } + } + } + total + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + /// Ensure the contract has been initialized. + fn require_init(env: &Env) { + if !env.storage().instance().has(&DataKey::Owner) { + panic!("not initialized"); + } + } + + /// Ensure the caller is the contract owner. + fn require_owner(env: &Env, caller: &Address) { + Self::require_init(env); + caller.require_auth(); + let owner: Address = env + .storage() + .instance() + .get(&DataKey::Owner) + .unwrap(); + if *caller != owner { + panic!("unauthorized"); + } + } + + /// Check global and per-function pause controls. + /// + /// NatSpec: Panics with `"contract is paused"` if the global flag is set, + /// or `" is paused"` if the specific function pause flag is set. + fn require_not_paused(env: &Env, fn_name: &str) { + let global: bool = env + .storage() + .instance() + .get(&DataKey::PauseAll) + .unwrap_or(false); + if global { + panic!("contract is paused"); + } + let pause_map: Map = env + .storage() + .instance() + .get(&DataKey::PauseFn) + .unwrap_or(Map::new(env)); + let sym = Symbol::new(env, fn_name); + if pause_map.get(sym).unwrap_or(false) { + match fn_name { + "create" => panic!("create is paused"), + "pay" => panic!("pay is paused"), + "deactivate" => panic!("deactivate is paused"), + "set_ref" => panic!("set_ref is paused"), + "schedule" => panic!("schedule is paused"), + _ => panic!("function is paused"), + } + } + } + + /// Bump instance TTL when it is below the threshold. + fn bump_ttl(env: &Env) { + env.storage().instance().extend_ttl( + INSTANCE_LIFETIME_THRESHOLD, + INSTANCE_BUMP_AMOUNT, + ); + } } \ No newline at end of file diff --git a/insurance/src/test.rs b/insurance/src/test.rs index ec536c69..033fa6da 100644 --- a/insurance/src/test.rs +++ b/insurance/src/test.rs @@ -1676,4 +1676,601 @@ fn test_get_premium_schedules() { &None, ); } + + // ======================================================================= + // 18. Pause-control regression tests — emergency pause-all + // ======================================================================= + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_create_policy() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + // Enable global pause + client.set_pause_all(&owner, &true); + // Must panic + client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_pay_premium() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + // Must panic + client.pay_premium(&caller, &id, &5_000_000i128); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_deactivate_policy() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + // Must panic + client.deactivate_policy(&owner, &id); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_set_external_ref() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + let new_ref = String::from_str(&env, "REF-001"); + // Must panic + client.set_external_ref(&owner, &id, &Some(new_ref)); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_create_premium_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + // Must panic + client.create_premium_schedule(&caller, &id, &3000, &2592000); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_modify_premium_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + env.ledger().set_timestamp(1000u64); + let pid = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let sid = client.create_premium_schedule(&caller, &pid, &3000, &2592000); + client.set_pause_all(&owner, &true); + // Must panic + client.modify_premium_schedule(&caller, &sid, &4000, &2678400); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_cancel_premium_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + env.ledger().set_timestamp(1000u64); + let pid = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let sid = client.create_premium_schedule(&caller, &pid, &3000, &2592000); + client.set_pause_all(&owner, &true); + // Must panic + client.cancel_premium_schedule(&caller, &sid); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_pause_all_blocks_batch_pay_premiums() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let mut ids = soroban_sdk::Vec::new(&env); + ids.push_back(id); + client.set_pause_all(&owner, &true); + // Must panic + client.batch_pay_premiums(&caller, &ids); + } + + // ======================================================================= + // 19. Pause-control regression tests — granular function pauses + // ======================================================================= + + #[test] + #[should_panic(expected = "create is paused")] + fn test_fn_pause_create_blocks_only_create() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + // create_policy must panic + client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + } + + #[test] + fn test_fn_pause_create_does_not_block_pay() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + // Pause create only + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + // pay_premium must still work + let result = client.pay_premium(&caller, &id, &5_000_000i128); + assert!(result); + } + + #[test] + #[should_panic(expected = "pay is paused")] + fn test_fn_pause_pay_blocks_pay_premium() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_fn(&owner, &Symbol::new(&env, "pay"), &true); + client.pay_premium(&caller, &id, &5_000_000i128); + } + + #[test] + #[should_panic(expected = "pay is paused")] + fn test_fn_pause_pay_blocks_batch_pay() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let mut ids = soroban_sdk::Vec::new(&env); + ids.push_back(id); + client.set_pause_fn(&owner, &Symbol::new(&env, "pay"), &true); + client.batch_pay_premiums(&caller, &ids); + } + + #[test] + #[should_panic(expected = "deactivate is paused")] + fn test_fn_pause_deactivate_blocks_deactivate() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_fn(&owner, &Symbol::new(&env, "deactivate"), &true); + client.deactivate_policy(&owner, &id); + } + + #[test] + fn test_fn_pause_deactivate_does_not_block_create() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + // Pause deactivate only + client.set_pause_fn(&owner, &Symbol::new(&env, "deactivate"), &true); + // create_policy must still work + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + assert!(id > 0); + } + + #[test] + #[should_panic(expected = "set_ref is paused")] + fn test_fn_pause_set_ref_blocks_set_external_ref() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_fn(&owner, &Symbol::new(&env, "set_ref"), &true); + let new_ref = String::from_str(&env, "NEW-REF"); + client.set_external_ref(&owner, &id, &Some(new_ref)); + } + + #[test] + #[should_panic(expected = "schedule is paused")] + fn test_fn_pause_schedule_blocks_create_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + env.ledger().set_timestamp(1000u64); + let pid = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_fn(&owner, &Symbol::new(&env, "schedule"), &true); + client.create_premium_schedule(&caller, &pid, &3000, &2592000); + } + + #[test] + #[should_panic(expected = "schedule is paused")] + fn test_fn_pause_schedule_blocks_modify_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + env.ledger().set_timestamp(1000u64); + let pid = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let sid = client.create_premium_schedule(&caller, &pid, &3000, &2592000); + client.set_pause_fn(&owner, &Symbol::new(&env, "schedule"), &true); + client.modify_premium_schedule(&caller, &sid, &4000, &2678400); + } + + #[test] + #[should_panic(expected = "schedule is paused")] + fn test_fn_pause_schedule_blocks_cancel_schedule() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + env.ledger().set_timestamp(1000u64); + let pid = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + let sid = client.create_premium_schedule(&caller, &pid, &3000, &2592000); + client.set_pause_fn(&owner, &Symbol::new(&env, "schedule"), &true); + client.cancel_premium_schedule(&caller, &sid); + } + + // ======================================================================= + // 20. Pause-control — authorization and security + // ======================================================================= + + #[test] + #[should_panic(expected = "unauthorized")] + fn test_set_pause_all_non_owner_panics() { + let (env, client, _owner) = setup(); + let non_owner = Address::generate(&env); + client.set_pause_all(&non_owner, &true); + } + + #[test] + #[should_panic(expected = "unauthorized")] + fn test_set_pause_fn_non_owner_panics() { + let (env, client, _owner) = setup(); + let non_owner = Address::generate(&env); + client.set_pause_fn(&non_owner, &Symbol::new(&env, "create"), &true); + } + + // ======================================================================= + // 21. Pause-control — unpause restores normal operation + // ======================================================================= + + #[test] + fn test_unpause_all_restores_create_policy() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + // Pause, then unpause + client.set_pause_all(&owner, &true); + client.set_pause_all(&owner, &false); + // create_policy must succeed + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + assert!(id > 0); + } + + #[test] + fn test_unpause_all_restores_pay_premium() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + client.set_pause_all(&owner, &false); + let result = client.pay_premium(&caller, &id, &5_000_000i128); + assert!(result); + } + + #[test] + fn test_unpause_fn_restores_specific_function() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + // Pause create, then unpause + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &false); + // create_policy must succeed + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + assert!(id > 0); + } + + // ======================================================================= + // 22. Pause-control — query functions (is_paused, is_fn_paused) + // ======================================================================= + + #[test] + fn test_is_paused_default_false() { + let (_env, client, _owner) = setup(); + assert!(!client.is_paused()); + } + + #[test] + fn test_is_paused_reflects_set_pause_all() { + let (_env, client, owner) = setup(); + client.set_pause_all(&owner, &true); + assert!(client.is_paused()); + client.set_pause_all(&owner, &false); + assert!(!client.is_paused()); + } + + #[test] + fn test_is_fn_paused_default_false() { + let (env, client, _owner) = setup(); + assert!(!client.is_fn_paused(&Symbol::new(&env, "create"))); + assert!(!client.is_fn_paused(&Symbol::new(&env, "pay"))); + } + + #[test] + fn test_is_fn_paused_reflects_set_pause_fn() { + let (env, client, owner) = setup(); + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + assert!(client.is_fn_paused(&Symbol::new(&env, "create"))); + assert!(!client.is_fn_paused(&Symbol::new(&env, "pay"))); + } + + #[test] + fn test_is_fn_paused_returns_true_when_global_paused() { + let (env, client, owner) = setup(); + client.set_pause_all(&owner, &true); + // Even without per-function flag, is_fn_paused returns true under global pause + assert!(client.is_fn_paused(&Symbol::new(&env, "create"))); + assert!(client.is_fn_paused(&Symbol::new(&env, "pay"))); + } + + // ======================================================================= + // 23. Pause-control — multiple functions paused simultaneously + // ======================================================================= + + #[test] + fn test_multiple_fn_pauses_are_independent() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + + // Pause create and pay, but NOT deactivate + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + client.set_pause_fn(&owner, &Symbol::new(&env, "pay"), &true); + + // Verify deactivate still works — first we need a policy + // Unpause create temporarily to make one + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &false); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + + // Deactivate must still work + let result = client.deactivate_policy(&owner, &id); + assert!(result); + } + + // ======================================================================= + // 24. Pause-control — global pause overrides per-function unpause + // ======================================================================= + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_global_pause_overrides_fn_unpause() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + // Per-function "create" is explicitly NOT paused + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &false); + // But global pause IS set + client.set_pause_all(&owner, &true); + // Must still panic because global trumps per-function + client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + } + + // ======================================================================= + // 25. Pause-control — read-only queries work while paused + // ======================================================================= + + #[test] + fn test_queries_work_while_globally_paused() { + let (env, client, owner) = setup(); + let caller = Address::generate(&env); + let id = client.create_policy( + &caller, + &short_name(&env), + &CoverageType::Health, + &5_000_000i128, + &50_000_000i128, + &None, + ); + client.set_pause_all(&owner, &true); + + // Read-only queries must NOT be blocked + let policy = client.get_policy(&id); + assert_eq!(policy.id, id); + + let active = client.get_active_policies(); + assert_eq!(active.len(), 1); + + let total = client.get_total_monthly_premium(); + assert_eq!(total, 5_000_000i128); + } + + // ======================================================================= + // 26. Pause-control — owner can toggle pause while already paused + // ======================================================================= + + #[test] + fn test_owner_can_unpause_while_globally_paused() { + let (_env, client, owner) = setup(); + client.set_pause_all(&owner, &true); + assert!(client.is_paused()); + // Owner must be able to unpause even when globally paused + client.set_pause_all(&owner, &false); + assert!(!client.is_paused()); + } + + #[test] + fn test_owner_can_toggle_fn_pause_while_globally_paused() { + let (env, client, owner) = setup(); + client.set_pause_all(&owner, &true); + // Owner can still manage per-function flags + client.set_pause_fn(&owner, &Symbol::new(&env, "create"), &true); + assert!(client.is_fn_paused(&Symbol::new(&env, "create"))); + } + + // ======================================================================= + // 27. Pause-control — idempotent pause toggles + // ======================================================================= + + #[test] + fn test_set_pause_all_idempotent() { + let (_env, client, owner) = setup(); + client.set_pause_all(&owner, &true); + client.set_pause_all(&owner, &true); // double-set + assert!(client.is_paused()); + client.set_pause_all(&owner, &false); + client.set_pause_all(&owner, &false); // double-clear + assert!(!client.is_paused()); + } + + #[test] + fn test_set_pause_fn_idempotent() { + let (env, client, owner) = setup(); + let sym = Symbol::new(&env, "pay"); + client.set_pause_fn(&owner, &sym, &true); + client.set_pause_fn(&owner, &sym, &true); // double-set + assert!(client.is_fn_paused(&sym)); + client.set_pause_fn(&owner, &sym, &false); + client.set_pause_fn(&owner, &sym, &false); // double-clear + assert!(!client.is_fn_paused(&sym)); + } } \ No newline at end of file