Skip to content

test: add compliant minting invariant tests - #151

Merged
El-swaggerito merged 3 commits into
Raegis-RWA:mainfrom
jd5073356-max:feat/8-minting-invariant-tests
Jul 29, 2026
Merged

test: add compliant minting invariant tests#151
El-swaggerito merged 3 commits into
Raegis-RWA:mainfrom
jd5073356-max:feat/8-minting-invariant-tests

Conversation

@jd5073356-max

Copy link
Copy Markdown
Contributor

Description

Adds a deterministic scenario matrix (src/test.rs) covering mint_asset's compliance and authorization invariants: compliant success, non-whitelisted recipient, unauthorised issuer, invalid (non-Active) asset state, revoked compliance, and repeated rejected attempts — each asserting both the specific error variant and that balance/total supply are left byte-for-byte unchanged.

main did not compile and, once fixed, had 16 failing pre-existing tests plus a real production bug directly relevant to this issue's "invalid asset minting is rejected" criterion — see below. Each is its own commit, ahead of the actual test-suite addition.

Related Issues

Closes #8

Completion Table

Acceptance Criterion Status Implementation Evidence Test Evidence
AC 1: Compliant minting success path is tested Complete mint_asset (src/asset.rs), unchanged test_compliant_mint_succeeds_and_updates_balance_and_supply
AC 2: Non-whitelisted recipient minting is rejected Complete compliance::require_can_receive (src/asset.rs) test_mint_rejects_non_whitelisted_recipient_and_leaves_state_unchanged
AC 3: Unauthorised issuer minting is rejected Complete require_role(&admin, Role::AssetManager) (src/asset.rs) test_mint_rejects_unauthorised_issuer_and_leaves_state_unchanged
AC 4: Invalid asset minting is rejected Complete require_asset_movable (src/asset.rs) — plus a real bug fix, see below test_mint_rejects_invalid_asset_state_and_leaves_state_unchanged (Draft and Retired cases)
AC 5: Revoked compliance state is tested Complete compliance::require_can_receive (src/asset.rs) test_mint_rejects_revoked_compliance_and_leaves_state_unchanged
AC 6: State remains consistent after failed minting Complete No mutation occurs before all mint_asset checks pass (src/asset.rs) Every rejection test above asserts get_balance_of/get_total_supply unchanged; test_repeated_rejected_mint_attempts_never_mutate_state additionally covers 5 repeated failures in a row

Detailed Traceability Mapping

Acceptance Criteria Implementation Storage & State Changes Events Emitted Test Coverage Security/Safety Controls
AC 4 (invalid asset) Fixed asset::get_asset_status_internal default from Active to Draft (src/asset.rs) No new storage keys; corrects the default read of the existing DataKey::AssetStatus key None added test_mint_rejects_invalid_asset_state_and_leaves_state_unchanged Closes a real gap: before this fix, a freshly-initialized contract could be minted against immediately, without the admin ever calling set_asset_status(Active) — see "Pre-existing issues" below
All ACs 6 new #[test] functions in src/test.rs, no production logic changed beyond the AC 4 fix N/A (test-only, except the one-line default fix) N/A See Completion Table N/A

Type of Change

  • Bug fix (non-breaking change which fixes an issue) — see "Pre-existing issues" below
  • New feature (non-breaking change which adds functionality) — the invariant tests requested by this issue
  • Breaking change
  • Documentation update
  • Chore

Pre-existing issues found and fixed (blocking, discovered while establishing a clean baseline)

1. main did not compile. Two orphaned closing braces in src/test.rs (cargo check --workspace --tests failed outright) — one right after a section-header comment with no matching opener, one duplicated at the end of the file. Fixed in an isolated commit.

2. A real production bug, directly relevant to this issue's AC 4. asset::get_asset_status_internal (what require_asset_movable — the check mint_asset/transfer actually call — reads) defaulted an unset asset status to Active. lifecycle::get_asset_status (what set_asset_status itself reads) defaults the same storage key to Draft. Both the AssetStatus enum docs and lifecycle::get_asset_status's own doc comment are explicit that Draft is the intended initial state requiring an explicit activation step — but the mismatched default meant mint_asset silently treated an un-activated contract as Active. Verified with a throwaway probe before fixing: minting succeeded on a freshly-initialized contract that had never called set_asset_status. Fixed the default to Draft, matching the documented design and the already-correct writer-side check.

