import Testing @testable import Kordant import SwiftUI // MARK: - Mocks final class MockKeychainService: KeychainServiceProtocol { private var storage: [String: Data] = [:] func store(key: String, value: Data) throws { storage[key] = value } func retrieve(key: String) throws -> Data? { storage[key] } func delete(key: String) throws { storage.removeValue(forKey: key) } func clearAll() throws { storage.removeAll() } } final class MockAuthAPIClient: AuthAPIClientProtocol { var shouldSucceed = true func login(email: String, password: String) async throws -> AuthTokenResponse { if shouldSucceed { return AuthTokenResponse( accessToken: "mock-token", refreshToken: "mock-refresh", user: User(id: "1", name: "Test", email: email) ) } throw APIError.notImplemented } func signup(name: String, email: String, password: String) async throws -> AuthTokenResponse { if shouldSucceed { return AuthTokenResponse( accessToken: "mock-token", refreshToken: "mock-refresh", user: User(id: "1", name: name, email: email) ) } throw APIError.notImplemented } func resetPassword(email: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } } // MARK: - Password Strength Tests struct PasswordStrengthTests { @Test("Short password with no criteria is weak") func shortPassword() { #expect(PasswordStrengthCalculator.strength(of: "abc") == .weak) } @Test("Password meeting one criterion is weak") func oneCriterion() { #expect(PasswordStrengthCalculator.strength(of: "abcdefgh") == .weak) } @Test("Password with length and uppercase is fair") func lengthAndUppercase() { #expect(PasswordStrengthCalculator.strength(of: "Abcdefgh") == .fair) } @Test("Password with length, uppercase, and digit is good") func threeCriteria() { #expect(PasswordStrengthCalculator.strength(of: "Abcdefg1") == .good) } @Test("Password meeting all criteria is strong") func allCriteria() { #expect(PasswordStrengthCalculator.strength(of: "Abcdefg1!") == .strong) } @Test("Empty password is weak") func emptyPassword() { #expect(PasswordStrengthCalculator.strength(of: "") == .weak) } } // MARK: - Keychain Service Tests struct KeychainServiceTests { @Test("MockKeychainService stores and retrieves data") func storeAndRetrieve() throws { let keychain = MockKeychainService() try keychain.store(key: "test", value: Data("hello".utf8)) let result = try keychain.retrieve(key: "test") #expect(result == Data("hello".utf8)) } @Test("MockKeychainService returns nil for missing key") func missingKey() throws { let keychain = MockKeychainService() let result = try keychain.retrieve(key: "nonexistent") #expect(result == nil) } @Test("MockKeychainService overwrites existing value") func overwriteValue() throws { let keychain = MockKeychainService() try keychain.store(key: "key", value: Data("first".utf8)) try keychain.store(key: "key", value: Data("second".utf8)) let result = try keychain.retrieve(key: "key") #expect(result == Data("second".utf8)) } @Test("MockKeychainService deletes value") func deleteValue() throws { let keychain = MockKeychainService() try keychain.store(key: "key", value: Data("value".utf8)) try keychain.delete(key: "key") let result = try keychain.retrieve(key: "key") #expect(result == nil) } @Test("MockKeychainService clears all values") func clearAll() throws { let keychain = MockKeychainService() try keychain.store(key: "a", value: Data("1".utf8)) try keychain.store(key: "b", value: Data("2".utf8)) try keychain.clearAll() #expect(try keychain.retrieve(key: "a") == nil) #expect(try keychain.retrieve(key: "b") == nil) } } // MARK: - Auth Service Tests struct AuthServiceTests { @MainActor private func makeService(apiClient: MockAuthAPIClient = MockAuthAPIClient()) -> AuthService { AuthService( keychain: MockKeychainService(), apiClient: apiClient ) } @Test("AuthService starts unauthenticated") @MainActor func initialState() { let service = makeService() #expect(service.state == .unauthenticated) #expect(service.currentUser == nil) #expect(!service.isBiometricEnabled) #expect(!service.hasCompletedOnboarding) } @Test("AuthService login succeeds and updates state") @MainActor func loginSuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.login(email: "test@example.com", password: "password123") #expect(service.state == .authenticated) #expect(service.currentUser?.email == "test@example.com") #expect(service.currentUser?.name == "Test") } @Test("AuthService login failure sets error") @MainActor func loginFailure() async { let client = MockAuthAPIClient() client.shouldSucceed = false let service = makeService(apiClient: client) await service.login(email: "test@example.com", password: "wrong") #expect(service.state == .unauthenticated) #expect(service.error != nil) } @Test("AuthService signup succeeds and updates state") @MainActor func signupSuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.signup(name: "Test User", email: "test@example.com", password: "password123") #expect(service.state == .authenticated) #expect(service.currentUser?.name == "Test User") #expect(service.currentUser?.email == "test@example.com") } @Test("AuthService logout clears all state") @MainActor func logout() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.login(email: "test@example.com", password: "password123") service.completeOnboarding() service.enableBiometric() service.logout() #expect(service.state == .unauthenticated) #expect(service.currentUser == nil) #expect(!service.isBiometricEnabled) #expect(!service.hasCompletedOnboarding) } @Test("AuthService shows biometric prompt after first login") @MainActor func biometricPromptAfterLogin() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.login(email: "test@example.com", password: "password123") #expect(service.showBiometricPrompt) } @Test("AuthService does not show biometric prompt if already enabled") @MainActor func noBiometricPromptIfEnabled() async { let keychain = MockKeychainService() try? keychain.store(key: "useBiometric", value: Data("token".utf8)) let client = MockAuthAPIClient() client.shouldSucceed = true let service = AuthService(keychain: keychain, apiClient: client) await service.login(email: "test@example.com", password: "password123") #expect(!service.showBiometricPrompt) } @Test("AuthService enabling biometric sets flag and hides prompt") @MainActor func enableBiometric() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.login(email: "test@example.com", password: "password123") service.enableBiometric() #expect(service.isBiometricEnabled) #expect(!service.showBiometricPrompt) } @Test("AuthService onboarding state persists") @MainActor func onboardingCompletion() { let service = makeService() #expect(!service.hasCompletedOnboarding) service.completeOnboarding() #expect(service.hasCompletedOnboarding) } @Test("AuthService resetPassword succeeds without error") @MainActor func resetPasswordSuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = makeService(apiClient: client) await service.resetPassword(email: "test@example.com") #expect(service.error == nil) } @Test("AuthService resetPassword failure sets error") @MainActor func resetPasswordFailure() async { let client = MockAuthAPIClient() client.shouldSucceed = false let service = makeService(apiClient: client) await service.resetPassword(email: "test@example.com") #expect(service.error != nil) } } // MARK: - ThemeManager Tests struct ThemeManagerTests { @MainActor private func makeManager() -> ThemeManager { ThemeManager(defaults: UserDefaults(suiteName: UUID().uuidString)!) } @Test("ThemeManager starts with system mode by default") @MainActor func defaultThemeMode() { let manager = makeManager() #expect(manager.colorScheme == nil) } @Test("ThemeManager switches to light mode") @MainActor func setLightMode() { let manager = makeManager() manager.setLight() #expect(manager.colorScheme == .light) } @Test("ThemeManager switches to dark mode") @MainActor func setDarkMode() { let manager = makeManager() manager.setDark() #expect(manager.colorScheme == .dark) } @Test("ThemeManager switches back to system mode") @MainActor func setSystemMode() { let manager = makeManager() manager.setLight() #expect(manager.colorScheme == .light) manager.setSystem() #expect(manager.colorScheme == nil) } } // MARK: - Color Tests struct ColorTests { @Test("Brand primary color is accessible") func brandPrimaryColor() { #expect(Color.brandPrimary != .clear) } @Test("Brand accent color is accessible") func brandAccentColor() { #expect(Color.brandAccent != .clear) } @Test("Semantic colors are accessible") func semanticColors() { #expect(Color.success != .clear) #expect(Color.warning != .clear) #expect(Color.error != .clear) } @Test("Adaptive background colors are accessible") func adaptiveColors() { #expect(Color.bgPrimary != .clear) #expect(Color.bgSecondary != .clear) #expect(Color.bgTertiary != .clear) } @Test("Text colors are accessible") func textColors() { #expect(Color.textPrimary != .clear) #expect(Color.textSecondary != .clear) #expect(Color.textTertiary != .clear) } @Test("Border color is accessible") func borderColor() { #expect(Color.border != .clear) } } // MARK: - Route Tests struct RouteTests { @Test("Route initializes from valid dashboard deep link") func dashboardDeepLink() { guard let url = URL(string: "kordant://dashboard") else { Issue.record("Could not create URL") return } let route = Route(deepLink: url) #expect(route == .dashboard) } @Test("Route initializes from valid alerts deep link") func alertsDeepLink() { guard let url = URL(string: "kordant://alerts") else { Issue.record("Could not create URL") return } let route = Route(deepLink: url) #expect(route == .alerts) } @Test("Route initializes from alert detail deep link") func alertDetailDeepLink() { guard let url = URL(string: "kordant://alerts/abc123") else { Issue.record("Could not create URL") return } let route = Route(deepLink: url) #expect(route == .alertDetail(id: "abc123")) } @Test("Route returns nil for invalid scheme") func invalidScheme() { guard let url = URL(string: "https://example.com") else { Issue.record("Could not create URL") return } let route = Route(deepLink: url) #expect(route == nil) } @Test("Route returns nil for unknown host") func unknownHost() { guard let url = URL(string: "kordant://unknown") else { Issue.record("Could not create URL") return } let route = Route(deepLink: url) #expect(route == nil) } } // MARK: - AppRouter Tests struct AppRouterTests { @Test("AppRouter starts with empty path") @MainActor func emptyPath() { let router = AppRouter() #expect(router.path.isEmpty) } @Test("AppRouter navigates to a route") @MainActor func navigateToRoute() { let router = AppRouter() router.navigate(to: .dashboard) #expect(!router.path.isEmpty) } @Test("AppRouter pops to root clears path") @MainActor func popToRoot() { let router = AppRouter() router.navigate(to: .dashboard) router.navigate(to: .settings) router.popToRoot() #expect(router.path.isEmpty) } @Test("AppRouter pop removes last route") @MainActor func popRemovesLast() { let router = AppRouter() router.navigate(to: .dashboard) router.navigate(to: .settings) router.pop() #expect(router.path.count == 1) } } // MARK: - Spacing Tests struct SpacingTests { @Test("Spacing values match spec") func spacingValues() { #expect(Spacing.xs == 4) #expect(Spacing.sm == 8) #expect(Spacing.md == 16) #expect(Spacing.lg == 24) #expect(Spacing.xl == 32) #expect(Spacing.xxl == 48) } } // MARK: - Component Tests struct ShieldButtonTests { @Test("ShieldButtonStyle has all cases") func buttonStyles() { let styles: [ShieldButtonStyle] = [.primary, .secondary, .ghost, .danger] #expect(styles.count == 4) } @Test("ShieldButtonSize has all cases") func buttonSizes() { let sizes: [ShieldButtonSize] = [.small, .medium, .large] #expect(sizes.count == 3) } @Test("ShieldButton action fires on tap") @MainActor func buttonAction() { var fired = false let button = ShieldButton(title: "Test", action: { fired = true }) button.action() #expect(fired) } } struct ShieldBadgeTests { @Test("ShieldBadgeVariant has all cases") func badgeVariants() { let variants: [ShieldBadgeVariant] = [.default, .success, .warning, .error, .info] #expect(variants.count == 5) } } struct ShieldTextFieldTests { @Test("ShieldTextField renders with label") @MainActor func textFieldLabel() { let text = "test" _ = ShieldTextField(label: "Username", text: .constant(text)) #expect(text == "test") } @Test("ShieldTextField accepts error message") @MainActor func textFieldError() { let field = ShieldTextField( label: "Email", text: .constant("bad"), errorMessage: "Invalid email" ) #expect(field.errorMessage == "Invalid email") } } struct ToastTests { @Test("ToastData creates with defaults") func toastDataDefaults() { let toast = ToastData(message: "Hello") #expect(toast.message == "Hello") #expect(toast.variant == .info) #expect(toast.duration == 3.5) } @Test("ToastData creates with custom values") func toastDataCustom() { let toast = ToastData(message: "Error!", variant: .error, duration: 5.0) #expect(toast.message == "Error!") #expect(toast.variant == .error) #expect(toast.duration == 5.0) } @Test("ToastData instances are equatable by id") func toastDataEquality() { let toast1 = ToastData(message: "Test") let toast2 = ToastData(message: "Test") #expect(toast1 != toast2) } @MainActor @Test("ToastManager shows and dismisses toast") func toastManagerShowDismiss() { let manager = ToastManager.shared let initialCount = manager.toasts.count manager.showToast(message: "Test toast") #expect(manager.toasts.count == initialCount + 1) if let toast = manager.toasts.last { manager.dismiss(toast) #expect(manager.toasts.count == initialCount) } } } struct ShieldAvatarTests { @Test("ShieldAvatarSize has all cases") func avatarSizes() { let sizes: [ShieldAvatarSize] = [.small, .medium, .large] #expect(sizes.count == 3) #expect(ShieldAvatarSize.small.dimension == 32) #expect(ShieldAvatarSize.medium.dimension == 44) #expect(ShieldAvatarSize.large.dimension == 64) } @Test("AvatarStatus has all cases") func avatarStatus() { let statuses: [AvatarStatus] = [.online, .away, .offline] #expect(statuses.count == 3) } } struct ShieldProgressBarTests { @Test("ShieldProgressBar clamps progress between 0 and 1") func progressClamping() { let over = ShieldProgressBar(progress: 1.5) let under = ShieldProgressBar(progress: -0.5) #expect(over.progress <= 1.0) #expect(under.progress >= 0.0) } } struct ToastVariantTests { @Test("ToastVariant has all cases") func toastVariants() { let variants: [ToastVariant] = [.success, .error, .warning, .info] #expect(variants.count == 4) } } // MARK: - Plan Tests struct PlanTests { @Test("Plan has all expected cases") func planCases() { let plans = Plan.allCases #expect(plans.count == 3) #expect(plans.contains(.basic)) #expect(plans.contains(.plus)) #expect(plans.contains(.premium)) } @Test("Plus plan is recommended") func recommendedPlan() { #expect(Plan.basic.isRecommended == false) #expect(Plan.plus.isRecommended == true) #expect(Plan.premium.isRecommended == false) } @Test("Each plan has pricing") func planPricing() { #expect(Plan.basic.monthlyPrice == "Free") #expect(Plan.plus.monthlyPrice == "$12/mo") #expect(Plan.premium.monthlyPrice == "$29/mo") } @Test("Premium plan has most features") func planFeatures() { #expect(Plan.basic.features.count < Plan.plus.features.count) #expect(Plan.plus.features.count < Plan.premium.features.count) } } // MARK: - PasswordStrength Tests struct PasswordStrengthColorTests { @Test("PasswordStrength has correct label for each level") func strengthLabels() { #expect(PasswordStrength.weak.label == "Weak") #expect(PasswordStrength.fair.label == "Fair") #expect(PasswordStrength.good.label == "Good") #expect(PasswordStrength.strong.label == "Strong") } @Test("PasswordStrength levels are comparable") func strengthComparison() { #expect(PasswordStrength.weak < PasswordStrength.fair) #expect(PasswordStrength.fair < PasswordStrength.good) #expect(PasswordStrength.good < PasswordStrength.strong) } } // MARK: - Mock URL Protocol final class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let handler = Self.requestHandler else { fatalError("MockURLProtocol.requestHandler not set") } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } // MARK: - APIClient Tests struct APIClientTests { private func makeSession() -> URLSession { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] return URLSession(configuration: config) } @Test("APIClient injects auth header correctly") func authHeaderInjection() async throws { let session = makeSession() let client = APIClient(session: session) client.authToken = "test-jwt-token" MockURLProtocol.requestHandler = { request in let authHeader = request.value(forHTTPHeaderField: "Authorization") #expect(authHeader == "Bearer test-jwt-token") let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let data = try JSONEncoder().encode(User(id: "1", name: "Test", email: "test@example.com")) return (response, data) } let _: User = try await client.request("/api/trpc/user.me", method: "GET") } @Test("APIClient retries on server error") func retryOnServerError() async throws { let session = makeSession() let config = APIConfig(maxRetries: 2) let client = APIClient(config: config, session: session) client.authToken = "test-token" var attemptCount = 0 MockURLProtocol.requestHandler = { request in attemptCount += 1 if attemptCount < 2 { let response = HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: nil, headerFields: nil)! return (response, Data()) } let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let data = try JSONEncoder().encode(["message": "ok"]) return (response, data) } let result: [String: String] = try await client.request("/test") #expect(result["message"] == "ok") #expect(attemptCount == 2) } @Test("APIClient throws unauthorized on 401") func unauthorizedError() async throws { let session = makeSession() let client = APIClient(session: session) MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse(url: request.url!, statusCode: 401, httpVersion: nil, headerFields: nil)! return (response, Data()) } await #expect(throws: APIError.unauthorized) { let _: String = try await client.request("/test") } } @Test("APIClient sets content type headers") func contentTypeHeaders() async throws { let session = makeSession() let client = APIClient(session: session) MockURLProtocol.requestHandler = { request in #expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json") #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! return (response, Data("\"ok\"".utf8)) } let _: String = try await client.request("/test") } } // MARK: - TRPCBridge Tests struct TRPCBridgeTests { @Test("TRPCBridge rawRequest returns correct data") func rawRequestReturnsData() async throws { let session = makeSession() let client = APIClient(session: session) let expectedJSON = "{\"0\":{\"result\":{\"data\":{\"id\":\"1\",\"name\":\"Test\",\"email\":\"test@example.com\"}}}}" MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let data = Data(expectedJSON.utf8) return (response, data) } let data = try await client.rawRequest("/api/trpc/user.me", method: "POST") let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] #expect(json != nil) let firstVal = json?["0"] as? [String: Any] #expect(firstVal != nil) let result = firstVal?["result"] as? [String: Any] #expect(result != nil) let dataVal = result?["data"] as? [String: Any] #expect(dataVal != nil) #expect(dataVal?["id"] as? String == "1") } @Test("TRPCBridge callProcedure sends correct path") func correctPath() async throws { let session = makeSession() let client = APIClient(session: session) let bridge = TRPCBridge(client: client) MockURLProtocol.requestHandler = { request in #expect(request.url?.absoluteString.contains("/api/trpc/user.me") == true) #expect(request.httpMethod == "POST") let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let body: [String: Any] = ["0": ["result": ["data": ["id": "1", "name": "Test", "email": "test@example.com"]]]] let data = try JSONSerialization.data(withJSONObject: body) return (response, data) } let user: User = try await bridge.callProcedure(path: "user.me") #expect(user.id == "1") #expect(user.name == "Test") #expect(user.email == "test@example.com") } @Test("TRPCBridge handles tRPC error format") func trpcError() async throws { let session = makeSession() let client = APIClient(session: session) let bridge = TRPCBridge(client: client) MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let body: [String: Any] = ["error": ["message": "User not found", "code": 404]] let data = try JSONSerialization.data(withJSONObject: body) return (response, data) } await #expect(throws: APIError.tRPCError(code: 404, message: "User not found")) { let _: User = try await bridge.callProcedure(path: "user.me") } } @Test("TRPCBridge userMe convenience method") func userMeConvenience() async throws { let session = makeSession() let client = APIClient(session: session) client.authToken = "test-token" let bridge = TRPCBridge(client: client) MockURLProtocol.requestHandler = { request in #expect(request.url?.absoluteString.hasSuffix("/api/trpc/user.me") == true) let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let body: [String: Any] = ["0": ["result": ["data": ["id": "1", "name": "Test", "email": "test@example.com"]]]] let data = try JSONSerialization.data(withJSONObject: body) return (response, data) } let user = try await bridge.userMe() #expect(user.id == "1") } private func makeSession() -> URLSession { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] return URLSession(configuration: config) } } // MARK: - CacheManager Tests struct CacheManagerTests { @Test("CacheManager stores and retrieves values") func storeAndRetrieve() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let cache = CacheManager(defaults: defaults) cache.setCached(key: "user", value: User(id: "1", name: "Test", email: "test@example.com"), ttl: 60) let cached: User? = cache.getCached(key: "user") #expect(cached?.id == "1") #expect(cached?.name == "Test") } @Test("CacheManager returns nil for expired TTL") func expiredTTL() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let cache = CacheManager(defaults: defaults) cache.setCached(key: "item", value: "test-value", ttl: -1) let cached: String? = cache.getCached(key: "item") #expect(cached == nil) } @Test("CacheManager returns nil for missing key") func missingKey() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let cache = CacheManager(defaults: defaults) let cached: String? = cache.getCached(key: "nonexistent") #expect(cached == nil) } @Test("CacheManager clearAll removes all entries") func clearAll() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let cache = CacheManager(defaults: defaults) cache.setCached(key: "a", value: "1", ttl: 60) cache.setCached(key: "b", value: "2", ttl: 60) cache.clearAll() let a: String? = cache.getCached(key: "a") let b: String? = cache.getCached(key: "b") #expect(a == nil) #expect(b == nil) } @Test("CacheManager remove clears single key") func removeKey() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let cache = CacheManager(defaults: defaults) cache.setCached(key: "keep", value: "value", ttl: 60) cache.setCached(key: "remove", value: "value", ttl: 60) cache.remove(key: "remove") let kept: String? = cache.getCached(key: "keep") let removed: String? = cache.getCached(key: "remove") #expect(kept == "value") #expect(removed == nil) } } // MARK: - OfflineQueue Tests struct OfflineQueueTests { @Test("OfflineQueue persists and loads queued requests") func persistAndLoad() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let client = MockAPIClient() let queue = OfflineQueue(client: client, defaults: defaults) let request = QueuedRequest(endpoint: "/test", method: "POST", body: Data("\"hello\"".utf8)) queue.addToQueue(request) #expect(queue.pendingCount() == 1) let queue2 = OfflineQueue(client: client, defaults: defaults) #expect(queue2.pendingCount() == 1) } @Test("OfflineQueue processes queued requests successfully") func processQueueSuccess() async { let defaults = UserDefaults(suiteName: UUID().uuidString)! let client = MockAPIClient() client.shouldSucceed = true let queue = OfflineQueue(client: client, defaults: defaults) let request = QueuedRequest(endpoint: "/test", method: "POST") queue.addToQueue(request) await queue.processQueue() #expect(queue.pendingCount() == 0) } @Test("OfflineQueue retries failed requests up to maxRetries") func processQueueRetries() async { let defaults = UserDefaults(suiteName: UUID().uuidString)! let client = MockAPIClient() client.shouldSucceed = false let queue = OfflineQueue(client: client, defaults: defaults) let request = QueuedRequest(endpoint: "/test", method: "POST") queue.addToQueue(request) await queue.processQueue() #expect(queue.pendingCount() == 1) var loaded = loadFromDefaults(defaults) #expect(loaded?.first?.retryCount == 1) } @Test("OfflineQueue marks failed after max retries") func maxRetriesExhausted() async { let defaults = UserDefaults(suiteName: UUID().uuidString)! let client = MockAPIClient() client.shouldSucceed = false let queue = OfflineQueue(client: client, defaults: defaults) var request = QueuedRequest(endpoint: "/test", method: "POST") request.retryCount = 3 queue.addToQueue(request) await queue.processQueue() #expect(queue.pendingCount() == 0) } @Test("OfflineQueue clearQueue removes all") func clearQueue() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let client = MockAPIClient() let queue = OfflineQueue(client: client, defaults: defaults) queue.addToQueue(QueuedRequest(endpoint: "/test", method: "POST")) queue.addToQueue(QueuedRequest(endpoint: "/test2", method: "GET")) queue.clearQueue() #expect(queue.pendingCount() == 0) } private func loadFromDefaults(_ defaults: UserDefaults) -> [QueuedRequest]? { guard let data = defaults.data(forKey: "kordant.offlineQueue") else { return nil } return try? JSONDecoder().decode([QueuedRequest].self, from: data) } } final class MockAPIClient: APIClientProtocol { var shouldSucceed = true var authToken: String? let config: APIConfig = .shared func request(_ endpoint: String, method: String, body: Data?) async throws -> T { let data = try await rawRequest(endpoint, method: method, body: body) return try JSONDecoder().decode(T.self, from: data) } func rawRequest(_ endpoint: String, method: String, body: Data?) async throws -> Data { if shouldSucceed { return try JSONSerialization.data(withJSONObject: ["message": "ok"]) } throw APIError.serverError(statusCode: 500) } } // MARK: - NetworkMonitor Tests struct NetworkMonitorTests { @Test("NetworkMonitor starts connected by default") func initialState() { let monitor = NetworkMonitor() #expect(monitor.isConnected == true) monitor.stopMonitoring() } } // MARK: - Model Tests struct ModelTests { @Test("User conforms to Codable") func userCodable() throws { let user = User(id: "1", name: "Test", email: "t@t.com") let data = try JSONEncoder().encode(user) let decoded = try JSONDecoder().decode(User.self, from: data) #expect(decoded == user) } @Test("User conforms to Identifiable") func userIdentifiable() { let user = User(id: "42", name: "A", email: "a@b.com") #expect(user.id == "42") } @Test("SubscriptionTier enum has all cases") func subscriptionTierCases() { let tiers = SubscriptionTier.allCases #expect(tiers.count == 4) #expect(tiers.contains(.free)) #expect(tiers.contains(.basic)) #expect(tiers.contains(.premium)) #expect(tiers.contains(.enterprise)) } @Test("Alert isCritical computed property") func alertIsCritical() { let critical = Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil) let low = Alert(id: "2", userId: "1", type: .exposure, severity: .low, title: "?", message: "?", read: false, createdAt: nil) #expect(critical.isCritical == true) #expect(low.isCritical == false) } @Test("AlertSeverity raw values") func alertSeverityValues() { #expect(AlertSeverity.low.rawValue == "low") #expect(AlertSeverity.critical.rawValue == "critical") } @Test("Subscription tier raw values") func subscriptionTierRawValues() { #expect(SubscriptionTier.free.rawValue == "free") #expect(SubscriptionTier.enterprise.rawValue == "enterprise") } @Test("Exposure source enum cases") func exposureSourceCases() { let sources: [ExposureSource] = [.darkWeb, .dataBreach, .socialMedia, .publicRecord, .brokerSite] #expect(sources.count == 5) } @Test("WatchlistItem has CodingKeys") func watchlistItemCodingKeys() { let item = WatchlistItem(id: "1", userId: "1", term: "test", type: .email, status: "active", createdAt: nil) #expect(item.term == "test") #expect(item.type == .email) } } // MARK: - Mock TRPCalling final class MockTRPCalling: TRPCalling { var shouldSucceed = true var stubbedAlerts: [Kordant.Alert] = [] var stubbedExposures: [Exposure] = [] var stubbedWatchlist: [WatchlistItem] = [] var stubbedEnrollments: [VoiceEnrollment] = [] var stubbedAnalyses: [VoiceAnalysis] = [] var stubbedRules: [SpamRule] = [] var stubbedProperties: [PropertyWatchlistItem] = [] var stubbedRemovalRequests: [RemovalRequest] = [] var stubbedBrokerListings: [BrokerListing] = [] var stubbedCorrelationGroups: [CorrelationGroup] = [] var stubbedUser = User(id: "1", name: "Test User", email: "test@kordant.ai") var stubbedSubscription = Subscription(id: "1", userId: "1", tier: .premium, status: "active", currentPeriodStart: nil, currentPeriodEnd: nil, stripeCustomerId: nil, stripeSubscriptionId: nil) var addedWatchlistItem: WatchlistItem? var addedProperty: PropertyWatchlistItem? var createdRule: SpamRule? var startedRemoval: RemovalRequest? func callProcedure(path: String, input: (any Encodable)?) async throws -> T { throw APIError.notImplemented } func userMe() async throws -> User { if shouldSucceed { return stubbedUser } throw APIError.notImplemented } func getSubscription() async throws -> Subscription { if shouldSucceed { return stubbedSubscription } throw APIError.notImplemented } func getWatchlist() async throws -> [WatchlistItem] { if shouldSucceed { return stubbedWatchlist } throw APIError.notImplemented } func getExposures() async throws -> [Exposure] { if shouldSucceed { return stubbedExposures } throw APIError.notImplemented } func getAlerts() async throws -> [Kordant.Alert] { if shouldSucceed { return stubbedAlerts } throw APIError.notImplemented } func getVoiceEnrollments() async throws -> [VoiceEnrollment] { if shouldSucceed { return stubbedEnrollments } throw APIError.notImplemented } func getVoiceAnalyses() async throws -> [VoiceAnalysis] { if shouldSucceed { return stubbedAnalyses } throw APIError.notImplemented } func getSpamRules() async throws -> [SpamRule] { if shouldSucceed { return stubbedRules } throw APIError.notImplemented } func getPropertyWatchlist() async throws -> [PropertyWatchlistItem] { if shouldSucceed { return stubbedProperties } throw APIError.notImplemented } func getRemovalRequests() async throws -> [RemovalRequest] { if shouldSucceed { return stubbedRemovalRequests } throw APIError.notImplemented } func getBrokerListings() async throws -> [BrokerListing] { if shouldSucceed { return stubbedBrokerListings } throw APIError.notImplemented } func getNormalizedAlerts() async throws -> [NormalizedAlert] { if shouldSucceed { return [] } throw APIError.notImplemented } func getCorrelationGroups() async throws -> [CorrelationGroup] { if shouldSucceed { return stubbedCorrelationGroups } throw APIError.notImplemented } func getSecurityReports() async throws -> [SecurityReport] { if shouldSucceed { return [] } throw APIError.notImplemented } func addWatchlistItem(term: String, type: WatchlistItemType) async throws -> WatchlistItem { if shouldSucceed { let item = WatchlistItem(id: "new-1", userId: "1", term: term, type: type, status: "active", createdAt: nil) addedWatchlistItem = item return item } throw APIError.notImplemented } func deleteWatchlistItem(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func scanForExposures() async throws -> [Exposure] { if shouldSucceed { return stubbedExposures } throw APIError.notImplemented } func deleteVoiceEnrollment(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func createSpamRule(pattern: String, action: SpamRuleAction, priority: Int, enabled: Bool) async throws -> SpamRule { if shouldSucceed { let rule = SpamRule(id: "rule-1", userId: "1", pattern: pattern, action: action, priority: priority, enabled: enabled, createdAt: nil) createdRule = rule return rule } throw APIError.notImplemented } func updateSpamRule(id: String, enabled: Bool) async throws -> SpamRule { if shouldSucceed { return SpamRule(id: id, userId: "1", pattern: "test", action: .block, priority: 1, enabled: enabled, createdAt: nil) } throw APIError.notImplemented } func deleteSpamRule(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func addProperty(address: String, city: String, state: String, zipCode: String) async throws -> PropertyWatchlistItem { if shouldSucceed { let property = PropertyWatchlistItem(id: "prop-1", userId: "1", propertyType: "residential", address: address, city: city, state: state, zipCode: zipCode, status: "active", createdAt: nil) addedProperty = property return property } throw APIError.notImplemented } func deleteProperty(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func startRemoval(exposureId: String, notes: String?) async throws -> RemovalRequest { if shouldSucceed { let request = RemovalRequest(id: "rem-1", userId: "1", exposureId: exposureId, status: .pending, requestedAt: nil, completedAt: nil, notes: notes) startedRemoval = request return request } throw APIError.notImplemented } func checkPhoneNumber(_ number: String) async throws -> SpamCheckResult { if shouldSucceed { return SpamCheckResult(phone: number, isSpam: true, confidence: 0.85, category: "telemarketer", reportCount: 42) } throw APIError.notImplemented } func resolveAlert(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func reportFalsePositive(id: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func updateNotificationPreferences(enabled: Bool) async throws { if !shouldSucceed { throw APIError.notImplemented } } func updateProfile(name: String, email: String) async throws -> User { if shouldSucceed { stubbedUser = User(id: "1", name: name, email: email) return stubbedUser } throw APIError.notImplemented } func registerDevice(token: String) async throws { if !shouldSucceed { throw APIError.notImplemented } } func createVoiceEnrollment(audioData: Data) async throws -> VoiceEnrollment { if shouldSucceed { return VoiceEnrollment(id: "new-enrollment", userId: "1", voiceSampleCount: 1, status: .pending, createdAt: Date()) } throw APIError.notImplemented } } // MARK: - ViewModel Tests @MainActor struct DashboardViewModelTests { @Test("DashboardViewModel loads alerts, exposures, and watchlist") func loadDashboard() async { let mock = MockTRPCalling() mock.stubbedAlerts = [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "Critical", message: "!", read: false, createdAt: Date()), Alert(id: "2", userId: "1", type: .exposure, severity: .low, title: "Low", message: "?", read: true, createdAt: Date().addingTimeInterval(-3600)) ] mock.stubbedExposures = [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: "a@b.com", severity: "high", discoveredAt: nil, status: .new), Exposure(id: "2", userId: "1", source: .dataBreach, dataType: "password", exposedData: nil, severity: "medium", discoveredAt: nil, status: .remediated) ] mock.stubbedWatchlist = [ WatchlistItem(id: "1", userId: "1", term: "test@email.com", type: .email, status: "active", createdAt: nil) ] let vm = DashboardViewModel(api: mock) await vm.loadDashboard() #expect(vm.alerts.count == 2) #expect(vm.exposures.count == 2) #expect(vm.watchlistItems.count == 1) #expect(vm.isLoading == false) #expect(vm.error == nil) #expect(vm.unresolvedExposures == 1) #expect(vm.activeWatchlistCount == 1) } @Test("DashboardViewModel handles errors gracefully") func loadDashboardError() async { let mock = MockTRPCalling() mock.shouldSucceed = false let vm = DashboardViewModel(api: mock) await vm.loadDashboard() #expect(vm.alerts.isEmpty) #expect(vm.error != nil) #expect(vm.isLoading == false) } @Test("DashboardViewModel threatScore with no issues") func threatScoreNoIssues() async { let mock = MockTRPCalling() mock.stubbedAlerts = [] mock.stubbedExposures = [] let vm = DashboardViewModel(api: mock) await vm.loadDashboard() #expect(vm.threatScore == 0) } @Test("DashboardViewModel threatScore with critical alerts") func threatScoreCritical() async { let mock = MockTRPCalling() mock.stubbedAlerts = [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil) ] mock.stubbedExposures = [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: nil, severity: "high", discoveredAt: nil, status: .new) ] let vm = DashboardViewModel(api: mock) await vm.loadDashboard() #expect(vm.threatScore > 0) } @Test("DashboardViewModel recentAlerts returns top 5") func recentAlertsLimit() async { let mock = MockTRPCalling() mock.stubbedAlerts = (1...10).map { i in Alert(id: "\(i)", userId: "1", type: .exposure, severity: .low, title: "Alert \(i)", message: "", read: false, createdAt: Date().addingTimeInterval(-Double(i) * 60)) } let vm = DashboardViewModel(api: mock) await vm.loadDashboard() #expect(vm.recentAlerts.count == 5) } } @MainActor struct DarkWatchViewModelTests { @Test("DarkWatchViewModel loads watchlist and exposures") func loadData() async { let mock = MockTRPCalling() mock.stubbedWatchlist = [ WatchlistItem(id: "1", userId: "1", term: "test@email.com", type: .email, status: "active", createdAt: nil) ] mock.stubbedExposures = [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "ssn", exposedData: nil, severity: "critical", discoveredAt: nil, status: .new) ] let vm = DarkWatchViewModel(api: mock) await vm.loadData() #expect(vm.watchlistItems.count == 1) #expect(vm.exposures.count == 1) #expect(vm.isLoading == false) } @Test("DarkWatchViewModel adds watchlist item") func addWatchlistItem() async { let mock = MockTRPCalling() let vm = DarkWatchViewModel(api: mock) await vm.addWatchlistItem(term: "john@test.com", type: .email) #expect(vm.watchlistItems.count == 1) #expect(vm.watchlistItems[0].term == "john@test.com") #expect(vm.watchlistItems[0].type == .email) } @Test("DarkWatchViewModel deletes watchlist item") func deleteWatchlistItem() async { let mock = MockTRPCalling() mock.stubbedWatchlist = [ WatchlistItem(id: "1", userId: "1", term: "test", type: .email, status: "active", createdAt: nil) ] let vm = DarkWatchViewModel(api: mock) await vm.loadData() #expect(vm.watchlistItems.count == 1) await vm.deleteWatchlistItem(id: "1") #expect(vm.watchlistItems.isEmpty) } @Test("DarkWatchViewModel scan for exposures") func scanExposures() async { let mock = MockTRPCalling() mock.stubbedExposures = [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: nil, severity: "high", discoveredAt: nil, status: .new) ] let vm = DarkWatchViewModel(api: mock) await vm.scanForExposures() #expect(vm.exposures.count == 1) #expect(vm.isScanning == false) } } @MainActor struct VoicePrintViewModelTests { @Test("VoicePrintViewModel loads enrollments and analyses") func loadData() async { let mock = MockTRPCalling() mock.stubbedEnrollments = [ VoiceEnrollment(id: "1", userId: "1", voiceSampleCount: 3, status: .active, createdAt: nil) ] mock.stubbedAnalyses = [ VoiceAnalysis(id: "1", userId: "1", enrollmentId: "1", result: "Match found", confidence: 0.92, processedAt: nil) ] let vm = VoicePrintViewModel(api: mock) await vm.loadData() #expect(vm.enrollments.count == 1) #expect(vm.analyses.count == 1) #expect(vm.isLoading == false) } @Test("VoicePrintViewModel deletes enrollment") func deleteEnrollment() async { let mock = MockTRPCalling() mock.stubbedEnrollments = [ VoiceEnrollment(id: "1", userId: "1", voiceSampleCount: 3, status: .active, createdAt: nil) ] mock.stubbedAnalyses = [ VoiceAnalysis(id: "1", userId: "1", enrollmentId: "1", result: "Match", confidence: 0.9, processedAt: nil) ] let vm = VoicePrintViewModel(api: mock) await vm.loadData() #expect(vm.enrollments.count == 1) await vm.deleteEnrollment(id: "1") #expect(vm.enrollments.isEmpty) #expect(vm.analyses.isEmpty) } } @MainActor struct SpamShieldViewModelTests { @Test("SpamShieldViewModel loads rules") func loadRules() async { let mock = MockTRPCalling() mock.stubbedRules = [ SpamRule(id: "1", userId: "1", pattern: "+1234", action: .block, priority: 1, enabled: true, createdAt: nil), SpamRule(id: "2", userId: "1", pattern: "spam", action: .flag, priority: 2, enabled: false, createdAt: nil) ] let vm = SpamShieldViewModel(api: mock) await vm.loadRules() #expect(vm.rules.count == 2) #expect(vm.blockedCount == 1) #expect(vm.flaggedCount == 0) #expect(vm.allowedCount == 0) } @Test("SpamShieldViewModel creates rule") func createRule() async { let mock = MockTRPCalling() let vm = SpamShieldViewModel(api: mock) await vm.createRule(pattern: "555", action: .block, priority: 1) #expect(vm.rules.count == 1) #expect(vm.rules[0].pattern == "555") #expect(vm.rules[0].action == .block) } @Test("SpamShieldViewModel toggles rule") func toggleRule() async { let mock = MockTRPCalling() mock.stubbedRules = [ SpamRule(id: "1", userId: "1", pattern: "test", action: .block, priority: 1, enabled: true, createdAt: nil) ] let vm = SpamShieldViewModel(api: mock) await vm.loadRules() #expect(vm.rules[0].enabled == true) await vm.toggleRule(vm.rules[0]) #expect(vm.rules[0].enabled == false) } @Test("SpamShieldViewModel deletes rule") func deleteRule() async { let mock = MockTRPCalling() mock.stubbedRules = [ SpamRule(id: "1", userId: "1", pattern: "test", action: .block, priority: 1, enabled: true, createdAt: nil) ] let vm = SpamShieldViewModel(api: mock) await vm.loadRules() #expect(vm.rules.count == 1) await vm.deleteRule(id: "1") #expect(vm.rules.isEmpty) } @Test("SpamShieldViewModel checks phone number") func checkNumber() async { let mock = MockTRPCalling() let vm = SpamShieldViewModel(api: mock) vm.checkPhoneNumber = "+15551234567" await vm.checkNumber() #expect(vm.checkResult != nil) #expect(vm.checkResult?.isSpam == true) #expect(vm.checkResult?.confidence == 0.85) #expect(vm.isCheckingNumber == false) } } @MainActor struct HomeTitleViewModelTests { @Test("HomeTitleViewModel loads properties") func loadProperties() async { let mock = MockTRPCalling() mock.stubbedProperties = [ PropertyWatchlistItem(id: "1", userId: "1", propertyType: "residential", address: "123 Main St", city: "Springfield", state: "IL", zipCode: "62701", status: "active", createdAt: nil) ] let vm = HomeTitleViewModel(api: mock) await vm.loadProperties() #expect(vm.properties.count == 1) #expect(vm.properties[0].address == "123 Main St") #expect(vm.isLoading == false) } @Test("HomeTitleViewModel adds property") func addProperty() async { let mock = MockTRPCalling() let vm = HomeTitleViewModel(api: mock) await vm.addProperty(address: "456 Oak Ave", city: "Portland", state: "OR", zipCode: "97201") #expect(vm.properties.count == 1) #expect(vm.properties[0].address == "456 Oak Ave") } @Test("HomeTitleViewModel deletes property") func deleteProperty() async { let mock = MockTRPCalling() mock.stubbedProperties = [ PropertyWatchlistItem(id: "1", userId: "1", propertyType: "residential", address: "123 Main St", city: "Springfield", state: "IL", zipCode: "62701", status: "active", createdAt: nil) ] let vm = HomeTitleViewModel(api: mock) await vm.loadProperties() #expect(vm.properties.count == 1) await vm.deleteProperty(id: "1") #expect(vm.properties.isEmpty) } } @MainActor struct RemoveBrokersViewModelTests { @Test("RemoveBrokersViewModel loads listings and requests") func loadData() async { let mock = MockTRPCalling() mock.stubbedBrokerListings = [ BrokerListing(id: "1", userId: "1", brokerName: "DataBrokerPro", url: "https://example.com", dataFound: true, status: .active, createdAt: nil) ] mock.stubbedRemovalRequests = [ RemovalRequest(id: "1", userId: "1", exposureId: "exp-1", status: .inProgress, requestedAt: nil, completedAt: nil, notes: nil) ] let vm = RemoveBrokersViewModel(api: mock) await vm.loadData() #expect(vm.listings.count == 1) #expect(vm.removalRequests.count == 1) #expect(vm.isLoading == false) } @Test("RemoveBrokersViewModel filters listings by search") func filterListings() async { let mock = MockTRPCalling() mock.stubbedBrokerListings = [ BrokerListing(id: "1", userId: "1", brokerName: "Acme Data", url: nil, dataFound: true, status: .active, createdAt: nil), BrokerListing(id: "2", userId: "1", brokerName: "Global Info", url: nil, dataFound: false, status: .pending, createdAt: nil) ] let vm = RemoveBrokersViewModel(api: mock) await vm.loadData() vm.searchQuery = "acme" #expect(vm.filteredListings.count == 1) #expect(vm.filteredListings[0].brokerName == "Acme Data") } @Test("RemoveBrokersViewModel starts removal") func startRemoval() async { let mock = MockTRPCalling() let vm = RemoveBrokersViewModel(api: mock) await vm.startRemoval(exposureId: "exp-1", notes: "Urgent") #expect(vm.removalRequests.count == 1) #expect(vm.removalRequests[0].exposureId == "exp-1") #expect(vm.removalRequests[0].notes == "Urgent") } } @MainActor struct AlertDetailViewModelTests { @Test("AlertDetailViewModel loads correlated alerts") func loadCorrelated() async { let mock = MockTRPCalling() let alert = Alert(id: "alert-1", userId: "1", type: .breach, severity: .critical, title: "Test", message: "!", read: false, createdAt: nil) mock.stubbedCorrelationGroups = [ CorrelationGroup(id: "cg-1", userId: "1", name: "Group 1", alertIds: ["alert-1", "alert-2"], correlationScore: 0.9, createdAt: nil) ] mock.stubbedAlerts = [ alert, Alert(id: "alert-2", userId: "1", type: .exposure, severity: .medium, title: "Related", message: "?", read: false, createdAt: nil) ] let vm = AlertDetailViewModel(alert: alert, api: mock) await vm.loadCorrelatedAlerts() #expect(vm.correlationGroups.count == 1) #expect(vm.correlatedAlerts.count == 1) #expect(vm.correlatedAlerts[0].id == "alert-2") } @Test("AlertDetailViewModel resolves alert") func resolveAlert() async { let mock = MockTRPCalling() let alert = Alert(id: "alert-1", userId: "1", type: .breach, severity: .critical, title: "Test", message: "!", read: false, createdAt: nil) let vm = AlertDetailViewModel(alert: alert, api: mock) await vm.resolveAlert() #expect(vm.isResolved == true) } @Test("AlertDetailViewModel reports false positive") func falsePositive() async { let mock = MockTRPCalling() let alert = Alert(id: "alert-1", userId: "1", type: .breach, severity: .critical, title: "Test", message: "!", read: false, createdAt: nil) let vm = AlertDetailViewModel(alert: alert, api: mock) await vm.reportFalsePositive() #expect(vm.isResolved == true) } } @MainActor struct SettingsViewModelTests { @Test("SettingsViewModel loads user and subscription") func loadSettings() async { let mock = MockTRPCalling() mock.stubbedUser = User(id: "1", name: "John Doe", email: "john@kordant.ai") mock.stubbedSubscription = Subscription(id: "1", userId: "1", tier: .premium, status: "active", currentPeriodStart: nil, currentPeriodEnd: nil, stripeCustomerId: nil, stripeSubscriptionId: nil) let vm = SettingsViewModel(api: mock) await vm.loadSettings() #expect(vm.user?.name == "John Doe") #expect(vm.user?.email == "john@kordant.ai") #expect(vm.subscription?.tier == .premium) #expect(vm.name == "John Doe") #expect(vm.email == "john@kordant.ai") } @Test("SettingsViewModel updates profile") func updateProfile() async { let mock = MockTRPCalling() mock.stubbedUser = User(id: "1", name: "Old Name", email: "old@kordant.ai", subscriptionTier: nil, createdAt: nil, updatedAt: nil) let vm = SettingsViewModel(api: mock) await vm.loadSettings() vm.name = "New Name" vm.email = "new@kordant.ai" await vm.updateProfile() #expect(vm.user?.name == "New Name") #expect(vm.user?.email == "new@kordant.ai") } @Test("SettingsViewModel error shows on API failure") func loadSettingsError() async { let mock = MockTRPCalling() mock.shouldSucceed = false let vm = SettingsViewModel(api: mock) await vm.loadSettings() #expect(vm.user == nil) #expect(vm.error != nil) } } // MARK: - APIConfig Tests struct APIConfigTests { @Test("APIConfig uses development in debug") func debugEnvironment() { let config = APIConfig() #expect(config.environment == .development) #expect(config.baseURL.absoluteString == "http://localhost:3000") } @Test("APIConfig custom config") func customConfig() { let config = APIConfig(environment: .production, timeout: 60, maxRetries: 5) #expect(config.environment == .production) #expect(config.baseURL.absoluteString == "https://api.kordant.ai") #expect(config.timeout == 60) #expect(config.maxRetries == 5) } @Test("APIConfig staging URL") func stagingURL() { let config = APIConfig(environment: .staging) #expect(config.baseURL.absoluteString == "https://staging.kordant.ai") } } // MARK: - Push Notification Tests struct PushNotificationServiceTests { @Test("NotificationPayload parses valid userInfo") func validPayload() { let userInfo: [AnyHashable: Any] = [ "screen": "alerts", "id": "alert-123", "aps": ["alert": ["title": "Test Title", "body": "Test Body"]], "image-url": "https://example.com/image.png" ] let payload = NotificationPayload(userInfo: userInfo) #expect(payload?.screen == "alerts") #expect(payload?.id == "alert-123") #expect(payload?.title == "Test Title") #expect(payload?.body == "Test Body") #expect(payload?.imageURL?.absoluteString == "https://example.com/image.png") } @Test("NotificationPayload returns nil without screen key") func missingScreen() { let userInfo: [AnyHashable: Any] = ["aps": [:]] let payload = NotificationPayload(userInfo: userInfo) #expect(payload == nil) } @Test("NotificationPayload parses minimal payload") func minimalPayload() { let userInfo: [AnyHashable: Any] = ["screen": "dashboard"] let payload = NotificationPayload(userInfo: userInfo) #expect(payload?.screen == "dashboard") #expect(payload?.id == nil) #expect(payload?.imageURL == nil) } @Test("Route initializes from notification payload") func routeFromNotification() { let payload: [AnyHashable: Any] = ["screen": "alerts", "id": "abc"] let route = Route(notificationPayload: payload) #expect(route == .alertDetail(id: "abc")) } @Test("Route from notification payload returns dashboard for home screen") func routeFromNotificationHome() { let payload: [AnyHashable: Any] = ["screen": "home"] let route = Route(notificationPayload: payload) #expect(route == .dashboard) } } // MARK: - BiometricAuthService Tests @MainActor struct BiometricAuthServiceTests { @Test("BiometricAuthService uses mock keychain") func usesMockKeychain() throws { let keychain = MockKeychainService() let service = BiometricAuthService(keychain: keychain) #expect(!service.isEnabled) } @Test("BiometricAuthService enable stores in keychain") func enableBiometric() throws { let keychain = MockKeychainService() try keychain.store(key: "jwt", value: Data("token".utf8)) let service = BiometricAuthService(keychain: keychain) try service.enable() #expect(service.isEnabled) } @Test("BiometricAuthService disable removes from keychain") func disableBiometric() throws { let keychain = MockKeychainService() try keychain.store(key: "jwt", value: Data("token".utf8)) let service = BiometricAuthService(keychain: keychain) try service.enable() #expect(service.isEnabled) service.disable() #expect(!service.isEnabled) } @Test("BiometricAuthService biometryType returns unavailable on simulator") func biometryTypeOnSimulator() { let keychain = MockKeychainService() let service = BiometricAuthService(keychain: keychain) let type = service.biometryType #expect(type != .faceID) // LAContext returns .none on simulator } @Test("BiometricAuthService isAvailable returns false on simulator") func isAvailableOnSimulator() { let keychain = MockKeychainService() let service = BiometricAuthService(keychain: keychain) #expect(!service.isAvailable) // No biometric hardware on simulator } } // MARK: - CameraService Tests @MainActor struct CameraServiceTests { @Test("CameraService returns correct permission status defaults") func permissionDefaults() { let service = CameraService() #expect(service.cameraPermission != .granted) // Not yet authorized #expect(service.microphonePermission != .granted) } @Test("CameraService usage descriptions are not empty") func usageDescriptions() { #expect(!CameraService.cameraUsageDescription().isEmpty) #expect(!CameraService.microphoneUsageDescription().isEmpty) } } // MARK: - VoicePrintViewModel Tests (submit enrollment) @MainActor struct VoicePrintSubmitTests { @Test("VoicePrintViewModel submits enrollment") func submitEnrollment() async { let mock = MockTRPCalling() mock.shouldSucceed = true let vm = VoicePrintViewModel(api: mock) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.wav") try? Data("mock-audio".utf8).write(to: tempURL) await vm.submitEnrollment(audioURL: tempURL) #expect(vm.enrollments.count == 1) #expect(vm.enrollments[0].status == .pending) #expect(vm.submitSuccess) #expect(!vm.showingRecordingSheet) } @Test("VoicePrintViewModel handles submission failure") func submitEnrollmentFailure() async { let mock = MockTRPCalling() mock.shouldSucceed = false let vm = VoicePrintViewModel(api: mock) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.wav") try? Data("mock-audio".utf8).write(to: tempURL) await vm.submitEnrollment(audioURL: tempURL) #expect(vm.enrollments.isEmpty) #expect(vm.error != nil) #expect(!vm.isSubmitting) } }