Skip to content

test: add deterministic failure-boundary coverage for completes in ./contracts/admin/src/lib.rs (#1423) - #1563

Open
Omolola-art wants to merge 7 commits into
CredenceOrg:mainfrom
Omolola-art:test/issue-1423-admin-completes-coverage
Open

Omolola-art wants to merge 7 commits into
CredenceOrg:mainfrom
Omolola-art:test/issue-1423-admin-completes-coverage

Conversation

@Omolola-art

Copy link
Copy Markdown

Summary

Adds deterministic failure-boundary coverage for the completes entry point in
./contracts/admin/src/lib.rs — i.e. accept_ownership, the function that
completes the two-step ownership transfer. It is the only path by which durable
control of the contract changes hands.

The authorization and state-validation logic already in accept_ownership was
reviewed and found correct; it is unchanged. This PR adds tests, and repairs
the pre-existing breakage that made the admin suite unrunnable.

Closes #1423


Important: main did not build or test

The admin suite could not be executed at all before this PR. Three independent
breakages, all predating this work:

  1. contracts/credence_errors/src/lib.rs did not compile. A botched merge
    (c2d6b98b) reduced the soroban_sdk import list to just contracterror
    (leaving contracttype, panic_with_error, Address, Env unresolved),
    deleted three enum variants whose uses remained, renamed two variants onto
    already-used codes, and duplicated two discriminants — 40 errors. Because
    every contract depends on this crate, no contract test could run.
  2. Cargo.lock resolved an incompatible ed25519-dalek 3.0.0.
    soroban-env-host 22.1.3 declares ed25519-dalek >= 2.0.0 but only works
    with the 2.x rand_core; the 3.0.0 tree's rand_core 0.10 traits are
    unsatisfied, failing inside a third-party crate.
  3. contracts/admin did not compile — a duplicate is_admin -> bool (test: add deterministic failure-boundary coverage for is_admin #1496)
    collided with the pre-existing is_admin -> Role.

Once made to compile, 30 further tests failed, all from the same root cause
(detailed below). crates/interfaces is also corrupt on main (base64 blob in
consts.rs, stray #[config(test)]] in governable.rs, from #1480) and still
does not build. That is untouched here and out of scope for this issue, but it
does mean cargo build --workspace remains red for reasons unrelated to admin.


Acceptance criteria → code and tests

1. Deterministic behavior for valid, invalid, duplicate, and boundary inputs

New module: contracts/admin/src/test_completes_failure_boundaries.rs (15 tests).

Class Test Asserts
valid eligible_candidate_completes_transfer_exactly_once owner moves, proposal consumed, epoch +1, exactly admin_rotated + ownership_transfer_accepted
invalid completion_requires_the_pending_owner a different SuperAdmin and a stranger both → NotAdmin; proposal survives
invalid completion_without_a_proposal_is_rejected → NoPendingAdmin
invalid paused_contract_blocks_completion_and_recovers_on_unpause → ContractPaused; proposal preserved; succeeds after unpause
duplicate replay_after_completion_is_rejected_and_emits_nothing 3 replays → NoPendingAdmin, no events, epoch frozen
boundary timelock_boundary_is_inclusive_and_rejection_is_recoverable rejected at eligible_at - 1, accepted at eligible_at
boundary timelock_overflow_is_rejected u64::MAX proposal stamp → Overflow, not a wrapped, immediately-satisfiable timelock
boundary candidate_demoted_during_timelock_cannot_complete → NotAdmin
boundary candidate_suspended_during_timelock_cannot_complete → AdminSuspended
boundary candidate_removed_during_timelock_cannot_complete → NotAdmin
boundary candidate_deactivated_during_timelock_cannot_complete → AlreadyDeactivated
recovery owner_recovers_by_replacing_an_ineligible_candidate owner keeps control, can re-propose
recovery candidate_whose_suspension_expired_during_timelock_can_complete self-expiring clock, not a lockout
recovery rejected_attempt_is_retryable_without_reproposing same proposal works once eligibility is restored
— event_helper_sees_contract_events_and_ignores_diagnostics guards against vacuous assertions

2. Authorization, validation, and state-transition invariants remain enforced

Every negative test asserts the full no-op invariant from the retry contract
documented at the top of lib.rs, via the assert_no_op helper:

  • owner unchanged,
  • pending proposal unchanged,
  • get_config_epoch() unchanged,
  • no contract event published.

3. Retries, partial failure, and concurrency cannot produce an unsafe result

  • Replay of a consumed proposal is rejected and republishes nothing
    (replay_after_completion_is_rejected_and_emits_nothing).
  • A rejected attempt is retryable against the same proposal once the blocking
    condition clears, which is the retry contract's core promise.
  • The four eligibility-change-during-timelock tests cover the concurrency hazard:
    a proposal is only an intent, so a candidate who is demoted, deactivated,
    suspended, or removed while the clock runs cannot take ownership. This is the
    anti-stale-proposal guarantee from df30d820.

4. Focused tests cover success, rejection, boundary, and regression scenarios

See the table above, plus the 30 pre-existing tests repaired below.

5. Existing callers remain compatible

No public interface changed. The only signature-affecting edit is the removal of
the duplicate is_admin -> bool (which made the crate uncompilable); the
surviving is_admin -> Role is the one every existing caller and test uses.


Test execution evidence

Toolchain: rustc 1.89.0 (per rust-toolchain.toml), installed for this run —
the container had no Rust toolchain.

$ cargo test -p admin
running 236 tests
test result: ok. 236 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 3 tests   (tests/datakey_fingerprint.rs)
test result: ok. 3 passed; 0 failed

The new module in isolation:

$ cargo test -p admin test_completes
test test_completes_failure_boundaries::candidate_deactivated_during_timelock_cannot_complete ... ok
test test_completes_failure_boundaries::candidate_demoted_during_timelock_cannot_complete ... ok
test test_completes_failure_boundaries::candidate_removed_during_timelock_cannot_complete ... ok
test test_completes_failure_boundaries::candidate_suspended_during_timelock_cannot_complete ... ok
test test_completes_failure_boundaries::candidate_whose_suspension_expired_during_timelock_can_complete ... ok
test test_completes_failure_boundaries::completion_requires_the_pending_owner ... ok
test test_completes_failure_boundaries::completion_without_a_proposal_is_rejected ... ok
test test_completes_failure_boundaries::eligible_candidate_completes_transfer_exactly_once ... ok
test test_completes_failure_boundaries::event_helper_sees_contract_events_and_ignores_diagnostics ... ok
test test_completes_failure_boundaries::owner_recovers_by_replacing_an_ineligible_candidate ... ok
test test_completes_failure_boundaries::paused_contract_blocks_completion_and_recovers_on_unpause ... ok
test test_completes_failure_boundaries::rejected_attempt_is_retryable_without_reproposing ... ok
test test_completes_failure_boundaries::replay_after_completion_is_rejected_and_emits_nothing ... ok
test test_completes_failure_boundaries::timelock_boundary_is_inclusive_and_rejection_is_recoverable ... ok
test test_completes_failure_boundaries::timelock_overflow_is_rejected ... ok
test result: ok. 15 passed; 0 failed

cargo fmt -p admin -- --check clean; cargo clippy -p admin --all-targets
reports 0 errors.

Mutation testing — the tests are not vacuous

Each mutation below was applied to lib.rs temporarily, then reverted. Every one
is caught:

Mutation to lib.rs Result
timelock off-by-one (now < eligible_at → eligible_at - 1) 1 test fails
remove require_effective_super_admin at acceptance 7 tests fail
remove the pending-owner authorization check 1 test fails
don't consume the proposal on success 5 tests fail
remove the ROLE_REVOKED event from deactivate_admin 3 tests fail
remove the MinAdmins suspension guard 1 test fails

The last two also confirm the repaired pre-existing tests still detect
regressions rather than passing by default.


Repairing the 30 pre-existing failures

The dominant cause was a single misunderstanding: in soroban-sdk 22,
env.events().all() returns the events of the most recent top-level
invocation
, not a cumulative log. Every "publishes no event" assertion
therefore compared two unrelated snapshots and could never hold. Assertions now
read the log immediately after the call under test, and multi-call tests
accumulate per invocation via a new role_event_tags helper. (Reading the count
after an intervening client call also resets the frame, so the reads are
ordered before the state/epoch assertions.)

Each remaining defect was fixed at the cause, not by loosening an assertion:

  • test_ownership_transfer expected errors #109 / #107 for cases that
    now correctly raise NoPendingAdmin (115) / AdminUnchanged (111) — stale
    against the wire-stable error table.
  • test_suspension::test_suspend_below_min_admins_rejected suspended the
    caller itself, so it tripped the earlier self-suspension guard (111) and
    never reached the min-admins guard. That guard is only reachable with
    min_admins >= 2; the test now sets up two admins and suspends one.
  • setup_three_super_admins called mock_all_auths then re-entered
    as_contract, failing with frame is already authorized; the calls are now
    split into separate frames.
  • test_atomic_rollback / test_emergency needed the Events trait import
    and a corrected set_pause_signer arity.
  • test_basic.rs was overwritten by test: extend adversarial regression cases for admin test_basic #1478 with 875 lines that never compiled
    (44 × assert_eq( instead of assert_eq!(, plus calls to assign_role,
    reinstate_admin, suspend_admin_with_reason, env.register, and
    mock_all_authentications — none of which exist). Restored to its last-good
    revision. This is why the suite shows 236 rather than a higher number: that
    code could not build, so it was never running coverage. This is the one
    change most worth a reviewer's attention
    , since it is a large deletion of
    another contributor's work.

Security and failure-mode notes

  • No safeguard was weakened. Every fix moves an assertion closer to the
    contract's actual behavior.
  • credence_errors restores wire-stable codes; no renumbering. The restored
    definitions match what surviving call sites and test_errors.rs already
    assert (e.g. InvalidCurrency == 234).
  • Rejections remain diagnosable: each surfaces a distinct stable error code, and
    the assert_no_op helper proves a rejected call leaks no partial state and no
    event.

Out of scope

  • crates/interfaces is still corrupt on main (from test: add boundary and recovery coverage for governable interface #1480) and still does not
    build; cargo build --workspace remains red for that reason. Not touched.
  • CI is stubbed on this repo (contracts-tests.yml is a no-op "while main-branch
    CI is being stabilized"), so the commands above were run locally.

The workspace did not compile on main, which blocked every contract test
(including the admin suite targeted by this issue). Two independent
breakages:

1. contracts/credence_errors/src/lib.rs — a botched merge (c2d6b98) left
   the file in a non-compiling state: the soroban_sdk import list had been
   reduced to just `contracterror` (so `contracttype`, `panic_with_error`,
   `Address` and `Env` were all unresolved), three enum variants were
   deleted while their uses remained elsewhere in the file, two variants
   were renamed onto codes that already existed, and two discriminants
   were duplicated. This restores the pre-merge-damage revision, which
   compiles and whose own test suite (test_errors.rs) already asserts the
   restored codes (e.g. InvalidCurrency == 234).

2. Cargo.lock — soroban-env-host 22.1.3 accepts `ed25519-dalek >= 2.0.0` but
   is only compatible with the 2.x rand_core. Resolution had picked 3.0.0,
   whose rand_core 0.10 traits are unsatisfied, so the build failed inside
   a third-party crate. Pinning to 2.2.0 removes the conflicting 3.0.0
   dependency tree and nothing else.

No error codes are renumbered by this change: the restored definitions
match the wire-stable table that the surviving call sites and tests use.

Refs CredenceOrg#1423
Adds test_completes_failure_boundaries.rs covering accept_ownership, the
entry point that completes the two-step ownership transfer in lib.rs. It
is the only path by which durable control of the contract changes hands, so
it is asserted across every input class: valid, invalid, duplicate, and
boundary.

Coverage (15 tests):

* valid — an eligible pending SuperAdmin accepts after the timelock,
  becomes owner, consumes the proposal, advances the epoch exactly once,
  and publishes exactly admin_rotated + ownership_transfer_accepted.
* invalid — completion requires the pending owner (a different SuperAdmin
  and a stranger are both rejected with NotAdmin); no proposal is
  NoPendingAdmin; a paused contract is ContractPaused and recovers on
  unpause.
* duplicate — replaying a consumed proposal is rejected three times over
  and republishes nothing, so a retried transaction cannot re-run the
  rotation.
* boundary — the timelock is an inclusive lower bound (rejected at
  eligible_at - 1, accepted at eligible_at, and the rejection is a no-op so
  the same proposal succeeds one second later); a proposal stamped near
  u64::MAX is rejected with Overflow rather than wrapping into an
  immediately-satisfiable timelock; a candidate demoted, deactivated,
  suspended, or removed during the timelock cannot complete.
* recovery — an ineligible candidate is not a dead end: the owner keeps
  control and can replace the proposal; a suspension that merely expires
  does not block completion; a rejected attempt is retryable without
  re-proposing once the blocking condition is cleared.

Every negative test asserts the full no-op invariant from the retry
contract documented at the top of lib.rs: the owner is unchanged, the
pending proposal is unchanged, the config epoch does not advance, and no
contract event is published.

The authorization and state-validation logic already in accept_ownership
was found correct and is unchanged. This commit adds tests only.

Verified by mutation testing — each of these mutations to lib.rs is caught:
  * timelock off-by-one (`now < eligible_at` -> `eligible_at - 1`)
  * removal of require_effective_super_admin at acceptance (7 tests fail)
  * removal of the pending-owner authorization check
  * not consuming the proposal on success (5 tests fail)

Refs CredenceOrg#1423
The admin test suite could not run, and once made to run, 30 tests failed
for reasons unrelated to their subject matter. All of it stems from one
misunderstanding plus three smaller defects.

The misunderstanding: in soroban-sdk 22 `env.events().all()` returns the
events of the most recent top-level invocation, not a cumulative log.
Every "publishes no event" assertion therefore compared two unrelated
snapshots and could never hold. Assertions now read the log immediately
after the call under test, and multi-call tests accumulate per invocation
via a new `role_event_tags` helper. Note that reading the count *after* an
intervening client call also resets the frame, so the reads are ordered
before the state/epoch assertions.

Smaller defects, each fixed at the cause rather than by loosening the
assertion:

* test_ownership_transfer expected error CredenceOrg#109 and CredenceOrg#107 for cases that now
  correctly raise NoPendingAdmin (115) and AdminUnchanged (111). The
  expectations were stale against the wire-stable error table.
* test_suspension's min-admins test suspended the caller itself, so it
  tripped the earlier self-suspension guard (111) and never reached the
  min-admins guard. That guard is only reachable with min_admins >= 2, so
  the test now sets up two admins and suspends one of them.
* test_ownership_transfer's setup_three_super_admins called mock_all_auths
  and then re-entered as_contract, which fails with "frame is already
  authorized"; the calls are now split into separate frames.
* test_atomic_rollback, test_emergency, and the four event-reading test
  modules needed the Events trait import and a corrected
  set_pause_signer arity.
* test_basic.rs was overwritten by CredenceOrg#1478 with 875 lines of code that never
  compiled (44 `assert_eq(` instead of `assert_eq!(`, and calls to
  assign_role / reinstate_admin / suspend_admin_with_reason / env.register
  / mock_all_authentications, none of which exist). It is restored to its
  last-good revision, which is why this commit deletes 875 lines: that code
  could not build in the first place, so it was never running coverage.

Also removes the duplicate `is_admin -> bool` added by CredenceOrg#1496, which
collided with the pre-existing `is_admin -> Role`; all existing callers
and tests use the Role signature, so that one is kept. Without this the
admin crate does not compile at all.

After this, `cargo test -p admin` is green: 236 lib + 3 integration tests,
0 failures. The repaired tests were themselves checked to still fail under
mutation (removing the ROLE_REVOKED event, and removing the min-admins
guard each fail the tests that cover them).

Refs CredenceOrg#1423
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Omolola-art Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Baskarayelu

Copy link
Copy Markdown
Contributor

This pull request currently has a merge conflict with main and can't be merged yet.

Could you please resolve the conflict (merge or rebase on the latest main and push the update)? Once it's resolved, we'll merge it right away. Thanks for your contribution!

Merge upstream/main (d05ade3) into test/issue-1423-admin-completes-coverage.

Conflict resolution:
- contracts/admin/src/lib.rs, test_basic.rs: keep ours
- contracts/credence_errors/src/lib.rs: take theirs. Ours removed 10
  wire-stable ContractError variants (e.g. DuplicateIdempotencyKey, 20
  call sites in credence_bond) and renumbered SignatureExpired, which
  upstream's tip commit d05ade3 deliberately restored.

Preserved root workspace files deleted by upstream commit c9e5c7b
("Extend adversarial regression cases ... CredenceOrg#1578"), a test-only PR that
removed 59 tracked files including Cargo.toml, leaving upstream/main
with no workspace root. Cargo.lock intentionally left at upstream's
version so dependency updates are not clobbered.

Also fixed two enum defects in credence_errors inherited from upstream:
- restored SnapshotGenerationMismatch/CooldownRequestAlreadyPending/
  CooldownRequestNotFound/CooldownPeriodNotElapsed (235-238), deleted
  by upstream test commit e311fbc while still referenced by
  credence_bond and asserted in test_errors.rs
- removed 14 duplicate variant definitions (E0428/E0081) re-declared
  after the 500s block, and restored ContractIdMismatch = 221, which
  upstream had overwritten with a duplicated BytesTooLarge = 239

Dropped build_log.txt and test_out.txt build artifacts added by c9e5c7b.

Verified: no conflict markers, no staged deletions, no duplicate variant
names or discriminants, all 116 variants covered by the exhaustive
category()/description()/is_recoverable() impls, and every referenced
ContractError variant resolves. cargo check was not run (no Rust
toolchain in this environment).
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 deterministic failure-boundary coverage for completes in ./contracts/admin/src/lib.rs

2 participants