3. 16 pre-existing failing tests in src/test.rs, surfaced once (1) and (2) above were fixed — all either depended on the buggy Active default (a shared test helper never explicitly activated the asset, only "working" because of the bug) or asserted a stale error taxonomy / hardcoded count that had drifted from the current code:

  • setup_transferable() helper never called set_asset_status(Active), unlike the sibling setup_active() helper — added the missing call.
  • One test's now-redundant re-activation call (hit the "no-op transition rejected" path once the helper above was fixed) — removed.
  • test_restriction_checks_never_panic_on_uninitialized_contract expected the wrong first-blocking-reason on an uninitialized contract, contradicting evaluate_transfer's own documented check order — corrected.
  • 6 assertions expected the older AssetLifecyclePaused/AssetRetired/AssetBlocked errors (6001-6003) instead of the current *Restriction variants (7000-7002) that require_asset_movable actually returns, per its own doc comment.
  • 1 assertion expected InvalidAssetStatusTransition (6005, defined but never actually raised anywhere) instead of InvalidLifecycleTransition (6004, what set_asset_status actually returns).
  • 1 test hardcoded HoldingCapExceeded/SupplyCapExceeded at 7003/7004 instead of their real, stable (append-only per errors.rs) codes 5003/5002.
  • 4 assertions across 2 tests hardcoded get_capability_keys().len() at 28 or 31 instead of the current, correct count of 33.

Each of the three items above is a separate, minimal commit ahead of the actual test-suite addition, in dependency order.

