An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add test plan for MM-146 DID ceremony flow

authored by

Malpercio and committed by
Tangled
94cb35af f66b1ab0

+173
+173
docs/test-plans/2026-03-20-MM-146.md
··· 1 + # MM-146 DID Ceremony Flow — Test Plan 2 + 3 + ## Coverage Summary 4 + 5 + **Automated Criteria:** 11 | **Covered:** 11 | **Missing:** 0 6 + 7 + ### Automated Test Coverage 8 + 9 + | Criterion | Test File | Verifies | 10 + |-----------|-----------|----------| 11 + | **AC1.1** Returns 200 with `{ keyId, publicKey, algorithm }` when a signing key is provisioned | `crates/relay/src/routes/get_relay_signing_key.rs` :: `get_relay_keys_returns_200_with_active_key` | Inserts a test key via `insert_test_key`, sends GET /v1/relay/keys, asserts 200 status, deserializes body and asserts `keyId`, `algorithm`, and `publicKey` presence in the JSON response. | 12 + | **AC1.2** Returns the most recently created key when multiple keys exist | `crates/relay/src/routes/get_relay_signing_key.rs` :: `get_relay_keys_returns_most_recently_created_key` | Inserts two keys with different `created_at` timestamps ("2026-01-01" and "2026-01-02"), sends GET, asserts that the response `keyId` matches the newer key ("did:key:zNewerKey"). Confirms the `ORDER BY created_at DESC LIMIT 1` query logic. | 13 + | **AC1.3** Returns 503 when no signing key is provisioned | `crates/relay/src/routes/get_relay_signing_key.rs` :: `get_relay_keys_returns_503_when_no_key_provisioned` | Uses an empty test DB (no keys inserted), sends GET, asserts `StatusCode::SERVICE_UNAVAILABLE` (503). | 14 + | **AC1.4** Endpoint requires no authentication (public, no Bearer token) | `crates/relay/src/routes/get_relay_signing_key.rs` :: `get_relay_keys_requires_no_authentication` | `get_keys()` helper sends no Authorization header. Test inserts a key and asserts 200 response, proving the endpoint is accessible without auth. Additionally confirmed in `app.rs` line 124: the route is registered as `.route("/v1/relay/keys", get(get_relay_signing_key).post(create_signing_key))` with no auth middleware layer applied to the GET handler. The `test_state()` has `admin_token: None`, and the handler signature takes only `State(state)` with no auth extractor. | 15 + | **AC2.1** Callback receives CBOR-encoded unsigned op bytes; returned `PlcGenesisOp` passes `verify_genesis_op` | `crates/crypto/src/plc.rs` :: `external_signer_callback_produces_valid_genesis_op` | Generates a real P-256 keypair, creates a `SigningKey` from it, passes a closure that calls `Signer::sign(&sk, data)` to `build_did_plc_genesis_op_with_external_signer`. Then calls `verify_genesis_op` on the resulting `signed_op_json` with the signing key and asserts the verified DID matches the builder's DID. This proves the callback receives valid CBOR and the resulting op is cryptographically valid. | 16 + | **AC2.2** Callback returning `Err` propagates as `CryptoError::PlcOperation` | `crates/crypto/src/plc.rs` :: `external_signer_callback_error_propagates_as_plc_operation` | Passes a callback that returns `Err(CryptoError::PlcOperation("SE signing failed"))`. Asserts the result is `Err`, matches `CryptoError::PlcOperation`, and the error message contains "SE signing failed". | 17 + | **AC2.3** Existing `build_did_plc_genesis_op` (wrapper) produces identical output to before | `crates/crypto/src/plc.rs` :: *(all pre-existing tests)* | The existing tests (`did_matches_expected_format`, `signed_op_json_contains_required_fields`, `keys_placed_in_correct_positions`, `same_inputs_produce_same_did`, `invalid_signing_key_returns_error`, `sig_field_is_base64url_no_padding_and_64_bytes`, `also_known_as_contains_at_uri`, plus all `verify_genesis_op` tests) all call `build_did_plc_genesis_op`, which now delegates to `build_did_plc_genesis_op_with_external_signer` internally. If the wrapper delegation introduced a regression, these tests would fail. No new test code needed. | 18 + | **AC3.4** `NoRelaySigningKey` serializes as `{ code: "NO_RELAY_SIGNING_KEY" }` | `apps/identity-wallet/src-tauri/src/lib.rs` :: `did_ceremony_error_no_relay_signing_key_serializes_correctly` | Serializes `DIDCeremonyError::NoRelaySigningKey` to JSON and asserts `json["code"] == "NO_RELAY_SIGNING_KEY"`. | 19 + | **AC3.5** `RelayKeyFetchFailed` serializes correctly | `apps/identity-wallet/src-tauri/src/lib.rs` :: `did_ceremony_error_relay_key_fetch_failed_serializes_correctly` | Serializes `DIDCeremonyError::RelayKeyFetchFailed` to JSON and asserts `json["code"] == "RELAY_KEY_FETCH_FAILED"`. | 20 + | **AC3.6** `SigningFailed` serializes correctly | `apps/identity-wallet/src-tauri/src/lib.rs` :: `did_ceremony_error_signing_failed_serializes_correctly` | Serializes `DIDCeremonyError::SigningFailed` to JSON and asserts `json["code"] == "SIGNING_FAILED"`. | 21 + | **AC3.7** `DidCreationFailed` serializes correctly | `apps/identity-wallet/src-tauri/src/lib.rs` :: `did_ceremony_error_did_creation_failed_serializes_correctly` | Serializes `DIDCeremonyError::DidCreationFailed` to JSON and asserts `json["code"] == "DID_CREATION_FAILED"`. | 22 + 23 + ### Supporting Tests (not mapped to ACs but strengthen coverage) 24 + 25 + | Test | File | Verifies | 26 + |------|------|----------| 27 + | `did_ceremony_result_serializes_did_in_camel_case` | `lib.rs` | `DIDCeremonyResult { did }` serializes with `did` field name (camelCase rename from `#[serde(rename_all = "camelCase")]`). Ensures the TypeScript `DIDCeremonyResult.did` field matches. | 28 + | `did_ceremony_error_key_not_found_serializes_correctly` | `lib.rs` | `KeyNotFound` variant serializes as `{ code: "KEY_NOT_FOUND" }`. | 29 + | `did_ceremony_error_keychain_error_serializes_correctly` | `lib.rs` | `KeychainError` variant serializes as `{ code: "KEYCHAIN_ERROR" }`. | 30 + | `did_ceremony_error_network_error_serializes_with_message` | `lib.rs` | `NetworkError { message }` variant serializes as `{ code: "NETWORK_ERROR", message: "..." }`. | 31 + 32 + --- 33 + 34 + ## Human Test Plan 35 + 36 + ### Prerequisites 37 + 38 + - macOS with Xcode installed (iOS Simulator platform downloaded) 39 + - Nix dev shell activated from workspace root: `nix develop --impure --accept-flake-config` 40 + - Frontend dependencies installed: `cd apps/identity-wallet && pnpm install` 41 + - Xcode project generated: `cargo tauri ios init` (with PATH and sandbox patches applied per CLAUDE.md) 42 + - `cargo test` passing (confirms all automated criteria are green) 43 + - A local relay available to start/stop: `cargo run -p relay` (with appropriate config) 44 + 45 + ### Phase 1: Relay Endpoint Smoke Test (AC1) 46 + 47 + These are already fully automated but serve as a quick sanity check before mobile testing. 48 + 49 + | Step | Action | Expected | 50 + |------|--------|----------| 51 + | 1 | Start the local relay: `cargo run -p relay` | Relay starts on `http://localhost:8080` | 52 + | 2 | Provision a signing key: `POST http://localhost:8080/v1/relay/keys` with admin auth (use Bruno collection, `local` environment) | 201 response with `keyId`, `publicKey`, `algorithm` | 53 + | 3 | Fetch the key without auth: `curl http://localhost:8080/v1/relay/keys` | 200 response with JSON `{ "keyId": "did:key:z...", "publicKey": "z...", "algorithm": "p256" }` | 54 + | 4 | Stop the relay. Start a fresh relay (empty database). Fetch the key: `curl http://localhost:8080/v1/relay/keys` | 503 response with error body | 55 + 56 + ### Phase 2: DID Ceremony Happy Path (AC3.1, AC3.2, AC3.3, AC4.1, AC4.2) 57 + 58 + | Step | Action | Expected | 59 + |------|--------|----------| 60 + | 1 | Start the local relay and provision a signing key (POST /v1/relay/keys with admin auth) | Relay running with active signing key | 61 + | 2 | Generate a claim code via the relay admin API (POST /v1/accounts/claim-codes with admin auth) | Claim code returned | 62 + | 3 | Launch the app on the iOS Simulator: `cd apps/identity-wallet && cargo tauri ios dev` | App opens in Simulator showing the Welcome screen | 63 + | 4 | Tap "Get Started" on the Welcome screen | App transitions to the Claim Code screen | 64 + | 5 | Enter the claim code from step 2, tap "Next" | App transitions to the Email screen | 65 + | 6 | Enter a valid email address (e.g. `test@example.com`), tap "Next" | App transitions to the Handle screen | 66 + | 7 | Enter a valid handle (e.g. `alice`), tap "Next" | App shows the loading screen with text "Creating your account..." briefly, then transitions to the DID Ceremony screen | 67 + | 8 | **Observe the DID Ceremony loading state** | The screen displays `LoadingScreen` with text "Setting up your identity..." (confirms AC4.1). This may be brief on localhost — use Network Link Conditioner to add latency if needed. | 68 + | 9 | **Wait for the ceremony to complete** | The app transitions to the DID Success screen showing: heading "Identity Created!", a truncated DID in `did:plc:xxxxx…xxxx` format, and a "Continue" button (confirms AC4.2) | 69 + | 10 | **Verify the DID format** | The displayed DID starts with `did:plc:` and the full (non-truncated) DID is 32 characters (`did:plc:` prefix + 24 alphanumeric chars). Verify by checking Xcode console logs or relay database. (confirms AC3.1) | 70 + | 11 | **Verify Keychain session-token was overwritten** | In Terminal (while simulator is still running): check Xcode console for `tracing` log output from `keychain::store_item`, or restart the app and verify it reads the upgraded token. (confirms AC3.2) | 71 + | 12 | **Verify Keychain DID was stored** | Same approach as step 11, checking for key `"did"`. The stored value should match the DID shown on the success screen. (confirms AC3.3) | 72 + 73 + ### Phase 3: DID Success Screen Navigation (AC4.5) 74 + 75 + | Step | Action | Expected | 76 + |------|--------|----------| 77 + | 1 | After a successful ceremony (from Phase 2 step 9), tap the "Continue" button on the DID Success screen | App transitions to a placeholder screen with heading "Backup" and text "Shamir backup coming soon…" | 78 + 79 + ### Phase 4: Error Path — No Relay Signing Key (AC3.4, AC4.3) 80 + 81 + | Step | Action | Expected | 82 + |------|--------|----------| 83 + | 1 | Start a fresh local relay instance (empty database, no signing key provisioned) | Relay running, GET /v1/relay/keys returns 503 | 84 + | 2 | Launch the app on the iOS Simulator and complete account creation (claim code, email, handle) — the relay must accept the account creation step, so provision a claim code first | App reaches the DID Ceremony screen | 85 + | 3 | **Observe the error state** | The ceremony screen shows red error text: "The relay hasn't been configured yet. Please try again later." and a "Retry" button. The app does NOT navigate back to a previous screen. (confirms AC3.4 runtime behavior, AC4.3) | 86 + 87 + ### Phase 5: Error Path — Relay Unreachable (AC3.5, AC4.3, AC4.4) 88 + 89 + | Step | Action | Expected | 90 + |------|--------|----------| 91 + | 1 | Start the relay, provision a signing key, and provision a claim code | Relay ready | 92 + | 2 | Launch the app and complete account creation (claim code, email, handle) | App creates the account and transitions to the DID Ceremony screen | 93 + | 3 | **Immediately stop the relay process** (kill or Ctrl-C) before or during the ceremony | The ceremony screen shows red error text: "Couldn't reach the server. Check your connection." and a "Retry" button. The app does NOT navigate back. (confirms AC3.5 runtime behavior, AC4.3) | 94 + | 4 | **Restart the relay** (with the same database so the signing key is still provisioned) | Relay running again | 95 + | 5 | **Tap the "Retry" button** | The loading screen reappears with "Setting up your identity…", the ceremony re-executes from the beginning (re-fetches device key, relay key, builds genesis op), and on success transitions to the DID Success screen. (confirms AC4.4) | 96 + 97 + ### Phase 6: Error Path — DID Creation Fails (AC3.7) 98 + 99 + | Step | Action | Expected | 100 + |------|--------|----------| 101 + | 1 | Start the relay, provision a signing key and claim code | Relay ready | 102 + | 2 | Complete account creation in the app | App transitions to DID Ceremony screen | 103 + | 3 | **Cause POST /v1/dids to fail.** Options: (a) use the same account that already has a DID (re-run the ceremony after a successful one — the pending session token is now invalid), or (b) use a proxy like mitmproxy to intercept and return a 400/500 for POST /v1/dids | The ceremony screen shows: "Couldn't create your identity. Please try again." and a "Retry" button. (confirms AC3.7 runtime behavior) | 104 + 105 + ### Phase 7: Error Path — Signing Failed (AC3.6) 106 + 107 + | Step | Action | Expected | 108 + |------|--------|----------| 109 + | 1 | **Code review verification** (this error is difficult to trigger on Simulator): Inspect `DIDCeremonyScreen.svelte` line 47 | The `errorMessage` function maps `SIGNING_FAILED` to "Device signing failed. Please try again." | 110 + | 2 | **Automated test verification**: Confirm `did_ceremony_error_signing_failed_serializes_correctly` passes in `cargo test -p identity-wallet` | `DIDCeremonyError::SigningFailed` serializes as `{ "code": "SIGNING_FAILED" }`, matching the TypeScript switch case | 111 + | 3 | **Indirect runtime verification**: If a Secure Enclave signing failure can be triggered (e.g., by revoking key access on a physical device with biometric policy), the error path would produce the message above. On Simulator, the software signing path uses RFC 6979 and is unlikely to fail. | Accept as verified via code review and serde test. | 112 + 113 + ### End-to-End: Full Onboarding Flow 114 + 115 + **Purpose:** Validates that all eight onboarding steps work in sequence, end-to-end, with real IPC round-trips between the Svelte frontend and Rust backend. 116 + 117 + **Steps:** 118 + 1. Start the local relay, provision a signing key and a claim code. 119 + 2. Launch the app on the iOS Simulator via `cargo tauri ios dev`. 120 + 3. Welcome screen: tap "Get Started". 121 + 4. Claim Code screen: enter the claim code, tap "Next". 122 + 5. Email screen: enter an email, tap "Next". 123 + 6. Handle screen: enter a handle, tap "Next". 124 + 7. Loading screen: observe "Creating your account..." text (brief). 125 + 8. DID Ceremony screen: observe "Setting up your identity..." loading state. 126 + 9. DID Success screen: verify "Identity Created!" heading, truncated DID, and "Continue" button. 127 + 10. Tap "Continue": verify the Shamir Backup placeholder screen appears ("Backup" / "Shamir backup coming soon…"). 128 + 11. Verify the relay database contains the new DID record (query `SELECT * FROM dids ORDER BY created_at DESC LIMIT 1`). 129 + 130 + --- 131 + 132 + ## Human Verification Required 133 + 134 + | Criterion | Why Manual | Steps | 135 + |-----------|------------|-------| 136 + | AC3.1: Returns `DIDCeremonyResult { did }` with valid `did:plc:` | Requires Keychain (token retrieval), Secure Enclave/software signing, and live HTTP round-trips to a running relay. No mock seams exist. | Phase 2, steps 8-10 | 137 + | AC3.2: Keychain `"session-token"` overwritten with full session token | Keychain writes require a running app with correct entitlements; `Security.framework` calls cannot be mocked in `cargo test`. | Phase 2, step 11 | 138 + | AC3.3: Keychain `"did"` populated with resulting DID | Same as AC3.2. | Phase 2, step 12 | 139 + | AC3.4: `NoRelaySigningKey` when relay has no key (runtime) | Requires an actual HTTP 503 from a real relay with no signing key. | Phase 4, steps 1-3 | 140 + | AC3.5: `RelayKeyFetchFailed` when relay unreachable (runtime) | Requires actual network failure (relay process stopped). | Phase 5, steps 1-3 | 141 + | AC3.6: `SigningFailed` when SE signing fails (runtime) | Secure Enclave failures cannot be reliably triggered on Simulator. Verified via code review + serde test. | Phase 7 | 142 + | AC3.7: `DidCreationFailed` when POST /v1/dids returns non-2xx (runtime) | Requires specific relay state producing a non-2xx response. | Phase 6, steps 1-3 | 143 + | AC4.1: Loading screen with "Setting up your identity..." | UI rendering requires Tauri runtime + WKWebView. | Phase 2, step 8 | 144 + | AC4.2: Success screen with truncated DID and "Continue" button | Requires real DID from ceremony and Svelte rendering pipeline. | Phase 2, step 9 | 145 + | AC4.3: Error shows inline message and Retry button (no rewind) | Tests the error UI path end-to-end. | Phase 4, step 3; Phase 5, step 3 | 146 + | AC4.4: Retry button re-invokes ceremony from beginning | Verifies `runCeremony()` is called again from scratch. | Phase 5, steps 4-5 | 147 + | AC4.5: "Continue" transitions to `shamir_backup` placeholder | Simple navigation check. | Phase 3, step 1 | 148 + 149 + --- 150 + 151 + ## Traceability 152 + 153 + | Acceptance Criterion | Automated Test | Manual Step | 154 + |----------------------|----------------|-------------| 155 + | AC1.1: GET /v1/relay/keys returns 200 with key fields | `get_relay_keys_returns_200_with_active_key` | Phase 1, step 3 | 156 + | AC1.2: Returns most recently created key | `get_relay_keys_returns_most_recently_created_key` | — | 157 + | AC1.3: Returns 503 when no key provisioned | `get_relay_keys_returns_503_when_no_key_provisioned` | Phase 1, step 4 | 158 + | AC1.4: No authentication required | `get_relay_keys_requires_no_authentication` | Phase 1, step 3 (no auth header used) | 159 + | AC2.1: External signer callback produces valid genesis op | `external_signer_callback_produces_valid_genesis_op` | — | 160 + | AC2.2: Callback error propagates as PlcOperation | `external_signer_callback_error_propagates_as_plc_operation` | — | 161 + | AC2.3: Wrapper produces identical output | *(all pre-existing plc.rs tests)* | — | 162 + | AC3.1: Ceremony returns valid did:plc | — | Phase 2, steps 8-10 | 163 + | AC3.2: Keychain session-token overwritten | — | Phase 2, step 11 | 164 + | AC3.3: Keychain did populated | — | Phase 2, step 12 | 165 + | AC3.4: NoRelaySigningKey error (serde + runtime) | `did_ceremony_error_no_relay_signing_key_serializes_correctly` | Phase 4, steps 1-3 | 166 + | AC3.5: RelayKeyFetchFailed error (serde + runtime) | `did_ceremony_error_relay_key_fetch_failed_serializes_correctly` | Phase 5, steps 1-3 | 167 + | AC3.6: SigningFailed error (serde + runtime) | `did_ceremony_error_signing_failed_serializes_correctly` | Phase 7 (code review) | 168 + | AC3.7: DidCreationFailed error (serde + runtime) | `did_ceremony_error_did_creation_failed_serializes_correctly` | Phase 6, steps 1-3 | 169 + | AC4.1: Loading screen during ceremony | — | Phase 2, step 8 | 170 + | AC4.2: Success screen with truncated DID | — | Phase 2, step 9 | 171 + | AC4.3: Error shows inline message + Retry (no rewind) | — | Phase 4, step 3; Phase 5, step 3 | 172 + | AC4.4: Retry re-invokes ceremony from beginning | — | Phase 5, steps 4-5 | 173 + | AC4.5: Continue transitions to shamir_backup | — | Phase 3, step 1 |