test: add compliant minting invariant tests - #151
Merged
El-swaggerito merged 3 commits intoJul 29, 2026
Conversation
`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.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a deterministic scenario matrix (
src/test.rs) coveringmint_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.maindid 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
mint_asset(src/asset.rs), unchangedtest_compliant_mint_succeeds_and_updates_balance_and_supplycompliance::require_can_receive(src/asset.rs)test_mint_rejects_non_whitelisted_recipient_and_leaves_state_unchangedrequire_role(&admin, Role::AssetManager)(src/asset.rs)test_mint_rejects_unauthorised_issuer_and_leaves_state_unchangedrequire_asset_movable(src/asset.rs) — plus a real bug fix, see belowtest_mint_rejects_invalid_asset_state_and_leaves_state_unchanged(Draft and Retired cases)compliance::require_can_receive(src/asset.rs)test_mint_rejects_revoked_compliance_and_leaves_state_unchangedmint_assetchecks pass (src/asset.rs)get_balance_of/get_total_supplyunchanged;test_repeated_rejected_mint_attempts_never_mutate_stateadditionally covers 5 repeated failures in a rowDetailed Traceability Mapping
asset::get_asset_status_internaldefault fromActivetoDraft(src/asset.rs)DataKey::AssetStatuskeytest_mint_rejects_invalid_asset_state_and_leaves_state_unchangedset_asset_status(Active)— see "Pre-existing issues" below#[test]functions insrc/test.rs, no production logic changed beyond the AC 4 fixType of Change
Pre-existing issues found and fixed (blocking, discovered while establishing a clean baseline)
1.
maindid not compile. Two orphaned closing braces insrc/test.rs(cargo check --workspace --testsfailed 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(whatrequire_asset_movable— the checkmint_asset/transferactually call — reads) defaulted an unset asset status toActive.lifecycle::get_asset_status(whatset_asset_statusitself reads) defaults the same storage key toDraft. Both theAssetStatusenum docs andlifecycle::get_asset_status's own doc comment are explicit thatDraftis the intended initial state requiring an explicit activation step — but the mismatched default meantmint_assetsilently treated an un-activated contract as Active. Verified with a throwaway probe before fixing: minting succeeded on a freshly-initialized contract that had never calledset_asset_status. Fixed the default toDraft, 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 buggyActivedefault (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 calledset_asset_status(Active), unlike the siblingsetup_active()helper — added the missing call.test_restriction_checks_never_panic_on_uninitialized_contractexpected the wrong first-blocking-reason on an uninitialized contract, contradictingevaluate_transfer's own documented check order — corrected.AssetLifecyclePaused/AssetRetired/AssetBlockederrors (6001-6003) instead of the current*Restrictionvariants (7000-7002) thatrequire_asset_movableactually returns, per its own doc comment.InvalidAssetStatusTransition(6005, defined but never actually raised anywhere) instead ofInvalidLifecycleTransition(6004, whatset_asset_statusactually returns).HoldingCapExceeded/SupplyCapExceededat 7003/7004 instead of their real, stable (append-only pererrors.rs) codes 5003/5002.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 --workspacestill has 2 pre-existing failures in the separatetests/sdk_fixtures.rsintegration target — fixture drift against the committed JSON snapshots infixtures/sdk/. Confirmed independent of everything in this PR (that target doesn't depend onsrc/test.rs, and the library itself always compiled even before the brace fix). The repo documents aUPDATE_FIXTURES=1 cargo test --test sdk_fixturesregeneration 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
src/test.rs(brace fixes, 8 corrected assertions, 6 new tests),src/asset.rs(1-line default-value fix).3. Tests Added or Justification
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 inconfig.rs/config_test.rs/lib.rsand two untouched lines oftest.rsis 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
make verify(documented as "the recommended pre-push check") is the local equivalent, covered above.6. Acceptance Criteria Coverage
Policy & Standards
cargo fmtclean on touched files.cargo clippywarnings.cargo test --lib: 180/180).Additional Context