Not fixed (out of scope, unrelated, flagging for visibility): cargo test --workspace still has 2 pre-existing failures in the separate tests/sdk_fixtures.rs integration target — fixture drift against the committed JSON snapshots in fixtures/sdk/. Confirmed independent of everything in this PR (that target doesn't depend on src/test.rs, and the library itself always compiled even before the brace fix). The repo documents a UPDATE_FIXTURES=1 cargo test --test sdk_fixtures regeneration flow for intentional changes, but understanding why it drifted needs its own investigation rather than a blind regenerate.

PR Evidence Checklist

1. Issue Reference

2. Implementation Summary

  • See Description and Detailed Traceability Mapping above. Files touched: src/test.rs (brace fixes, 8 corrected assertions, 6 new tests), src/asset.rs (1-line default-value fix).

3. Tests Added or Justification

  • 6 new tests, listed in the Completion Table, all in src/test.rs. Every rejection case covers both the failure path and post-failure state consistency.

4. Commands Run

  • cargo test --lib: 180 passed; 0 failed (was uncompilable, then 16 failing, before the two prerequisite fix commits).
  • cargo fmt --all -- --check: clean on every file this PR touches (src/asset.rs, src/test.rs) — pre-existing formatting drift in config.rs/config_test.rs/lib.rs and two untouched lines of test.rs is unrelated and left alone.
  • cargo clippy --all-targets: no new warnings introduced (verified by diffing warning output before/after); all remaining warnings are pre-existing and in code this PR doesn't touch.
  • cargo build --release --target wasm32v1-none: succeeds.

5. CI Status

  • This repo's only GitHub Actions workflows are auto-merge/auto-assign triggers — there is no build/test/lint CI gate to report green. make verify (documented as "the recommended pre-push check") is the local equivalent, covered above.

6. Acceptance Criteria Coverage

  • All 6 acceptance criteria addressed — see Completion Table.

Policy & Standards

  • Read the Contributor Evaluation Policy and Payment-Period Conduct guidance; understand merge does not guarantee payment.
  • Read CONTRIBUTING.md.
  • cargo fmt clean on touched files.
  • No new cargo clippy warnings.
  • Tests added/updated, all pass (cargo test --lib: 180/180).
  • Detailed traceability mapping filled out above (storage default fix + tests).
  • Reviewed changes against the Reviewer Checklist.
  • No new documentation or features imply regulatory completeness beyond smart contract enforcement.

Additional Context

$ cargo test --lib
test result: ok. 180 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

$ cargo build --release --target wasm32v1-none
Finished `release` profile [optimized] target(s)

`main` did not compile `cargo check --workspace --tests`: two stray `}`
in src/test.rs with no matching opener — one right after the
"Compliance lifecycle state machine" section-header comment (leftover
from what was likely a removed `mod { ... }` wrapper, since every test
in the file is otherwise flat/top-level), and one duplicated at the
very end of the file. Removed both; re-indented the section-header
comment to match the file's flat structure now that it's not inside
a (never-actually-open) block.

This is a separate, minimal commit from the actual issue #8 work
(compliant minting invariant tests) since nothing could be compiled or
tested until this was fixed.
`asset::get_asset_status_internal` (used by `require_asset_movable` —
the check `mint_asset`/`transfer` actually call) defaulted an unset
asset status to `Active`, while `lifecycle::get_asset_status` (used by
`set_asset_status` itself) defaulted the same, single storage key
(`DataKey::AssetStatus`) to `Draft`. Both the `AssetStatus` enum docs
and `lifecycle::get_asset_status`'s own doc comment are explicit that
`Draft` is the intended initial state, requiring an explicit
`Active` transition before minting/transfers — but the mismatched
default silently let `mint_asset`/`transfer` succeed against a
never-activated contract, since the read they actually gate on saw
"Active" regardless. Verified with a throwaway probe test before
fixing: minting succeeded on a freshly-initialized contract with
`set_asset_status` never called. Fixed the default to `Draft`, matching
the documented design and the (already-correct) writer-side check.

This surfaced/required a chain of test fixes to get back to a clean
`cargo test --lib`, all pre-existing and unrelated to the actual issue
#8 work:

- `setup_transferable()` (a test helper used by ~15 tests) never called
  `set_asset_status(Active)` after `initialize`, unlike the sibling
  `setup_active()` helper that does — it only worked before this fix
  because the buggy Active-default silently covered for it. Added the
  missing call, matching `setup_active()`'s established pattern.
- One test that had started explicitly re-activating (redundant once
  `setup_transferable` does it) now hit the "no-op transition rejected"
  path — removed the now-redundant call.
- `test_restriction_checks_never_panic_on_uninitialized_contract`
  expected `SenderNotCompliant` as the first blocking reason on an
  uninitialized contract; `evaluate_transfer`'s own doc comment states
  the check order is pause → amount → asset lifecycle → sender
  compliance — so asset lifecycle (now correctly `Draft` → `AssetBlocked`)
  is checked, and returned, first. Updated the expectation to match the
  code's own documented (and, since this fix, actually-followed) order.
- 6 assertions in src/test.rs expected an older error taxonomy
  (`AssetLifecyclePaused`/`AssetRetired`/`AssetBlocked`, 6001-6003) that
  `require_asset_movable` no longer returns — it returns the newer,
  more specific `AssetPausedRestriction`/`AssetRetiredRestriction`/
  `AssetBlockedRestriction` (7000-7002), per its own doc comment.
  Updated the assertions to the current, documented error variants.
- `test_restriction_reason_retired_asset` expected
  `InvalidAssetStatusTransition` (6005) for a rejected `Retired -> Active`
  transition; `lifecycle::set_asset_status` only ever returns the older
  `InvalidLifecycleTransition` (6004) — 6005 is defined in errors.rs but
  never actually raised anywhere. Updated the assertion to match what
  the code actually returns.
- `test_restriction_reason_code_mapping_is_total_and_round_trips`
  hardcoded `HoldingCapExceeded`/`SupplyCapExceeded` at 7003/7004; the
  real, stable (append-only per errors.rs) codes are 5003/5002 — the
  7000s range is reserved for asset-lifecycle restrictions only.
  Corrected the expected codes.
- 4 assertions across 2 tests hardcoded `get_capability_keys().len()`
  at 28 or 31; the capability registry has grown to 33 entries since
  those were written. Updated to the current, correct count.

Verified: `cargo test --lib` now passes 174/174 (was 16 failing before
any fix in this commit, on top of the prior commit's compile fix).
`cargo test --workspace` still has 2 pre-existing failures in the
separate `tests/sdk_fixtures.rs` integration target (fixture drift
against `fixtures/sdk/*.json`, unrelated to anything touched here and
predating this branch — confirmed independent since that target
doesn't depend on src/test.rs and the library itself always compiled).
Left unfixed as out of scope; flagged in the PR description.
Adds a deterministic scenario matrix covering mint_asset's compliance
and authorization invariants, per the issue's acceptance criteria:

- test_compliant_mint_succeeds_and_updates_balance_and_supply: the
  happy path — an AssetManager minting to an Approved, whitelisted
  recipient on an Active asset succeeds and updates balance/supply.
- test_mint_rejects_non_whitelisted_recipient_and_leaves_state_unchanged:
  a recipient at the default (Unknown) compliance status is rejected.
- test_mint_rejects_unauthorised_issuer_and_leaves_state_unchanged: a
  caller with no AssetManager/Admin role is rejected.
- test_mint_rejects_invalid_asset_state_and_leaves_state_unchanged: a
  never-activated (Draft) asset and an explicitly Retired asset both
  reject minting, with their own specific restriction error.
- test_mint_rejects_revoked_compliance_and_leaves_state_unchanged: a
  recipient who was Approved and then had that revoked is rejected —
  distinct from having never been whitelisted.
- test_repeated_rejected_mint_attempts_never_mutate_state: the same
  rejected mint retried 5 times leaves balance/supply untouched every
  time, and the recipient is still mintable afterwards once actually
  made compliant (no latent state survived the repeated failures).

Every rejection case asserts both the specific error variant and that
balance/total supply are byte-for-byte unchanged, so a regression that
silently mutated state on a failed mint would be caught even if the
error code alone still looked right.

Verified each test is genuine, not just compiling: temporarily zeroed
out the asset-status-default fix from the prior commit and confirmed
test_mint_rejects_invalid_asset_state_and_leaves_state_unchanged (and
the pre-existing test_restriction_checks_never_panic_on_uninitialized_contract)
both fail without it, then restored the fix and confirmed all 180
tests pass again.
@El-swaggerito
El-swaggerito merged commit dacdc70 into Raegis-RWA:main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add compliant minting invariant tests

2 participants