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

fix(MM-144): address code review feedback

Critical:
- Add 7 unit tests for serde serialization contract (AC2.2, AC2.5, AC3.1-AC3.5)
- create_mobile_account_request_serializes_camel_case: verifies camelCase field names
- create_account_result_serializes_camel_case: verifies nextStep serialization
- error_*_serializes_correctly: 5 tests for all CreateAccountError variants

Important:
- Change CreateAccountResult and CreateAccountError from interface to type
(pure data shapes, not class contracts; CreateAccountParams correctly stays interface)
- Use LazyLock<RelayClient> static for RELAY_CLIENT to avoid allocating new
connection pool and TLS session per IPC invocation

Minor:
- Remove greet dead code: greet command, tests, and generate_handler! entry
- Document OnboardingStep deviation: per-screen error rewinding is better UX
than a dedicated error screen (no 'error' step needed)

Verification:
- cargo test --lib: 7/7 tests pass
- cargo build --workspace: success
- cargo clippy --workspace -- -D warnings: success
- cargo fmt --all --check: success
- pnpm build: success
- svelte-check: 0 errors, 0 warnings

authored by malpercio.dev and committed by

Tangled edeaa134 352e2b4e

+86 -23
+69 -16
apps/identity-wallet/src-tauri/src/lib.rs
··· 3 3 4 4 use crypto::generate_p256_keypair; 5 5 use serde::{Deserialize, Serialize}; 6 + use std::sync::LazyLock; 6 7 7 8 // ── Request / response types ──────────────────────────────────────────────── 8 9 ··· 68 69 Unknown { message: String }, 69 70 } 70 71 72 + // ── Static relay client ───────────────────────────────────────────────────── 73 + 74 + static RELAY_CLIENT: LazyLock<http::RelayClient> = LazyLock::new(http::RelayClient::new); 75 + 71 76 // ── IPC command ───────────────────────────────────────────────────────────── 72 77 73 78 #[tauri::command] ··· 98 103 claim_code, 99 104 }; 100 105 101 - let resp = http::RelayClient::new() 106 + let resp = RELAY_CLIENT 102 107 .post("/v1/accounts/mobile", &req) 103 108 .await 104 109 .map_err(|e| CreateAccountError::NetworkError { ··· 154 159 } 155 160 } 156 161 157 - #[tauri::command] 158 - fn greet(name: String) -> String { 159 - format!("Hello, {}!", name) 160 - } 161 - 162 162 #[cfg_attr(mobile, tauri::mobile_entry_point)] 163 163 pub fn run() { 164 164 tauri::Builder::default() 165 - .invoke_handler(tauri::generate_handler![greet, create_account]) 165 + .invoke_handler(tauri::generate_handler![create_account]) 166 166 .run(tauri::generate_context!()) 167 167 .expect("error while running tauri application"); 168 168 } ··· 171 171 mod tests { 172 172 use super::*; 173 173 174 + // -- AC2.2: CreateMobileAccountRequest serialization -- 175 + #[test] 176 + fn create_mobile_account_request_serializes_camel_case() { 177 + let req = CreateMobileAccountRequest { 178 + email: "test@example.com".into(), 179 + handle: "alice".into(), 180 + device_public_key: "pubkey123".into(), 181 + platform: "ios".into(), 182 + claim_code: "ABC123".into(), 183 + }; 184 + let json = serde_json::to_value(&req).unwrap(); 185 + assert_eq!(json["email"], "test@example.com"); 186 + assert_eq!(json["handle"], "alice"); 187 + assert_eq!(json["devicePublicKey"], "pubkey123"); 188 + assert_eq!(json["platform"], "ios"); 189 + assert_eq!(json["claimCode"], "ABC123"); 190 + } 191 + 192 + // -- AC2.5: CreateAccountResult serialization -- 193 + #[test] 194 + fn create_account_result_serializes_camel_case() { 195 + let result = CreateAccountResult { 196 + next_step: "did_creation".into(), 197 + }; 198 + let json = serde_json::to_value(&result).unwrap(); 199 + assert_eq!(json["nextStep"], "did_creation"); 200 + } 201 + 202 + // -- AC3.1: CreateAccountError::ExpiredCode serialization -- 174 203 #[test] 175 - fn greet_formats_name() { 176 - assert_eq!(greet("World".to_string()), "Hello, World!"); 204 + fn error_expired_code_serializes_correctly() { 205 + let err = CreateAccountError::ExpiredCode; 206 + let json = serde_json::to_value(&err).unwrap(); 207 + assert_eq!(json["code"], "EXPIRED_CODE"); 177 208 } 178 209 210 + // -- AC3.2: CreateAccountError::RedeemedCode serialization -- 179 211 #[test] 180 - fn greet_empty_name() { 181 - assert_eq!(greet(String::new()), "Hello, !"); 212 + fn error_redeemed_code_serializes_correctly() { 213 + let err = CreateAccountError::RedeemedCode; 214 + let json = serde_json::to_value(&err).unwrap(); 215 + assert_eq!(json["code"], "REDEEMED_CODE"); 182 216 } 183 217 218 + // -- AC3.3: CreateAccountError::EmailTaken serialization -- 184 219 #[test] 185 - fn greet_special_characters() { 186 - assert_eq!( 187 - greet("<script>alert(1)</script>".to_string()), 188 - "Hello, <script>alert(1)</script>!" 189 - ); 220 + fn error_email_taken_serializes_correctly() { 221 + let err = CreateAccountError::EmailTaken; 222 + let json = serde_json::to_value(&err).unwrap(); 223 + assert_eq!(json["code"], "EMAIL_TAKEN"); 224 + } 225 + 226 + // -- AC3.4: CreateAccountError::HandleTaken serialization -- 227 + #[test] 228 + fn error_handle_taken_serializes_correctly() { 229 + let err = CreateAccountError::HandleTaken; 230 + let json = serde_json::to_value(&err).unwrap(); 231 + assert_eq!(json["code"], "HANDLE_TAKEN"); 232 + } 233 + 234 + // -- AC3.5: CreateAccountError::NetworkError serialization -- 235 + #[test] 236 + fn error_network_error_serializes_correctly() { 237 + let err = CreateAccountError::NetworkError { 238 + message: "Connection timeout".into(), 239 + }; 240 + let json = serde_json::to_value(&err).unwrap(); 241 + assert_eq!(json["code"], "NETWORK_ERROR"); 242 + assert_eq!(json["message"], "Connection timeout"); 190 243 } 191 244 }
+9 -7
apps/identity-wallet/src/lib/ipc.ts
··· 1 1 import { invoke } from '@tauri-apps/api/core'; 2 2 3 - export const greet = (name: string): Promise<string> => 4 - invoke('greet', { name }); 5 - 6 3 // ── create_account ────────────────────────────────────────────────────────── 7 4 8 5 export interface CreateAccountParams extends Record<string, unknown> { ··· 11 8 handle: string; 12 9 } 13 10 14 - export interface CreateAccountResult { 11 + /** 12 + * Successful result from the `create_account` Rust command. 13 + * This is a pure data shape returned on success. 14 + */ 15 + export type CreateAccountResult = { 15 16 nextStep: string; 16 - } 17 + }; 17 18 18 19 /** 19 20 * Error returned by the `create_account` Rust command. 20 21 * 21 22 * Serialized as `{ code: "EXPIRED_CODE" }` etc. by the Rust backend. 22 23 * The `message` field is present only on NETWORK_ERROR and UNKNOWN variants. 24 + * This is a pure data shape used for error handling. 23 25 */ 24 - export interface CreateAccountError { 26 + export type CreateAccountError = { 25 27 code: 26 28 | 'EXPIRED_CODE' 27 29 | 'REDEEMED_CODE' ··· 30 32 | 'NETWORK_ERROR' 31 33 | 'UNKNOWN'; 32 34 message?: string; 33 - } 35 + }; 34 36 35 37 /** 36 38 * Create a new account via the relay.
+8
apps/identity-wallet/src/routes/+page.svelte
··· 7 7 import { createAccount, type CreateAccountError } from '$lib/ipc'; 8 8 9 9 // ── Onboarding step type ───────────────────────────────────────────────── 10 + // 11 + // Design plan originally specified an 'error' state for displaying errors, 12 + // but the implementation uses per-screen error rewinding instead. 13 + // When an error occurs (e.g., EXPIRED_CODE, EMAIL_TAKEN), the app rewinds 14 + // to the relevant screen and displays an inline error message below the 15 + // input field, rather than showing a separate error screen. This is a better 16 + // UX pattern — users can immediately correct the issue on the same screen 17 + // instead of navigating through an extra modal. No 'error' step is needed. 10 18 11 19 type OnboardingStep = 12 20 | 'welcome'