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 human test plan for PLC monitoring and alerting

+147
+147
docs/test-plans/2026-03-29-plc-monitoring-alerting.md
··· 1 + # Test Analysis: PLC Monitoring & Alerting (AC6) 2 + 3 + **Date:** 2026-03-29 4 + **Base SHA:** 684f9a0 5 + **Head SHA:** 3741eb5 6 + 7 + --- 8 + 9 + ## Coverage Validation 10 + 11 + **Automated Criteria:** 5 | **Covered:** 5 | **Missing:** 0 12 + 13 + ### Covered 14 + 15 + | Criterion | Test File | Verifies | 16 + |-----------|-----------|----------| 17 + | AC6.1: Authorized operation detection | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_1_authorized_change_detected` | Builds a real PLC genesis op signed by the device key using `crypto::build_did_plc_genesis_op`, serves it via `httpmock::MockServer`, calls `check_for_changes(did)`, asserts return is `Ok(vec![])` (no alerts). Second call verifies cache was updated (still no alerts from same log). | 18 + | AC6.1 (multi-identity) | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_1_multi_identity_all_authorized` | Registers two DIDs (alice, bob), both with device-key-signed genesis ops, calls `check_all()`, asserts both `IdentityStatus` entries have `alert_count: 0`. | 19 + | AC6.2: Unauthorized operation detection | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_2_unauthorized_change_detected` | Generates a genesis op signed by a different P-256 key (not the device key), calls `check_for_changes(did)`, asserts exactly one `UnauthorizedChange` with matching CID `"bafy_ac62_genesis"`. | 20 + | AC6.2 (multi-identity) | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_2_multi_identity_mixed_auth` | Two DIDs: alice authorized (device-key-signed), bob unauthorized (other-key-signed). Calls `check_all()`, asserts alice `alert_count: 0`, bob `alert_count: 1`. | 21 + | AC6.3: Recovery deadline correctness (backend) | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_3_created_at_matches_audit_log` | Sets `createdAt: "2026-03-29T12:34:56.789Z"` in mock audit log, triggers an unauthorized change, asserts `changes[0].created_at == "2026-03-29T12:34:56.789Z"`. Proves the raw ISO 8601 timestamp is faithfully passed through for frontend deadline computation. | 22 + | AC6.3: Recovery deadline correctness (frontend) | `apps/identity-wallet/src/lib/utils/deadline.test.ts` | Tests `getDeadline('2026-03-29T12:00:00.000Z')` returns exactly 72 hours later. Tests `getUrgency` at all 4 threshold boundaries: safe (>24h), warning (4-24h), critical (<4h), expired (<=0). Tests `formatCountdown` edge cases: 72h remaining, 23h 59m, 0h 1m, exactly expired, past expired. Validates `RECOVERY_WINDOW_MS == 72 * 60 * 60 * 1000`. | 23 + | AC6.7: Graceful plc.directory unreachable | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_7_network_error_graceful_handling` | Mock server returns HTTP 500, calls `check_for_changes(did)`, asserts `Ok(vec![])` (no error propagated, no alerts). Cache preservation is structurally guaranteed: `fetch_audit_log` failure causes early return at line 86, before `store_plc_log` at line 159 is ever reached. | 24 + | AC6.8: Empty audit log handling | `apps/identity-wallet/src-tauri/src/plc_monitor.rs` -- `test_ac6_8_empty_audit_log` | Mock server returns `[]`, calls `check_for_changes(did)` on a freshly registered identity (no cached log), asserts `Ok(vec![])`. Covers both "no cached log" and "empty remote log" paths. | 25 + 26 + ### Notes on AC6.2 Signing Key Assertion 27 + 28 + The test requirements specify verifying that `signing_key` matches the `did:key` URI of the non-device key. The `test_ac6_2_unauthorized_change_detected` test does not explicitly assert on `signing_key`. This is structurally correct: since the test uses a single-entry audit log (genesis only), `identify_signing_key` has no previous entry to extract rotation keys from, so `signing_key` is `None`. The test correctly validates the primary criterion (unauthorized change detection). Signing key identification requires a multi-entry log, which is a supplementary behavior rather than the core acceptance criterion. 29 + 30 + ### Additional Tests (Serialization + Error Types) 31 + 32 + The test module also includes 8 unit tests for serialization correctness: 33 + - `test_unauthorized_change_serializes_camel_case` -- verifies `createdAt`, `signingKey` camelCase 34 + - `test_unauthorized_change_no_signing_key` -- verifies `signingKey: null` serialization 35 + - `test_identity_status_serializes_camel_case` -- verifies `alertCount`, `unauthorizedChanges` camelCase 36 + - `test_identity_status_with_changes` -- verifies nested serialization 37 + - `test_plc_monitor_creation` -- verifies `PlcMonitor::new` compiles 38 + - `test_monitor_error_network_error` -- verifies `NETWORK_ERROR` tag 39 + - `test_monitor_error_identity_store_error` -- verifies `IDENTITY_STORE_ERROR` tag 40 + - `test_monitor_error_parse_error` -- verifies `PARSE_ERROR` tag 41 + 42 + These support IPC contract correctness between the Rust backend and TypeScript frontend. 43 + 44 + **Result: PASS** 45 + 46 + --- 47 + 48 + ## Human Test Plan 49 + 50 + ### Prerequisites 51 + 52 + - macOS workstation with Xcode installed and iOS Simulator available 53 + - Nix dev shell active (`nix develop --impure --accept-flake-config` from workspace root) 54 + - Frontend dependencies installed (`cd apps/identity-wallet && pnpm install`) 55 + - Automated backend tests passing: `cargo test -p identity-wallet` (from workspace root) 56 + - Automated frontend tests passing: `cd apps/identity-wallet && pnpm test` 57 + - Type checking passing: `cd apps/identity-wallet && pnpm check` 58 + - App launchable in iOS Simulator: `cd apps/identity-wallet && cargo tauri ios dev` 59 + - At least one identity claimed in the wallet (complete either the Create or Import flow first) 60 + 61 + ### Phase 1: Alert Badge on Home Screen (AC6.4) 62 + 63 + This phase verifies that the home screen renders a red alert badge on identity cards with active alerts. 64 + 65 + | Step | Action | Expected | 66 + |------|--------|----------| 67 + | 1.1 | Launch the app in the iOS Simulator. Complete the Import flow (or verify an existing claimed identity is present). The home screen (`IdentityListHome`) should display at least one identity card. | Identity card visible with handle, DID, PDS URL, and a "Root Key" or "Not Root" badge. | 68 + | 1.2 | Observe the identity card before any unauthorized operations exist. | No red "Alert" badge is visible. Only the rotation key status badge (green "Root Key", amber "Not Root", or gray "Unknown") is shown. | 69 + | 1.3 | Simulate an unauthorized PLC operation: this requires either (a) using a staging plc.directory where you can inject a rotation operation signed by a different key, or (b) modifying the `check_for_changes` response in a debug build to return a mock `UnauthorizedChange`. The simplest approach is to temporarily modify `check_identity_status` in `plc_monitor.rs` to return a hardcoded `IdentityStatus` with `alert_count: 1` and one `UnauthorizedChange` entry. | N/A (setup step). | 70 + | 1.4 | After the mock unauthorized operation is detectable, trigger a foreground check by suspending the app (press Home button in Simulator) then returning to the app, or wait for the background 15-minute cycle. Alternatively, pull-to-refresh or tap the refresh button on the home screen. | The `checkIdentityStatus()` call fires (observable in Xcode console logs). | 71 + | 1.5 | Observe the identity card for the affected DID. | A red badge appears showing "1 Alert" (or "N Alerts" for multiple). The badge has: `#fef2f2` background, a `#ef4444` red dot, and `#991b1b` dark red text. It is positioned above the rotation key badge in a vertical stack. | 72 + | 1.6 | If multiple identities exist, observe a second identity card that has no unauthorized operations. | The second card shows no red alert badge. Only the rotation key status badge is visible. | 73 + | 1.7 | Tap the red alert badge on the affected identity card. | Navigation transitions to the alert detail screen (`AlertDetailScreen`). The tap does NOT trigger the identity card's primary `onselect` action (DID document screen). | 74 + 75 + ### Phase 2: Alert Detail Screen Content (AC6.5) 76 + 77 + This phase verifies the content and visual presentation of the alert detail screen. 78 + 79 + | Step | Action | Expected | 80 + |------|--------|----------| 81 + | 2.1 | Arrive at the alert detail screen (from step 1.7 above). | Screen title reads "Security Alerts". The truncated DID is displayed under an "IDENTITY" label. One or more alert cards are visible below. | 82 + | 2.2 | Examine the signing key field on the alert card. | Displays either a `did:key:z...` URI in monospace font, or "Unknown key" if the signing key could not be identified. | 83 + | 2.3 | Examine the "Detected" timestamp field. | Shows a human-readable date/time (formatted by `toLocaleString()`, e.g. "3/29/2026, 12:00:00 AM"). This is the `createdAt` from plc.directory. | 84 + | 2.4 | Examine the "Recovery Deadline" field. | Shows a date/time that is exactly 72 hours after the "Detected" timestamp (e.g., if detected on 3/29 at noon, deadline is 4/1 at noon). | 85 + | 2.5 | Observe the countdown timer at the top of the alert card. | Displays time remaining in "Xh Ym remaining" format (e.g., "71h 45m remaining"). The badge is color-coded: green background if >24h remaining, amber if 4-24h, red if <4h or expired. | 86 + | 2.6 | Wait approximately 60 seconds while on the alert detail screen. | The countdown timer updates (the minutes value decreases by 1). The urgency color may change if crossing a threshold boundary. | 87 + | 2.7 | Examine the "Review & Override" button at the bottom of the alert card. | Button is visible but disabled (grayed out, opacity ~0.5, `cursor: not-allowed`). Tapping it does nothing. | 88 + | 2.8 | Tap the "Back" button (top-left, reads "< Back"). | Navigation returns to the home screen (`IdentityListHome`). The alert badge is still visible on the identity card. | 89 + 90 + ### Phase 3: Monitor Lifecycle (AC6.6) 91 + 92 + This phase verifies the 15-minute background timer and foreground check trigger. 93 + 94 + | Step | Action | Expected | 95 + |------|--------|----------| 96 + | 3.1 | Launch the app fresh. Open Xcode console or attach `os_log` to the Simulator process to observe `tracing` output from the Rust backend. | App starts normally. The PLC monitoring loop is spawned (look for the first tick being skipped -- the loop skips the initial immediate tick). | 97 + | 3.2 | Keep the app in the foreground for at least 15 minutes (set a timer). Observe the console logs. | After ~15 minutes, a monitoring cycle executes. Look for log output from `run_monitoring_cycle` or `check_for_changes` (e.g., `tracing::warn` messages if plc.directory is unreachable, or absence of warnings if all checks pass). | 98 + | 3.3 | If possible, inject a mock unauthorized operation (via staging plc.directory or debug modification) between monitoring cycles. | On the next 15-minute tick, the `plc_alert` Tauri event should fire. The home screen updates to show an alert badge without manual interaction. | 99 + | 3.4 | With the app on the home screen, press the Home button in the iOS Simulator to suspend the app. Wait a few seconds. | App is backgrounded. | 100 + | 3.5 | Return to the app by tapping its icon in the Simulator. | The `visibilitychange` event fires (`document.visibilityState === 'visible'`). `checkIdentityStatus()` is called (observable via network traffic to plc.directory in Xcode's Network inspector, or via console log of the `.catch` handler). | 101 + | 3.6 | Navigate away from the home screen (e.g., tap an identity card to view its DID document). Background the app and return. | `checkIdentityStatus()` should NOT fire, because the `handleVisibilityChange` function only calls it when `step === 'home'`. Verify no network call to plc.directory occurs. | 102 + 103 + ### End-to-End: Full Alert Lifecycle 104 + 105 + **Purpose:** Validates the complete flow from unauthorized operation appearing on plc.directory through to the user seeing and inspecting the alert in the app. 106 + 107 + | Step | Action | Expected | 108 + |------|--------|----------| 109 + | E2E.1 | Start with a clean state: a claimed identity with no alerts. Home screen shows the identity card with no red badge. | Baseline confirmed: no alerts. | 110 + | E2E.2 | Introduce an unauthorized PLC operation for the claimed identity (via staging plc.directory or mock). | Operation exists in the audit log at plc.directory. | 111 + | E2E.3 | Trigger a monitoring cycle: either wait 15 minutes, or suspend/resume the app. | The monitor fetches the audit log, detects the new entry is NOT signed by the device key, creates an `UnauthorizedChange`. | 112 + | E2E.4 | Observe the home screen. | A red "1 Alert" badge appears on the affected identity card. | 113 + | E2E.5 | Tap the alert badge. | Alert detail screen opens showing: signing key (or "Unknown key"), detection timestamp, recovery deadline (72h later), countdown timer with urgency coloring. | 114 + | E2E.6 | Press "Back" to return to the home screen. The badge persists. | Badge still shows "1 Alert". | 115 + | E2E.7 | Suspend and resume the app again. | `checkIdentityStatus()` fires again. Since the unauthorized operation is still in the audit log and now cached, no new alerts are generated (the diff shows no new entries beyond what is cached). The badge count remains at 1. | 116 + 117 + ### End-to-End: Network Resilience 118 + 119 + **Purpose:** Validates that the app handles plc.directory outages without false alarms or crashes. 120 + 121 + | Step | Action | Expected | 122 + |------|--------|----------| 123 + | NR.1 | With the app running and at least one identity claimed, disable network connectivity on the Simulator (toggle Airplane mode or use Network Link Conditioner to simulate no connectivity). | App is running but cannot reach plc.directory. | 124 + | NR.2 | Trigger a monitoring cycle (suspend/resume the app). | `checkIdentityStatus()` fires. The `fetch_audit_log` call fails. The monitor logs a warning (visible in Xcode console) and returns `Ok(vec![])`. | 125 + | NR.3 | Observe the home screen. | No new alert badges appear. Existing badges (if any) remain unchanged. No error dialog or crash. | 126 + | NR.4 | Restore network connectivity. Trigger another monitoring cycle. | The monitor successfully fetches the audit log and resumes normal operation. | 127 + 128 + ### Human Verification Required 129 + 130 + | Criterion | Why Manual | Steps | 131 + |-----------|------------|-------| 132 + | AC6.4: Alert badge on home screen | Visual rendering: red badge with `#fef2f2` background, `#ef4444` dot, `#991b1b` text; layout position relative to rotation key badge; conditional rendering based on `alertData`. Cannot be verified by headless tests. | Phase 1 steps 1.1-1.7 above. | 133 + | AC6.5: Alert detail screen content | Visual rendering: signing key display (truncated `did:key` or "Unknown key"), human-readable timestamp, live countdown timer that updates every 60 seconds, urgency color coding (green/amber/red/red-expired), disabled "Review & Override" button. Temporal behavior (timer updates) requires running app. | Phase 2 steps 2.1-2.8 above. | 134 + | AC6.6: Monitor lifecycle (timer + foreground) | 15-minute `tokio::time::interval` requires running Tauri runtime (not available in `cargo test`). `visibilitychange` event requires WKWebView context on iOS Simulator. | Phase 3 steps 3.1-3.6 above. | 135 + 136 + ### Traceability 137 + 138 + | Acceptance Criterion | Automated Test | Manual Step | 139 + |----------------------|----------------|-------------| 140 + | AC6.1: Authorized op, no alert | `test_ac6_1_authorized_change_detected`, `test_ac6_1_multi_identity_all_authorized` | -- | 141 + | AC6.2: Unauthorized op, creates alert | `test_ac6_2_unauthorized_change_detected`, `test_ac6_2_multi_identity_mixed_auth` | -- | 142 + | AC6.3: Recovery deadline = createdAt + 72h | `test_ac6_3_created_at_matches_audit_log` (backend), `deadline.test.ts` (frontend: getDeadline, getUrgency, formatCountdown) | Phase 2 step 2.4 (visual confirmation of deadline display) | 143 + | AC6.4: Red alert badge on home screen | `pnpm check` (type checking), backend `check_all` tests (data correctness) | Phase 1 steps 1.1-1.7 | 144 + | AC6.5: Alert detail screen content | `deadline.test.ts` (deadline computation), `pnpm check` (component prop types) | Phase 2 steps 2.1-2.8 | 145 + | AC6.6: Monitor lifecycle (15-min timer + foreground) | `run_monitoring_cycle` integration tests (cycle logic), `cargo check` (timer wiring), `pnpm check` (visibilitychange wiring) | Phase 3 steps 3.1-3.6 | 146 + | AC6.7: Graceful plc.directory unreachable | `test_ac6_7_network_error_graceful_handling` | End-to-End: Network Resilience (NR.1-NR.4) | 147 + | AC6.8: Empty audit log handling | `test_ac6_8_empty_audit_log` | -- |