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

docs: add test plan for MM-92 relay signing key generation

authored by malpercio.dev and committed by

Tangled 279c7d12 734ebc83

+162
+162
docs/test-plans/2026-03-11-MM-92.md
··· 1 + # Human Test Plan: MM-92 Relay Signing Key Generation 2 + 3 + ## Prerequisites 4 + 5 + - Development shell is active (`nix develop --impure --accept-flake-config` or direnv) 6 + - All automated tests pass: `cargo test --workspace` 7 + - Clippy is clean: `cargo clippy --workspace -- -D warnings` 8 + - Formatting is clean: `cargo fmt --all --check` 9 + 10 + ## Phase 1: Crypto Crate Unit Tests 11 + 12 + | Step | Action | Expected | 13 + |------|--------|----------| 14 + | 1.1 | Run `cargo test -p crypto` from the workspace root | All 9 tests in `crates/crypto/src/keys.rs` pass. Zero failures. | 15 + | 1.2 | Verify test output includes `generate_keypair_produces_valid_did_key` | Test name appears in output with `ok` status | 16 + | 1.3 | Verify test output includes `encrypt_decrypt_round_trip` | Test name appears in output with `ok` status | 17 + | 1.4 | Verify test output includes `encrypt_produces_different_ciphertexts_for_same_input` | Test name appears in output with `ok` status | 18 + 19 + ## Phase 2: Config Parsing Unit Tests 20 + 21 + | Step | Action | Expected | 22 + |------|--------|----------| 23 + | 2.1 | Run `cargo test -p common` from the workspace root | All tests in `crates/common/src/config.rs` pass, including the 6 new MM-92 tests. Zero failures. | 24 + | 2.2 | Verify test output includes `env_override_admin_token` | Test name appears with `ok` | 25 + | 2.3 | Verify test output includes `env_override_signing_key_master_key_valid_hex` | Test name appears with `ok` | 26 + | 2.4 | Verify test output includes `env_override_signing_key_master_key_wrong_length_returns_error` | Test name appears with `ok` | 27 + | 2.5 | Verify test output includes `env_override_signing_key_master_key_non_hex_returns_error` | Test name appears with `ok` | 28 + 29 + ## Phase 3: Integration Tests (Relay Endpoint) 30 + 31 + | Step | Action | Expected | 32 + |------|--------|----------| 33 + | 3.1 | Run `cargo test -p relay` from the workspace root | All tests pass, including 11 new endpoint tests and 1 new migration test. Zero failures. | 34 + | 3.2 | Verify test output includes `create_signing_key_returns_200_with_key_fields` | Test name appears with `ok` | 35 + | 3.3 | Verify test output includes `row_persisted_in_db_with_encrypted_private_key` | Test name appears with `ok` | 36 + | 3.4 | Verify test output includes `v003_relay_signing_keys_table_exists` | Test name appears with `ok` | 37 + | 3.5 | Verify test output includes `missing_master_key_returns_503` | Test name appears with `ok` | 38 + 39 + ## End-to-End: Full Workspace Validation 40 + 41 + **Purpose:** Confirm all crates compile together, all tests pass, and code quality gates are met. 42 + 43 + 1. From the workspace root, run `cargo fmt --all --check`. Expected: no output, exit code 0. 44 + 2. Run `cargo clippy --workspace -- -D warnings`. Expected: no warnings or errors, exit code 0. 45 + 3. Run `cargo test --workspace`. Expected: all tests pass across all crates (common, crypto, relay, repo-engine). Zero failures. 46 + 4. Run `cargo build --workspace`. Expected: clean build, no errors. 47 + 48 + ## End-to-End: Config-to-Endpoint Integration 49 + 50 + **Purpose:** Validate that env vars flow correctly into the endpoint. 51 + 52 + 1. Set environment variables: 53 + ``` 54 + export EZPDS_ADMIN_TOKEN="manual-test-token" 55 + export EZPDS_SIGNING_KEY_MASTER_KEY="0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" 56 + ``` 57 + 2. Start the relay (ensure `data_dir`, `public_url`, and `available_user_domains` are configured). 58 + 3. Send a request with no auth: 59 + ``` 60 + curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/v1/relay/keys \ 61 + -H "Content-Type: application/json" \ 62 + -d '{"algorithm": "p256"}' 63 + ``` 64 + Expected: `401` 65 + 4. Send a request with the wrong token: 66 + ``` 67 + curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/v1/relay/keys \ 68 + -H "Content-Type: application/json" \ 69 + -H "Authorization: Bearer wrong-token" \ 70 + -d '{"algorithm": "p256"}' 71 + ``` 72 + Expected: `401` 73 + 5. Send a valid request: 74 + ``` 75 + curl -s -X POST http://localhost:8080/v1/relay/keys \ 76 + -H "Content-Type: application/json" \ 77 + -H "Authorization: Bearer manual-test-token" \ 78 + -d '{"algorithm": "p256"}' 79 + ``` 80 + Expected: HTTP 200 with JSON body containing `keyId` (starts with `did:key:z`), `publicKey` (starts with `z`, no `did:key:` prefix), and `algorithm` equal to `"p256"`. No `privateKey` or `private_key` field. 81 + 6. Send a request with unsupported algorithm: 82 + ``` 83 + curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/v1/relay/keys \ 84 + -H "Content-Type: application/json" \ 85 + -H "Authorization: Bearer manual-test-token" \ 86 + -d '{"algorithm": "k256"}' 87 + ``` 88 + Expected: `400` 89 + 90 + ## End-to-End: Missing Master Key Behavior 91 + 92 + **Purpose:** Validate 503 when master key is not configured. 93 + 94 + 1. Unset the master key: `unset EZPDS_SIGNING_KEY_MASTER_KEY` 95 + 2. Keep `EZPDS_ADMIN_TOKEN="manual-test-token"` set. 96 + 3. Start (or restart) the relay. 97 + 4. Expected: The relay starts successfully (no crash on missing master key). 98 + 5. Send a valid auth request: 99 + ``` 100 + curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/v1/relay/keys \ 101 + -H "Content-Type: application/json" \ 102 + -H "Authorization: Bearer manual-test-token" \ 103 + -d '{"algorithm": "p256"}' 104 + ``` 105 + Expected: `503` 106 + 107 + ## End-to-End: Database Persistence Check 108 + 109 + **Purpose:** Confirm the generated key is persisted in SQLite with encrypted private key. 110 + 111 + 1. After a successful key generation (Config-to-Endpoint step 5), note the `keyId` from the response. 112 + 2. Open the relay database: `sqlite3 <data_dir>/relay.db` 113 + 3. Run: 114 + ```sql 115 + SELECT id, algorithm, public_key, length(private_key_encrypted), created_at 116 + FROM relay_signing_keys WHERE id = '<keyId>'; 117 + ``` 118 + Expected: One row with `id` matching `keyId`, `algorithm` = `p256`, `public_key` matching response `publicKey`, `length(private_key_encrypted)` = `80`, and `created_at` with an ISO 8601 timestamp. 119 + 4. Run: 120 + ```sql 121 + SELECT private_key_encrypted FROM relay_signing_keys WHERE id = '<keyId>'; 122 + ``` 123 + Expected: An 80-character base64 string (letters, digits, `+`, `/`, possibly `=` padding). Must not look like raw hex or plaintext. 124 + 125 + ## Human Verification Required 126 + 127 + | Criterion | Why Manual | Steps | 128 + |-----------|------------|-------| 129 + | AC8.1 — `cargo test --workspace` passes | Meta-criterion | Run from workspace root. Confirm zero failures. | 130 + | AC8.2 — `cargo clippy --workspace -- -D warnings` passes | Meta-criterion | Run from workspace root. Confirm zero warnings/errors. | 131 + | AC8.3 — `cargo fmt --all --check` passes | Meta-criterion | Run from workspace root. Confirm no output. | 132 + 133 + ## Traceability 134 + 135 + | Acceptance Criterion | Automated Test | Manual Step | 136 + |----------------------|----------------|-------------| 137 + | AC1.1 | `create_signing_key_returns_200_with_key_fields` | Config step 5 | 138 + | AC1.2 | `key_id_is_did_key_uri` + `generate_keypair_produces_valid_did_key` | Config step 5 | 139 + | AC1.3 | `public_key_is_multibase_base58btc` + `generate_keypair_public_key_is_multibase_without_did_prefix` | Config step 5 | 140 + | AC1.4 | `create_signing_key_returns_200_with_key_fields` | Config step 5 | 141 + | AC1.5 | `row_persisted_in_db_with_encrypted_private_key` | DB Persistence steps 3–4 | 142 + | AC2.1 | `response_has_no_private_key_field` | Config step 5 | 143 + | AC2.2 | `row_persisted_in_db_with_encrypted_private_key` | DB Persistence steps 4–5 | 144 + | AC3.1 | `encrypt_decrypt_round_trip` | Phase 1 step 1.3 | 145 + | AC3.2 | `decrypt_with_wrong_master_key_fails` | Phase 1 step 1.1 | 146 + | AC3.3 | `decrypt_invalid_base64_fails` + `decrypt_wrong_length_fails` | Phase 1 step 1.1 | 147 + | AC3.4 | `encrypt_produces_different_ciphertexts_for_same_input` | Phase 1 step 1.4 | 148 + | AC4.1 | `missing_authorization_header_returns_401` | Config step 3 | 149 + | AC4.2 | `wrong_bearer_token_returns_401` | Config step 4 | 150 + | AC4.3 | `bare_token_without_bearer_prefix_returns_401` | Phase 3 step 3.1 | 151 + | AC5.1 | `unsupported_algorithm_returns_400` | Config step 6 | 152 + | AC5.2 | `empty_algorithm_returns_400` | Phase 3 step 3.1 | 153 + | AC5.3 | `missing_algorithm_field_returns_400` | Phase 3 step 3.1 | 154 + | AC6.1 | `missing_master_key_returns_503` | Missing Master Key step 5 | 155 + | AC7.1 | `env_override_admin_token` | Phase 2 step 2.2 | 156 + | AC7.2 | `env_override_signing_key_master_key_valid_hex` | Phase 2 step 2.3 | 157 + | AC7.3 | `env_override_signing_key_master_key_wrong_length_returns_error` | Phase 2 step 2.4 | 158 + | AC7.4 | `env_override_signing_key_master_key_non_hex_returns_error` | Phase 2 step 2.5 | 159 + | AC7.5 | `admin_token_is_optional` + `signing_key_master_key_is_optional` | Missing Master Key step 4 | 160 + | AC8.1 | — | `cargo test --workspace` | 161 + | AC8.2 | — | `cargo clippy --workspace -- -D warnings` | 162 + | AC8.3 | — | `cargo fmt --all --check` |