An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

docs: add test plan for MM-90 DID ceremony endpoint

authored by malpercio.dev and committed by

Tangled 85b7219f 814ce9df

+116
+116
docs/test-plans/2026-03-13-MM-90.md
··· 1 + # Test Plan: MM-90 DID Ceremony — Device-Signed Genesis Op 2 + 3 + **Implementation plan:** `docs/implementation-plans/2026-03-13-MM-90/` 4 + **Coverage:** 19/19 acceptance criteria — all automated 5 + 6 + --- 7 + 8 + ## Prerequisites 9 + 10 + - Development shell active (`nix develop --impure --accept-flake-config` or direnv) 11 + - `cargo test` passes with zero failures before this plan is run 12 + - Bruno desktop app installed (for API collection inspection) 13 + 14 + --- 15 + 16 + ## Phase 1: Crypto Unit Tests 17 + 18 + | Step | Action | Expected | 19 + |------|--------|----------| 20 + | 1.1 | `cargo test -p crypto -- verify_valid_op_returns_correct_fields` | Passes. `VerifiedGenesisOp` fields correct (did starts with `did:plc:`, 32 chars; `also_known_as` contains `at://alice.example.com`; `verification_methods` has `atproto`; `atproto_pds_endpoint` set). | 21 + | 1.2 | `cargo test -p crypto -- verify_did_matches_build_did_plc_genesis_op` | Passes. DID from `verify_genesis_op` matches DID from `build_did_plc_genesis_op` (CBOR round-trip). | 22 + | 1.3 | `cargo test -p crypto -- verify_wrong_rotation_key_returns_error` | Passes. Wrong key returns `CryptoError::PlcOperation`. | 23 + | 1.4 | `cargo test -p crypto -- verify_corrupted_signature_returns_error` | Passes. Corrupted signature returns `CryptoError::PlcOperation`. | 24 + | 1.5 | `cargo test -p crypto -- verify_unknown_fields_returns_error` | Passes. Unknown JSON field is rejected by `deny_unknown_fields`. | 25 + 26 + --- 27 + 28 + ## Phase 2: Integration Tests (Happy Path) 29 + 30 + | Step | Action | Expected | 31 + |------|--------|----------| 32 + | 2.1 | `cargo test -p relay -- happy_path_promotes_account_and_returns_did` | Passes. Covers AC2.1–AC2.5 and AC4.1–AC4.3: HTTP 200, `did` starts with `did:plc:`, `status == "active"`, `did_document` is a JSON object, `alsoKnownAs` contains `at://{handle}`, `verificationMethod[0].publicKeyMultibase` starts with `z`, `service[0].serviceEndpoint == "https://test.example.com"`, all DB state transitions complete (accounts row, did_documents row, handles row, pending rows deleted). | 33 + | 2.2 | `cargo test -p relay -- retry_with_pending_did_skips_plc_directory` | Passes. Retry with pre-stored `pending_did` returns 200 without calling plc.directory (wiremock `expect(0)` enforces this). | 34 + 35 + --- 36 + 37 + ## Phase 3: Integration Tests (Error Paths) 38 + 39 + | Step | Action | Expected | 40 + |------|--------|----------| 41 + | 3.1 | `cargo test -p relay -- invalid_signature_returns_400` | Passes. Corrupted sig returns HTTP 400, `error.code == "INVALID_CLAIM"`. | 42 + | 3.2 | `cargo test -p relay -- wrong_handle_in_op_returns_400` | Passes. Handle mismatch returns HTTP 400, `error.code == "INVALID_CLAIM"`. | 43 + | 3.3 | `cargo test -p relay -- wrong_service_endpoint_returns_400` | Passes. Endpoint mismatch returns HTTP 400, `error.code == "INVALID_CLAIM"`. | 44 + | 3.4 | `cargo test -p relay -- wrong_rotation_key_in_op_returns_400` | Passes. `rotationKeys[0]` mismatch returns HTTP 400, `error.code == "INVALID_CLAIM"`. (Crypto verification passes; semantic check fails.) | 45 + | 3.5 | `cargo test -p relay -- already_promoted_account_returns_409` | Passes. Returns HTTP 409, `error.code == "DID_ALREADY_EXISTS"`. | 46 + | 3.6 | `cargo test -p relay -- missing_auth_returns_401` | Passes. No `Authorization` header returns HTTP 401, `error.code == "UNAUTHORIZED"`. | 47 + | 3.7 | `cargo test -p relay -- plc_directory_error_returns_502` | Passes. plc.directory returning 500 causes HTTP 502, `error.code == "PLC_DIRECTORY_ERROR"`. | 48 + 49 + --- 50 + 51 + ## Phase 4: Full Suite Validation 52 + 53 + | Step | Action | Expected | 54 + |------|--------|----------| 55 + | 4.1 | `cargo test --workspace` | All 269 tests pass (45 common + 39 crypto + 185 relay). No regressions. | 56 + | 4.2 | `cargo clippy --workspace -- -D warnings` | Zero warnings or errors. | 57 + | 4.3 | `cargo fmt --all --check` | Formatting passes (exit code 0). | 58 + 59 + --- 60 + 61 + ## End-to-End: DID Ceremony Flow 62 + 63 + **Purpose:** Validate the full provisioning pipeline (claim code → pending account → device → pending session → DID ceremony → promoted account). 64 + 65 + 1. Run `cargo test -p relay -- happy_path_promotes_account_and_returns_did` and observe the output. The test exercises the entire chain: inserts a claim code, creates a pending account, registers a device, creates a pending session, generates a P-256 keypair, builds and signs a genesis op, dispatches `POST /v1/dids`, and verifies all database state transitions complete atomically. 66 + 2. Confirm wiremock received exactly 1 POST request to plc.directory (enforced by `expect(1)`). 67 + 3. Run `cargo test -p relay -- retry_with_pending_did_skips_plc_directory` and confirm the retry path returns 200 without calling plc.directory (`expect(0)`). 68 + 69 + --- 70 + 71 + ## End-to-End: Semantic Validation Isolation 72 + 73 + **Purpose:** Verify that AC3.2–AC3.4 semantic checks are independent from cryptographic verification. A valid signature must not bypass semantic validation. 74 + 75 + 1. `cargo test -p relay -- wrong_rotation_key_in_op_returns_400` — the op was signed by `kp_x` (crypto verification passes), but `rotationKeys[0]` is `kp_y`. Confirm HTTP 400. 76 + 2. `cargo test -p relay -- wrong_handle_in_op_returns_400` — valid signature, wrong `alsoKnownAs[0]`. Confirm HTTP 400. 77 + 3. `cargo test -p relay -- wrong_service_endpoint_returns_400` — valid signature, wrong `services.atproto_pds.endpoint`. Confirm HTTP 400. 78 + 79 + --- 80 + 81 + ## Bruno API Collection Inspection 82 + 83 + | Step | Action | Expected | 84 + |------|--------|----------| 85 + | B.1 | Open `bruno/` collection in Bruno desktop app | Collection loads without errors. | 86 + | B.2 | Inspect `create-did.bru` request body | Body contains `rotationKeyPublic` (string, `did:key:` URI) and `signedCreationOp` (JSON object). Method is POST, path is `/v1/dids`. No merge conflict markers. | 87 + | B.3 | Verify authorization | `Authorization` header uses `Bearer {{pendingSessionToken}}`. | 88 + 89 + --- 90 + 91 + ## Traceability 92 + 93 + | Acceptance Criterion | Automated Test | Manual Step | 94 + |----------------------|----------------|-------------| 95 + | AC1.1 | `verify_valid_op_returns_correct_fields` | 1.1 | 96 + | AC1.2 | `verify_did_matches_build_did_plc_genesis_op` | 1.2 | 97 + | AC1.3 | `verify_wrong_rotation_key_returns_error` | 1.3 | 98 + | AC1.4 | `verify_corrupted_signature_returns_error` | 1.4 | 99 + | AC1.5 | `verify_unknown_fields_returns_error` | 1.5 | 100 + | AC2.1 | `happy_path_promotes_account_and_returns_did` | 2.1 | 101 + | AC2.2 | `happy_path_promotes_account_and_returns_did` | 2.1 | 102 + | AC2.3 | `happy_path_promotes_account_and_returns_did` | 2.1 | 103 + | AC2.4 | `happy_path_promotes_account_and_returns_did` | 2.1 | 104 + | AC2.5 | `happy_path_promotes_account_and_returns_did` | 2.1 | 105 + | AC2.6 | `retry_with_pending_did_skips_plc_directory` | 2.2 | 106 + | AC3.1 | `invalid_signature_returns_400` | 3.1 | 107 + | AC3.2 | `wrong_handle_in_op_returns_400` | 3.2 | 108 + | AC3.3 | `wrong_service_endpoint_returns_400` | 3.3 | 109 + | AC3.4 | `wrong_rotation_key_in_op_returns_400` | 3.4 | 110 + | AC3.5 | `already_promoted_account_returns_409` | 3.5 | 111 + | AC3.6 | `missing_auth_returns_401` | 3.6 | 112 + | AC3.7 | `plc_directory_error_returns_502` | 3.7 | 113 + | AC4.1 | `happy_path_promotes_account_and_returns_did` | 2.1 | 114 + | AC4.2 | `happy_path_promotes_account_and_returns_did` | 2.1 | 115 + | AC4.3 | `happy_path_promotes_account_and_returns_did` | 2.1 | 116 + | Bruno collection (dev tooling) | N/A | B.1–B.3 |