Mutation testing validates that the test suite can catch subtle bugs introduced by deliberate code mutations. Unlike traditional code coverage metrics, mutation testing proves that tests would actually fail when code is broken.
For TrustLink, mutation testing focuses on critical security-sensitive code paths in src/validation.rs and src/attestation.rs.
Install cargo-mutants:
cargo install cargo-mutantsExecute mutation testing on validation and attestation modules:
# Test all validation logic
cargo mutants --package trustlink --file src/validation.rs
# Test attestation creation and revocation
cargo mutants --package trustlink --file src/attestation.rs
# Test with verbose output
cargo mutants --package trustlink -v --file src/validation.rscargo-mutants will:
- Introduce deliberate bugs (mutations) into the code
- Run the test suite for each mutation
- Report which mutations were killed (caught by tests) vs. survived (missed by tests)
Expected output:
- Killed: Test suite caught the bug ✅
- Survived: Test suite missed the bug ❌ (indicates missing test coverage)
None – all mutations in validation and attestation are caught by the test suite.
If mutation testing reveals survivors, add targeted tests to tests/mutation_testing.rs:
- Identify the mutation – e.g., "changed
!=to==in admin check" - Understand the risk – e.g., "would allow unauthorized users to perform admin operations"
- Write a test – e.g., verify that an unauthorized user's operation fails
Example template for a survivor fix:
#[test]
fn test_mutation_inverted_check_would_fail() {
let env = Env::default();
let admin = Address::random(&env);
let unauthorized = Address::random(&env);
admin.require_auth();
contract.initialize(&env, &admin, &None);
// This should fail if the authorization check is inverted
unauthorized.require_auth();
assert!(contract.operation(&env, &unauthorized).is_err());
}Mutation: != → == or vice versa
Tests:
test_require_admin_inverted_check_would_failtest_require_issuer_must_return_error
Risk: Allows unauthorized access to admin/issuer functions
Mutation: Delete if !Storage::is_issuer(...) line
Tests:
test_require_issuer_must_return_errortest_require_authorized_creator_accepts_either_issuer_or_bridge
Risk: Anyone can create/revoke attestations
Mutation: || → &&
Tests:
test_require_authorized_creator_accepts_either_issuer_or_bridge
Risk: Requires both issuer AND bridge status (too restrictive, breaks bridges)
Mutation: len > 64 → len >= 64
Tests:
test_validate_claim_type_boundary_64_charstest_validate_metadata_boundary_256_chars
Risk: Off-by-one errors in length validation
Mutation: Remove return Err(Error::Unauthorized) statements
Tests:
test_require_not_paused_blocks_operationstest_require_admin_short_circuits_on_first_unauthorized
Risk: Authorization bypass; state mutations before auth checks
Mutation testing can be integrated into CI to prevent regressions:
# .github/workflows/mutation-testing.yml
- name: Run mutation tests
run: |
cargo install cargo-mutants
cargo mutants --package trustlink --file src/validation.rs --file src/attestation.rs
# Fail if any mutations survive
if [ $? -ne 0 ]; then exit 1; fi- Run mutations regularly – At least before security audits
- Fix survivors immediately – Each survivor represents a potential security gap
- Keep mutation tests updated – As new code paths are added, add corresponding mutation tests
- Document surviving mutants – If a mutation is acceptable, explain why (rare for security code)
- Use mutation testing alongside fuzzing – Complements fuzzing for comprehensive coverage