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 var lastAppleIdentityToken: String? var lastAppleAuthorizationCode: String? var lastAppleUserIdentifier: String? var lastGoogleIdToken: String? var lastRefreshToken: String? var didCallLogout = false 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.unauthorized } 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.serverError(statusCode: 409) } func resetPassword(email: String) async throws { if !shouldSucceed { throw APIError.notFound } } // MARK: - OAuth func loginWithApple( identityToken: String, authorizationCode: String, userIdentifier: String ) async throws -> AuthTokenResponse { lastAppleIdentityToken = identityToken lastAppleAuthorizationCode = authorizationCode lastAppleUserIdentifier = userIdentifier if shouldSucceed { return AuthTokenResponse( accessToken: "mock-token", refreshToken: "apple-refresh-token", user: User(id: "apple-user-1", name: "Apple User", email: "apple@privaterelay.appleid.com") ) } throw APIError.tRPCError(code: 401, message: "Invalid Apple identity token") } func loginWithGoogle(idToken: String) async throws -> AuthTokenResponse { lastGoogleIdToken = idToken if shouldSucceed { return AuthTokenResponse( accessToken: "mock-token", refreshToken: "google-refresh-token", user: User(id: "google-user-1", name: "Google User", email: "google@gmail.com") ) } throw APIError.tRPCError(code: 401, message: "Invalid Google ID token") } func refreshToken(refreshToken: String) async throws -> AuthTokenResponse { lastRefreshToken = refreshToken if shouldSucceed { return AuthTokenResponse( accessToken: "mock-token", refreshToken: "new-refresh-token", user: User(id: "", name: "", email: "") ) } throw APIError.unauthorized } func logout() async throws { didCallLogout = true if !shouldSucceed { throw APIError.serverError(statusCode: 500) } } } // 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? var onCallProcedure: ((String, (any Encodable)?) -> Any)? func callProcedure(path: String, input: (any Encodable)?) async throws -> T { if let onCallProcedure, let result = onCallProcedure(path, input) as? T { return result } 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: - Security Events var reportedSecurityEvents: [SecurityEventInput] = [] func reportSecurityEvent( eventType: SecurityEventType, severity: SecuritySeverity, indicators: [String], violations: [String], deviceInfo: DeviceSecurityInfo ) async throws { if !shouldSucceed { throw APIError.notImplemented } reportedSecurityEvents.append(SecurityEventInput( eventType: eventType.rawValue, severity: severity.rawValue, indicators: indicators, violations: violations, deviceInfo: deviceInfo )) } } struct SecurityEventInput { let eventType: String let severity: String let indicators: [String] let violations: [String] let deviceInfo: DeviceSecurityInfo } // 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("PushNotificationService has authorizationStatus helper") func hasAuthorizationStatus() async { // Verifying the method signature exists; actual status depends on simulator let service = PushNotificationService.shared let status = await service.authorizationStatus #expect(status == .notDetermined || status == .denied || status == .authorized) } @Test("PushNotificationService has isAuthorized helper") func hasIsAuthorized() async { let service = PushNotificationService.shared let authorized = await service.isAuthorized #expect(authorized == false) // Simulator starts as notDetermined } @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) } @Test("CameraService ensureCameraPermission handles denied state gracefully") func ensureCameraDenied() async { // On simulator, permission defaults to notDetermined let service = CameraService() let granted = await service.ensureCameraPermission() // Simulators will show system dialog -> simulator returns true // This test verifies the method doesn't crash #expect(granted == true || granted == false) } @Test("CameraService ensureMicrophonePermission handles denied state gracefully") func ensureMicrophoneDenied() async { let service = CameraService() let granted = await service.ensureMicrophonePermission() // Simulator may grant microphone automatically #expect(granted == true || granted == false) } @Test("CameraService openSettings does not crash") func openSettings() { // Verify the method exists and doesn't throw CameraService.shared.openSettings() } } // MARK: - Permission Type Tests @MainActor struct PermissionTypeTests { @Test("PermissionType has all expected cases") func allCases() { let cases = PermissionType.allCases #expect(cases.contains(.camera)) #expect(cases.contains(.microphone)) #expect(cases.contains(.notifications)) #expect(cases.contains(.faceID)) } @Test("PermissionType icons are non-empty") func icons() { for type in PermissionType.allCases { #expect(!type.icon.isEmpty) } } @Test("PermissionType titles are non-empty") func titles() { for type in PermissionType.allCases { #expect(!type.title.isEmpty) } } @Test("PermissionType explanations are non-empty") func explanations() { for type in PermissionType.allCases { #expect(!type.explanation.isEmpty) } } @Test("PermissionType benefits are non-empty") func benefits() { for type in PermissionType.allCases { #expect(!type.benefit.isEmpty) } } @Test("PermissionType setting names are non-empty") func settingNames() { for type in PermissionType.allCases { #expect(!type.settingName.isEmpty) } } } // MARK: - PermissionService Tests @MainActor struct PermissionServiceTests { @Test("PermissionRationaleView instantiates") func rationaleViewCamera() { let view = PermissionRationaleView(permissionType: .camera, onAllow: {}, onDeny: {}) #expect(view.permissionType == .camera) } @Test("PermissionRationaleView instantiates for each type") func rationaleViewAll() { for type in PermissionType.allCases { let view = PermissionRationaleView(permissionType: type, onAllow: {}, onDeny: {}) #expect(view.permissionType == type) } } @Test("PermissionDeniedView instantiates") func deniedView() { let view = PermissionDeniedView( permissionType: .microphone, onOpenSettings: {}, onDismiss: {} ) #expect(view.permissionType == .microphone) } @Test("PermissionDeniedView instantiates for each type") func deniedViewAll() { for type in PermissionType.allCases { let view = PermissionDeniedView( permissionType: type, onOpenSettings: {}, onDismiss: {} ) #expect(view.permissionType == type) } } @Test("PermissionSettingsOpener has openSettings") func settingsOpenerExists() { // Just verify the type exists and has the static method #expect(PermissionSettingsOpener.self != nil) } } // 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) } } // MARK: - OAuth & Social Login Tests @MainActor struct OAuthIntegrationTests { @Test("AuthService Apple Sign-In succeeds with mock client") func appleSignInAPISuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let response = try? await client.loginWithApple( identityToken: "mock-apple-identity-token", authorizationCode: "mock-apple-auth-code", userIdentifier: "000123.abc123def" ) #expect(response != nil) #expect(response?.user.id == "apple-user-1") #expect(response?.user.name == "Apple User") #expect(response?.refreshToken == "apple-refresh-token") #expect(response?.accessToken == "mock-token") #expect(client.lastAppleIdentityToken == "mock-apple-identity-token") #expect(client.lastAppleAuthorizationCode == "mock-apple-auth-code") #expect(client.lastAppleUserIdentifier == "000123.abc123def") } @Test("AuthService Apple Sign-In fails with invalid token") func appleSignInAPIFailure() async { let client = MockAuthAPIClient() client.shouldSucceed = false let service = AuthService(keychain: MockKeychainService(), apiClient: client) await service.loginWithApple() #expect(service.state == .unauthenticated) #expect(service.signInError != nil) } @Test("AuthService Google Sign-In succeeds with mock client") func googleSignInAPISuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let response = try? await client.loginWithGoogle(idToken: "mock-google-id-token") #expect(response != nil) #expect(response?.user.id == "google-user-1") #expect(response?.user.name == "Google User") #expect(response?.refreshToken == "google-refresh-token") #expect(response?.accessToken == "mock-token") #expect(client.lastGoogleIdToken == "mock-google-id-token") } @Test("AuthService Google Sign-In fails with invalid token") func googleSignInAPIFailure() async { let client = MockAuthAPIClient() client.shouldSucceed = false let service = AuthService(keychain: MockKeychainService(), apiClient: client) await service.loginWithGoogle() #expect(service.state == .unauthenticated) #expect(service.signInError != nil) } } // MARK: - Token Refresh Tests @MainActor struct TokenRefreshIntegrationTests { @Test("Token refresh succeeds with valid refresh token") func refreshTokenSuccess() async { let client = MockAuthAPIClient() client.shouldSucceed = true let keychain = MockKeychainService() try? keychain.store(key: "refreshToken", value: Data("existing-refresh-token".utf8)) try? keychain.store(key: "jwt", value: Data("existing-jwt".utf8)) let service = AuthService(keychain: keychain, apiClient: client) let result = await service.attemptSilentRefresh() #expect(result == true) #expect(client.lastRefreshToken == "existing-refresh-token") } @Test("Token refresh fails without stored refresh token") func refreshTokenFailsWithoutToken() async { let client = MockAuthAPIClient() client.shouldSucceed = true let service = AuthService(keychain: MockKeychainService(), apiClient: client) let result = await service.attemptSilentRefresh() #expect(result == false) } @Test("Token refresh fails when API returns unauthorized") func refreshTokenFailsUnauthorized() async { let client = MockAuthAPIClient() client.shouldSucceed = false let keychain = MockKeychainService() try? keychain.store(key: "refreshToken", value: Data("expired-token".utf8)) let service = AuthService(keychain: keychain, apiClient: client) let result = await service.attemptSilentRefresh() #expect(result == false) } @Test("Refresh token API returns new token pair") func refreshTokenAPIReturnsNewPair() async { let client = MockAuthAPIClient() client.shouldSucceed = true let response = try? await client.refreshToken(refreshToken: "valid-refresh-token") #expect(response != nil) #expect(response?.accessToken == "mock-token") #expect(response?.refreshToken == "new-refresh-token") #expect(client.lastRefreshToken == "valid-refresh-token") } @Test("Refresh token API throws on invalid token") func refreshTokenAPIThrows() async { let client = MockAuthAPIClient() client.shouldSucceed = false await #expect(throws: APIError.unauthorized) { try await client.refreshToken(refreshToken: "invalid-refresh-token") } } } // MARK: - Logout Tests @MainActor struct LogoutIntegrationTests { @Test("Logout calls backend to revoke tokens and clears local state") func logoutRevokesAndClears() async { let client = MockAuthAPIClient() client.shouldSucceed = true let keychain = MockKeychainService() try? keychain.store(key: "jwt", value: Data("token".utf8)) try? keychain.store(key: "refreshToken", value: Data("refresh".utf8)) try? keychain.store(key: "currentUser", value: try JSONEncoder().encode(User(id: "1", name: "Test", email: "t@t.com"))) let service = AuthService(keychain: keychain, apiClient: client) service.state = .authenticated service.logout() try? await Task.sleep(nanoseconds: 100_000_000) #expect(service.state == .unauthenticated) #expect(service.currentUser == nil) #expect(try? keychain.retrieve(key: "jwt") == nil) #expect(try? keychain.retrieve(key: "refreshToken") == nil) #expect(client.didCallLogout == true) } @Test("Logout handles backend revocation failure gracefully") func logoutHandlesFailure() async { let client = MockAuthAPIClient() client.shouldSucceed = false let keychain = MockKeychainService() try? keychain.store(key: "jwt", value: Data("token".utf8)) let service = AuthService(keychain: keychain, apiClient: client) service.state = .authenticated service.logout() try? await Task.sleep(nanoseconds: 100_000_000) #expect(service.state == .unauthenticated) #expect(client.didCallLogout == true) } @Test("Force logout skips backend call but clears state") func forceLogoutClearsState() async { let client = MockAuthAPIClient() let keychain = MockKeychainService() try? keychain.store(key: "jwt", value: Data("token".utf8)) let service = AuthService(keychain: keychain, apiClient: client) service.state = .authenticated service.forceLogout() #expect(service.state == .unauthenticated) #expect(client.didCallLogout == false) } } // MARK: - OAuth Security Tests @MainActor struct OAuthSecurityTests { @Test("Invalid Apple identity token is rejected") func rejectInvalidAppleToken() async { let client = MockAuthAPIClient() client.shouldSucceed = false do { _ = try await client.loginWithApple( identityToken: "invalid-token", authorizationCode: "invalid-code", userIdentifier: "invalid-user" ) Issue.record("Expected error but got success") } catch let error as APIError { if case .tRPCError(let code, _) = error { #expect(code == 401) } else { Issue.record("Expected tRPCError with code 401") } } catch { Issue.record("Unexpected error: \(error)") } } @Test("Invalid Google ID token is rejected") func rejectInvalidGoogleToken() async { let client = MockAuthAPIClient() client.shouldSucceed = false do { _ = try await client.loginWithGoogle(idToken: "invalid-token") Issue.record("Expected error but got success") } catch let error as APIError { if case .tRPCError(let code, _) = error { #expect(code == 401) } else { Issue.record("Expected tRPCError with code 401") } } catch { Issue.record("Unexpected error: \(error)") } } @Test("Expired refresh token forces re-authentication") func expiredRefreshTokenForcesReauth() async { let client = MockAuthAPIClient() client.shouldSucceed = false let keychain = MockKeychainService() try? keychain.store(key: "refreshToken", value: Data("expired-token".utf8)) let service = AuthService(keychain: keychain, apiClient: client) let result = await service.attemptSilentRefresh() #expect(result == false) #expect(service.state == .unauthenticated) } @Test("Cancelled sign-in does not show error alert") func cancelledSignInShowsNoError() { let service = AuthService(keychain: MockKeychainService(), apiClient: MockAuthAPIClient()) // When sign-in is cancelled, signInError is set to .cancelled // The view layer checks this and does not show the alert service.signInError = .cancelled #expect(service.signInError == .cancelled) #expect(service.error == nil) } @Test("Network error sets appropriate sign-in error") func networkErrorSetsProperError() { let service = AuthService(keychain: MockKeychainService(), apiClient: MockAuthAPIClient()) // Simulate network error handling service.signInError = .networkError #expect(service.signInError?.localizedDescription.contains("Unable to connect") == true) } } // MARK: - Image Cache Tests struct ImageCacheServiceTests { @Test("ImageCacheService shared instance exists") func sharedInstance() { let service = ImageCacheService.shared #expect(service !== nil) } @Test("ImageCacheService cache stats return valid data") @MainActor func cacheStatsValid() { let stats = ImageCacheService.shared.cacheStats #expect(stats.memoryCapacity == 50 * 1024 * 1024) #expect(stats.diskCapacity == 100 * 1024 * 1024) #expect(stats.memoryUsage >= 0) #expect(stats.diskUsage >= 0) #expect(stats.cachedEntries >= 0) } @Test("ImageCacheService clearCache resets stats") @MainActor func clearCacheResets() { let service = ImageCacheService.shared service.clearCache() let stats = service.cacheStats #expect(stats.memoryUsage == 0 || stats.diskUsage >= 0) } @Test("ImageCacheService isCached returns false for unknown URL") @MainActor func notCachedForUnknownURL() { let url = URL(string: "https://example.com/nonexistent.jpg")! let cached = ImageCacheService.shared.isCached(url: url) #expect(cached == false) } @Test("ImageCacheService cancelAllDownloads does not crash") @MainActor func cancelAllDownloads() { let service = ImageCacheService.shared // Should not crash when no active downloads service.cancelAllDownloads() #expect(true) // reached without crash } @Test("ImageCacheService handles memory warning gracefully") @MainActor func memoryWarningHandling() { let service = ImageCacheService.shared // Trigger the memory warning handler NotificationCenter.default.post( name: UIApplication.didReceiveMemoryWarningNotification, object: nil ) // Should not crash #expect(true) } } // MARK: - ImageOptimizer Tests struct ImageOptimizerTests { @Test("ImageOptimizer shared instance exists") func sharedInstance() { let optimizer = ImageOptimizer.shared #expect(optimizer !== nil) } @Test("ImageOptimizer compressForUpload produces smaller data") func compressForUploadReducesSize() { let optimizer = ImageOptimizer.shared let image = UIImage() let compressed = optimizer.compressForUpload(image, quality: 0.1) // Empty image should still produce some data #expect(compressed.isEmpty == false || compressed is Data) } @Test("ImageOptimizer sizedURL appends query parameters") func sizedURLAppendsParams() { let optimizer = ImageOptimizer.shared let baseURL = URL(string: "https://example.com/image.jpg")! let sized = optimizer.sizedURL(for: baseURL, size: .thumbnail) #expect(sized.absoluteString.contains("w=")) #expect(sized.absoluteString.contains("h=")) } @Test("ImageOptimizer ImageSize values are reasonable") func imageSizeValues() { #expect(ImageSize.thumbnail.size.width == 60) #expect(ImageSize.thumbnail.size.height == 60) #expect(ImageSize.medium.size.width == 300) #expect(ImageSize.large.size.width == 800) #expect(ImageSize.full.size.width == 4096) } @Test("ImageOptimizer ImageSize staticSizeForWidth") func sizeForWidth() { let smallSize = ImageSize.size(for: 60) let mediumSize = ImageSize.size(for: 300) let largeSize = ImageSize.size(for: 800) #expect(smallSize == ImageSize.thumbnail.size) #expect(mediumSize == ImageSize.medium.size) #expect(largeSize == ImageSize.large.size) } @Test("ImageFormat preferred is HEIC on iOS 17+") func preferredFormat() { let format = ImageFormat.preferred #expect(format == .heic || format == .jpeg) } @Test("ImageFormat mime types are correct") func formatMimeTypes() { #expect(ImageFormat.heic.mimeType == "image/heic") #expect(ImageFormat.jpeg.mimeType == "image/jpeg") #expect(ImageFormat.png.mimeType == "image/png") } } // MARK: - AsyncSemaphore Tests struct AsyncSemaphoreTests { @Test("AsyncSemaphore allows up to count concurrent operations") func allowsConcurrentOperations() async { let semaphore = AsyncSemaphore(count: 3) var concurrentCount = 0 var maxConcurrent = 0 await withTaskGroup(of: Void.self) { group in for _ in 0..<10 { group.addTask { await semaphore.wait() concurrentCount += 1 maxConcurrent = max(maxConcurrent, concurrentCount) try? await Task.sleep(nanoseconds: 10_000_000) concurrentCount -= 1 semaphore.signal() } } } #expect(maxConcurrent <= 3) } @Test("AsyncSemaphore withLock executes operation") func withLockExecutes() async { let semaphore = AsyncSemaphore(count: 1) var executed = false let result = await semaphore.withLock { executed = true return 42 } #expect(executed) #expect(result == 42) } } // MARK: - ImageUploadQueue Tests struct ImageUploadQueueTests { @Test("ImageUploadQueue enqueue does not crash") func enqueueUpload() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let queue = ImageUploadQueue(defaults: defaults) let image = UIImage() queue.enqueueUpload(image: image, endpoint: "/upload/test") #expect(queue.pendingCount == 1) } @Test("ImageUploadQueue clearQueue removes all") func clearQueue() { let defaults = UserDefaults(suiteName: UUID().uuidString)! let queue = ImageUploadQueue(defaults: defaults) let image = UIImage() queue.enqueueUpload(image: image, endpoint: "/upload/test") #expect(queue.pendingCount == 1) queue.clearQueue() #expect(queue.pendingCount == 0) } } // MARK: - CachedAsyncImage Tests struct CachedAsyncImageTests { @Test("CachedAsyncImage phases have all cases") func phaseCases() { // Verify the Phase enum exists by checking types _ = CachedAsyncImage.Phase.loading _ = CachedAsyncImage.Phase.failure(ImageCacheError.downloadFailed) #expect(true) } @Test("ImagePrefetcher shared instance exists") func prefetcherShared() { let prefetcher = ImagePrefetcher.shared #expect(prefetcher !== nil) } @Test("ImagePrefetcher cancelAllDownloads does not crash") func prefetcherCancel() { let prefetcher = ImagePrefetcher.shared prefetcher.cancelPrefetch([URL(string: "https://example.com/img.jpg")!]) #expect(true) } @Test("ImagePrefetcher reset clears state") func prefetcherReset() { let prefetcher = ImagePrefetcher.shared prefetcher.reset() #expect(true) // No crash } } // MARK: - ImageCacheError Tests struct ImageCacheErrorTests { @Test("ImageCacheError descriptions are non-empty") func errorDescriptions() { #expect(ImageCacheError.downloadFailed.errorDescription?.isEmpty == false) #expect(ImageCacheError.invalidImageData.errorDescription?.isEmpty == false) #expect(ImageCacheError.cancelled.errorDescription?.isEmpty == false) #expect(ImageCacheError.notCached.errorDescription?.isEmpty == false) } } // MARK: - Jailbreak Detector Tests struct JailbreakDetectorTests { private let detector = JailbreakDetector.shared @Test("JailbreakDetector returns notDetected on non-jailbroken device") func notJailbroken() { let status = detector.check() #expect(status == .notDetected) #expect(!detector.isJailbroken) } @Test("JailbreakDetector checkIndicator returns false for all indicators on clean device") func allIndicatorsFalse() { for indicator in JailbreakIndicator.allCases { #expect(!detector.checkIndicator(indicator), "\(indicator.rawValue) should not be detected on clean device") } } @Test("JailbreakIndicator severity values are correct") func indicatorSeverities() { #expect(JailbreakIndicator.cydiaApp.severity == .high) #expect(JailbreakIndicator.writableSystemPaths.severity == .critical) #expect(JailbreakIndicator.dyldInsertLibrary.severity == .medium) #expect(JailbreakIndicator.dodgeMasterApp.severity == .low) } @Test("JailbreakStatus indicatorCount is zero when not detected") func indicatorCountZero() { let status: JailbreakStatus = .notDetected #expect(status.indicatorCount == 0) #expect(!status.isJailbroken) } @Test("JailbreakStatus indicatorCount matches array count when detected") func indicatorCountMatches() { let indicators: [JailbreakIndicator] = [.cydiaApp, .writableSystemPaths] let status: JailbreakStatus = .detected(indicators: indicators) #expect(status.indicatorCount == 2) #expect(status.isJailbroken) } @Test("JailbreakSeverity has all expected cases") func severityCases() { let severities: [JailbreakSeverity] = [.critical, .high, .medium, .low] #expect(severities.count == 4) } } // MARK: - Runtime Integrity Monitor Tests struct RuntimeIntegrityMonitorTests { private let monitor = RuntimeIntegrityMonitor.shared @Test("RuntimeIntegrityMonitor has no violations on clean device") func noViolations() { let violations = monitor.checkIntegrity() #expect(violations.isEmpty) #expect(!monitor.hasViolations) } @Test("RuntimeIntegrityMonitor checkViolation returns false for all on clean device") func allViolationsFalse() { for violation in IntegrityViolation.allCases { // Skip simulator detection as it's expected on simulator if violation == .simulatorDetected { continue } #expect(!monitor.checkViolation(violation), "\(violation.rawValue) should not be detected on clean device") } } @Test("IntegrityViolation severity values are correct") func violationSeverities() { #expect(IntegrityViolation.debuggerAttached.severity == .high) #expect(IntegrityViolation.codeInjection.severity == .critical) #expect(IntegrityViolation.bundleIdentifierMismatch.severity == .critical) #expect(IntegrityViolation.simulatorDetected.severity == .low) } @Test("SecuritySeverity has all expected cases") func severityCases() { let severities: [SecuritySeverity] = [.critical, .high, .medium, .low, .info] #expect(severities.count == 5) } } // MARK: - Obfuscated String Tests struct ObfuscatedStringTests { @Test("ObfuscatedString encrypts and decrypts correctly") func encryptDecrypt() { let original = "https://api.kordant.ai" let obfuscated = ObfuscatedString(original) #expect(obfuscated.value == original) } @Test("ObfuscatedString is ExpressibleByStringLiteral") func stringLiteral() { let obfuscated: ObfuscatedString = "test-value" #expect(obfuscated.value == "test-value") } @Test("ObfuscatedString equality works") func equality() { let a = ObfuscatedString("same-value") let b = ObfuscatedString("same-value") let c = ObfuscatedString("different-value") #expect(a == b) #expect(a != c) } @Test("ObfuscatedString description returns decrypted value") func description() { let obfuscated = ObfuscatedString("secret") #expect(obfuscated.description == "secret") } @Test("ObfuscatedURL creates valid URL") func obfuscatedURL() { let url = ObfuscatedURL("https://api.kordant.ai") #expect(url.url != nil) #expect(url.value == "https://api.kordant.ai") } @Test("APIEndpoints are accessible") func apiEndpoints() { #expect(APIEndpoints.baseURL.value == "https://api.kordant.ai") #expect(APIEndpoints.stagingURL.value == "https://staging.kordant.ai") #expect(APIEndpoints.developmentURL.value == "http://localhost:3000") #expect(APIEndpoints.trpcBase.value == "/api/trpc") } } // MARK: - Secure Enclave Service Tests struct SecureEnclaveServiceTests { @Test("SecureEnclaveService is not available on simulator") func notAvailableOnSimulator() { let service = SecureEnclaveService(keychainService: MockKeychainService()) #expect(!service.isAvailable) } @Test("SecureEnclaveError descriptions are non-empty") func errorDescriptions() { #expect(SecureEnclaveError.keyGenerationFailed(-1).errorDescription?.isEmpty == false) #expect(SecureEnclaveError.biometryNotAvailable.errorDescription?.isEmpty == false) #expect(SecureEnclaveError.unsupportedDevice.errorDescription?.isEmpty == false) } } // MARK: - Security Manager Tests @MainActor struct SecurityManagerTests { private func makeManager() -> SecurityManager { let mockDetector = MockJailbreakDetector() let mockMonitor = MockRuntimeIntegrityMonitor() let mockEnclave = MockSecureEnclaveService() let mockTRPC = MockTRPCalling() mockTRPC.shouldSucceed = true return SecurityManager( jailbreakDetector: mockDetector, integrityMonitor: mockMonitor, secureEnclaveService: mockEnclave, trpcBridge: mockTRPC ) } @Test("SecurityManager starts in non-degraded mode") func initialMode() { let manager = makeManager() #expect(!manager.isDegradedMode) #expect(!manager.showSecurityWarning) #expect(!manager.securityCheckComplete) } @Test("SecurityManager runs security checks and completes") func runSecurityChecks() async { let manager = makeManager() await manager.runSecurityChecks() #expect(manager.securityCheckComplete) #expect(manager.lastSecurityCheck != nil) } @Test("SecurityManager activates degraded mode on jailbreak") func degradedModeOnJailbreak() async { let mockDetector = MockJailbreakDetector() mockDetector.simulateJailbreak = true let manager = SecurityManager( jailbreakDetector: mockDetector, integrityMonitor: MockRuntimeIntegrityMonitor(), secureEnclaveService: MockSecureEnclaveService(), trpcBridge: MockTRPCalling() ) await manager.runSecurityChecks() #expect(manager.isDegradedMode) #expect(manager.showSecurityWarning) } @Test("SecurityManager activates degraded mode on integrity violations") func degradedModeOnIntegrityViolation() async { let mockMonitor = MockRuntimeIntegrityMonitor() mockMonitor.simulateViolation = .codeInjection let manager = SecurityManager( jailbreakDetector: MockJailbreakDetector(), integrityMonitor: mockMonitor, secureEnclaveService: MockSecureEnclaveService(), trpcBridge: MockTRPCalling() ) await manager.runSecurityChecks() #expect(manager.isDegradedMode) #expect(manager.degradedConfig == .full) } @Test("SecurityManager stays in normal mode when clean") func normalModeWhenClean() async { let manager = makeManager() await manager.runSecurityChecks() #expect(!manager.isDegradedMode) #expect(!manager.showSecurityWarning) #expect(manager.degradedConfig == .none) } @Test("SecurityManager partial degraded mode on low-severity issues") func partialDegradedMode() async { let mockMonitor = MockRuntimeIntegrityMonitor() mockMonitor.simulateViolation = .debuggerAttached let manager = SecurityManager( jailbreakDetector: MockJailbreakDetector(), integrityMonitor: mockMonitor, secureEnclaveService: MockSecureEnclaveService(), trpcBridge: MockTRPCalling() ) await manager.runSecurityChecks() #expect(manager.isDegradedMode) #expect(manager.degradedConfig == .partial) } } // MARK: - Degraded Mode Config Tests struct DegradedModeConfigTests { @Test("Full degraded mode disables all features") func fullConfig() { let config = DegradedModeConfig.full #expect(config.disableBiometricAuth) #expect(config.disablePayments) #expect(config.disableSensitiveData) #expect(config.showWarningBanner) #expect(config.restrictAPIAccess) #expect(config.logAllActivity) } @Test("Partial degraded mode disables only critical features") func partialConfig() { let config = DegradedModeConfig.partial #expect(!config.disableBiometricAuth) #expect(config.disablePayments) #expect(!config.disableSensitiveData) #expect(config.showWarningBanner) #expect(!config.restrictAPIAccess) #expect(config.logAllActivity) } @Test("None degraded mode enables all features") func noneConfig() { let config = DegradedModeConfig.none #expect(!config.disableBiometricAuth) #expect(!config.disablePayments) #expect(!config.disableSensitiveData) #expect(!config.showWarningBanner) #expect(!config.restrictAPIAccess) #expect(!config.logAllActivity) } } // MARK: - Security Event Tests struct SecurityEventTests { @Test("SecurityEvent creates with unique ID") func uniqueID() { let event1 = SecurityEvent(eventType: .jailbreakDetected, severity: .high) let event2 = SecurityEvent(eventType: .jailbreakDetected, severity: .high) #expect(event1.id != event2.id) } @Test("SecurityEvent captures timestamp") func timestamp() { let event = SecurityEvent(eventType: .jailbreakDetected, severity: .high) #expect(event.timestamp.timeIntervalSince1970 > 0) } @Test("SecurityEventType has all expected cases") func eventTypeCases() { let types: [SecurityEventType] = [ .jailbreakDetected, .jailbreakNotDetected, .debuggerAttached, .codeInjectionDetected, .methodSwizzlingDetected, .binaryModified, .bundleIdentifierMismatch, .fridaDetected, .integrityCheckPassed, .secureEnclaveAvailable, .secureEnclaveUnavailable, .biometricAuthSuccess, .biometricAuthFailure, .keychainAccess, .securityInitialization ] #expect(types.count == 15) } @Test("DeviceSecurityInfo captures current device info") func deviceSecurityInfo() { let info = DeviceSecurityInfo.current #expect(!info.platform.isEmpty) #expect(!info.osVersion.isEmpty) #expect(!info.model.isEmpty) #expect(!info.bundleVersion.isEmpty) #expect(!info.buildNumber.isEmpty) #expect(!info.timestamp.isEmpty) } } // MARK: - Keychain Service Biometry Tests @MainActor struct KeychainServiceBiometryTests { @Test("KeychainService stores and retrieves with service identifier") func storeWithService() throws { let keychain = KeychainService() try keychain.store(key: "test_service", value: Data("hello".utf8)) let result = try keychain.retrieve(key: "test_service") #expect(result == Data("hello".utf8)) try keychain.delete(key: "test_service") } @Test("KeychainService clearAll respects service identifier") func clearAllWithService() throws { let keychain = KeychainService() 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) } @Test("KeychainError descriptions are non-empty") func errorDescriptions() { #expect(KeychainError.storeFailed(-1).errorDescription?.isEmpty == false) #expect(KeychainError.retrieveFailed(-1).errorDescription?.isEmpty == false) #expect(KeychainError.deleteFailed(-1).errorDescription?.isEmpty == false) } } // MARK: - Mock Classes for Security Tests final class MockJailbreakDetector: JailbreakDetecting { var simulateJailbreak = false var jailbreakIndicators: [JailbreakIndicator] = [.cydiaApp, .writableSystemPaths] var isJailbroken: Bool { status.isJailbroken } var status: JailbreakStatus { check() } func check() -> JailbreakStatus { if simulateJailbreak { return .detected(indicators: jailbreakIndicators) } return .notDetected } func checkIndicator(_ indicator: JailbreakIndicator) -> Bool { simulateJailbreak && jailbreakIndicators.contains(indicator) } } final class MockRuntimeIntegrityMonitor: RuntimeIntegrityMonitoring { var simulateViolation: IntegrityViolation? var hasViolations: Bool { !checkIntegrity().isEmpty } var violations: [IntegrityViolation] { checkIntegrity() } func checkIntegrity() -> [IntegrityViolation] { if let violation = simulateViolation { return [violation] } return [] } func checkViolation(_ violation: IntegrityViolation) -> Bool { simulateViolation == violation } } final class MockSecureEnclaveService: SecureEnclaveServiceProtocol { var simulateAvailable = true var isAvailable: Bool { simulateAvailable } func generateKeyPair(accessControl: SecAccessControl?) throws -> String { "mock-key-" + UUID().uuidString } func signData(_ data: Data, withKey keyID: String, requireBiometry: Bool) throws -> Data { Data("mock-signature".utf8) } func verifySignature(_ signature: Data, for data: Data, withKey keyID: String) throws -> Bool { true } func encrypt(_ data: Data, withKey keyID: String) throws -> Data { data } func decrypt(_ ciphertext: Data, withKey keyID: String) throws -> Data { ciphertext } func deleteKey(_ keyID: String) throws {} func createBiometryProtectedKeychainItem(key: String, value: Data, accessControl: SecAccessControl?) throws {} func retrieveBiometryProtectedKeychainItem(key: String, context: LAContext?) throws -> Data? { nil } func deleteBiometryProtectedKeychainItem(key: String) throws {} } // MARK: - Enhanced Notification Tests struct EnhancedNotificationPayloadTests { @Test("NotificationPayload derives type from screen") func derivesTypeFromScreen() { let alertPayload = NotificationPayload(userInfo: ["screen": "alerts", "id": "a1"])! #expect(alertPayload.type == .alert) #expect(alertPayload.category == .alert) let exposurePayload = NotificationPayload(userInfo: ["screen": "exposure"])! #expect(exposurePayload.type == .exposure) let scanPayload = NotificationPayload(userInfo: ["screen": "scanComplete"])! #expect(scanPayload.type == .scanComplete) let familyPayload = NotificationPayload(userInfo: ["screen": "familyInvite"])! #expect(familyPayload.type == .familyInvite) let billingPayload = NotificationPayload(userInfo: ["screen": "subscriptionRenewal"])! #expect(billingPayload.type == .subscriptionRenewal) let marketingPayload = NotificationPayload(userInfo: ["screen": "marketing"])! #expect(marketingPayload.type == .marketing) let unknownPayload = NotificationPayload(userInfo: ["screen": "unknown_screen"])! #expect(unknownPayload.type == .unknown) } @Test("NotificationPayload explicit type overrides screen-derived type") func explicitType() { let userInfo: [AnyHashable: Any] = [ "screen": "alerts", "type": "exposure", "id": "abc" ] let payload = NotificationPayload(userInfo: userInfo)! #expect(payload.type == .exposure) #expect(payload.screen == "alerts") } @Test("NotificationPayload parses metadata") func parsesMetadata() { let userInfo: [AnyHashable: Any] = [ "screen": "alerts", "metadata": ["severity": "critical", "source": "darkweb"] ] let payload = NotificationPayload(userInfo: userInfo)! #expect(payload.metadata["severity"] == "critical") #expect(payload.metadata["source"] == "darkweb") } @Test("NotificationPayload parses action URL") func parsesActionURL() { let userInfo: [AnyHashable: Any] = [ "screen": "alerts", "action-url": "kordant://alerts/abc123" ] let payload = NotificationPayload(userInfo: userInfo)! #expect(payload.actionURL?.absoluteString == "kordant://alerts/abc123") } @Test("NotificationPayload parses critical flag") func parsesCritical() { let criticalPayload = NotificationPayload(userInfo: [ "screen": "alerts", "kordant_critical": true ])! #expect(criticalPayload.isCritical == true) let normalPayload = NotificationPayload(userInfo: ["screen": "alerts"])! #expect(normalPayload.isCritical == false) } @Test("NotificationPayload category is inferred from type") func categoryInferredFromType() { let alertPayload = NotificationPayload(userInfo: ["screen": "alerts"])! #expect(alertPayload.category == .alert) let exposurePayload = NotificationPayload(userInfo: ["screen": "exposure"])! #expect(exposurePayload.category == .exposure) } @Test("NotificationPayload category can be explicitly set") func explicitCategory() { let userInfo: [AnyHashable: Any] = [ "screen": "alerts", "category": "EXPOSURE" ] let payload = NotificationPayload(userInfo: userInfo)! #expect(payload.category == .exposure) } @Test("NotificationPayload convenience init works") func convenienceInit() { let payload = NotificationPayload( screen: "alerts", id: "alert-1", title: "Test Alert", body: "This is a test", type: .alert, category: .alert, metadata: ["severity": "high"], isCritical: true ) #expect(payload.screen == "alerts") #expect(payload.id == "alert-1") #expect(payload.title == "Test Alert") #expect(payload.body == "This is a test") #expect(payload.type == .alert) #expect(payload.category == .alert) #expect(payload.metadata["severity"] == "high") #expect(payload.isCritical == true) } @Test("NotificationType all cases have non-empty display names and icons") func notificationTypeProperties() { for type in NotificationType.allCases { #expect(!type.displayName.isEmpty) #expect(!type.iconName.isEmpty) } } @Test("NotificationType from screen name") func notificationTypeFromScreen() { #expect(NotificationType.from(screen: "alerts") == .alert) #expect(NotificationType.from(screen: "exposure") == .exposure) #expect(NotificationType.from(screen: "darkwatch") == .exposure) #expect(NotificationType.from(screen: "scanComplete") == .scanComplete) #expect(NotificationType.from(screen: "scan_complete") == .scanComplete) #expect(NotificationType.from(screen: "familyInvite") == .familyInvite) #expect(NotificationType.from(screen: "family") == .familyInvite) #expect(NotificationType.from(screen: "subscriptionRenewal") == .subscriptionRenewal) #expect(NotificationType.from(screen: "marketing") == .marketing) #expect(NotificationType.from(screen: "unknown") == .unknown) } } // MARK: - Deep Link Router Tests struct NotificationDeepLinkRouterTests { @Test("Route deep link for alerts") func deepLinkAlerts() { let url = URL(string: "kordant://alerts/abc123")! let route = Route(deepLink: url) #expect(route == .alertDetail(id: "abc123")) } @Test("Route deep link for alerts list") func deepLinkAlertsList() { let url = URL(string: "kordant://alerts")! let route = Route(deepLink: url) #expect(route == .alerts) } @Test("Route deep link for dashboard") func deepLinkDashboard() { let url = URL(string: "kordant://dashboard")! let route = Route(deepLink: url) #expect(route == .dashboard) } @Test("Route deep link for settings") func deepLinkSettings() { let url = URL(string: "kordant://settings")! let route = Route(deepLink: url) #expect(route == .settings) } @Test("Route deep link for notifications") func deepLinkNotifications() { let url = URL(string: "kordant://notifications")! let route = Route(deepLink: url) #expect(route == .notificationSettings) } @Test("Route deep link for family") func deepLinkFamily() { let url = URL(string: "kordant://family")! let route = Route(deepLink: url) #expect(route == .family) } @Test("Route deep link for billing") func deepLinkBilling() { let url = URL(string: "kordant://billing")! let route = Route(deepLink: url) #expect(route == .billing) } @Test("Route deep link for scan") func deepLinkScan() { let url = URL(string: "kordant://scan")! let route = Route(deepLink: url) #expect(route == .scanComplete) } @Test("Route deep link for invalid scheme returns nil") func deepLinkInvalidScheme() { let url = URL(string: "https://kordant.com/alerts")! let route = Route(deepLink: url) #expect(route == nil) } @Test("Route deep link unknown host returns nil") func deepLinkUnknownHost() { let url = URL(string: "kordant://nonexistent")! let route = Route(deepLink: url) #expect(route == nil) } @Test("Route from notification payload for all types") func routeFromAllNotificationTypes() { // alert var route = Route(notificationPayload: ["screen": "alerts", "id": "a1"]) #expect(route == .alertDetail(id: "a1")) // dashboard route = Route(notificationPayload: ["screen": "dashboard"]) #expect(route == .dashboard) route = Route(notificationPayload: ["screen": "home"]) #expect(route == .dashboard) // settings route = Route(notificationPayload: ["screen": "settings"]) #expect(route == .settings) // darkwatch route = Route(notificationPayload: ["screen": "darkwatch"]) #expect(route == .serviceDetail(id: "darkwatch")) route = Route(notificationPayload: ["screen": "exposure"]) #expect(route == .serviceDetail(id: "darkwatch")) // family route = Route(notificationPayload: ["screen": "family"]) #expect(route == .family) route = Route(notificationPayload: ["screen": "familyInvite"]) #expect(route == .family) // billing route = Route(notificationPayload: ["screen": "billing"]) #expect(route == .billing) route = Route(notificationPayload: ["screen": "subscription"]) #expect(route == .billing) // scan complete route = Route(notificationPayload: ["screen": "scanComplete"]) #expect(route == .scanComplete) } @Test("NotificationDeepLinkRouter routeForPayload maps all types") func routerMapsAllTypes() { let router = NotificationDeepLinkRouter.shared // Alert with ID → alertDetail var payload = NotificationPayload(screen: "alerts", id: "a1", type: .alert) #expect(router.routeForPayload(payload) == .alertDetail(id: "a1")) // Alert without ID → alerts list payload = NotificationPayload(screen: "alerts", type: .alert) #expect(router.routeForPayload(payload) == .alerts) // Exposure → darkwatch payload = NotificationPayload(screen: "exposure", type: .exposure) #expect(router.routeForPayload(payload) == .serviceDetail(id: "darkwatch")) // Scan complete → dashboard payload = NotificationPayload(screen: "scanComplete", type: .scanComplete) #expect(router.routeForPayload(payload) == .dashboard) // Family invite → family payload = NotificationPayload(screen: "familyInvite", type: .familyInvite) #expect(router.routeForPayload(payload) == .family) // Subscription → billing payload = NotificationPayload(screen: "subscriptionRenewal", type: .subscriptionRenewal) #expect(router.routeForPayload(payload) == .billing) // Marketing → dashboard payload = NotificationPayload(screen: "marketing", type: .marketing) #expect(router.routeForPayload(payload) == .dashboard) } @Test("NotificationDeepLinkRouter routeForPayload marketing with feature metadata") func routerMarketingWithFeature() { let router = NotificationDeepLinkRouter.shared // Marketing with feature metadata pointing to a valid screen let payload = NotificationPayload( screen: "marketing", type: .marketing, metadata: ["feature": "alerts"] ) #expect(router.routeForPayload(payload) == .alerts) } @Test("NotificationDeepLinkRouter defers navigation when app not ready") func routerDefersColdStart() async { let payload = NotificationPayload(screen: "alerts", id: "cold-1", type: .alert) // Route with appIsReady = false (cold start) NotificationDeepLinkRouter.shared.route(payload: payload, appIsReady: false) #expect(NotificationDeepLinkRouter.shared.isProcessingColdStart) #expect(NotificationDeepLinkRouter.shared.hasPendingNavigation) // Clear for test isolation NotificationDeepLinkRouter.shared.clearPendingNavigation() #expect(!NotificationDeepLinkRouter.shared.hasPendingNavigation) } } // MARK: - Notification Analytics Tests struct NotificationAnalyticsTests { @Test("NotificationAnalytics tracks delivery") func tracksDelivery() { let analytics = NotificationAnalytics.shared analytics.resetCounts() let payload = NotificationPayload(screen: "alerts", id: "a1", type: .alert) analytics.trackNotificationDelivered(payload: payload) // Should not crash #expect(true) } @Test("NotificationAnalytics tracks open") func tracksOpen() { let analytics = NotificationAnalytics.shared analytics.resetCounts() let payload = NotificationPayload(screen: "alerts", id: "a1", type: .alert) analytics.trackNotificationOpened(payload: payload) #expect(analytics.openCount(for: .alert) == 1) } @Test("NotificationAnalytics tracks conversion") func tracksConversion() { let analytics = NotificationAnalytics.shared analytics.resetCounts() let payload = NotificationPayload(screen: "exposure", id: "e1", type: .exposure) analytics.trackNotificationDelivered(payload: payload) analytics.trackNotificationConversion(payload: payload, action: "viewed") #expect(analytics.conversionCount(for: .exposure) == 1) } @Test("NotificationAnalytics tracks action tap") func tracksAction() { let analytics = NotificationAnalytics.shared let payload = NotificationPayload(screen: "alerts", id: "a1", type: .alert) // Should not crash analytics.trackNotificationAction(payload: payload, actionIdentifier: "RESOLVE_ALERT") #expect(true) } @Test("NotificationAnalytics conversion rate calculation") func conversionRate() { let analytics = NotificationAnalytics.shared analytics.resetCounts() let payload = NotificationPayload(screen: "alerts", id: "a1", type: .alert) analytics.trackNotificationOpened(payload: payload) analytics.trackNotificationConversion(payload: payload, action: "resolved") #expect(analytics.conversionRate(for: .alert) == 1.0) } @Test("NotificationAnalytics conversion rate is 0 with no opens") func conversionRateZero() { let analytics = NotificationAnalytics.shared analytics.resetCounts() #expect(analytics.conversionRate(for: .alert) == 0.0) } @Test("NotificationAnalytics A/B variant assignment") func abVariantAssignment() { let analytics = NotificationAnalytics.shared analytics.assignVariant(notificationType: .alert, variant: "variant_a") #expect(analytics.assignedVariant(for: .alert) == "variant_a") // Unassigned type returns nil #expect(analytics.assignedVariant(for: .exposure) == nil) } } // MARK: - Notification Categories Tests struct NotificationCategorySetupTests { @Test("NotificationCategorySetup registerAll does not crash") func registerAll() { // Should not throw or crash NotificationCategorySetup.registerAll() #expect(true) } @Test("NotificationCategorySetup category identifiers are correct") func categoryIdentifiers() { #expect(NotificationCategoryIdentifier.alert.rawValue == "ALERT") #expect(NotificationCategoryIdentifier.exposure.rawValue == "EXPOSURE") #expect(NotificationCategoryIdentifier.scanComplete.rawValue == "SCAN_COMPLETE") #expect(NotificationCategoryIdentifier.familyInvite.rawValue == "FAMILY_INVITE") #expect(NotificationCategoryIdentifier.subscriptionRenewal.rawValue == "SUBSCRIPTION_RENEWAL") #expect(NotificationCategoryIdentifier.marketing.rawValue == "MARKETING") #expect(NotificationCategoryIdentifier.general.rawValue == "GENERAL") } @Test("NotificationActionIdentifier has all cases") func actionIdentifiers() { #expect(NotificationActionIdentifier.resolve.rawValue == "RESOLVE_ALERT") #expect(NotificationActionIdentifier.dismiss.rawValue == "DISMISS") #expect(NotificationActionIdentifier.viewDetails.rawValue == "VIEW_DETAILS") #expect(NotificationActionIdentifier.remindLater.rawValue == "REMIND_LATER") #expect(NotificationActionIdentifier.acceptInvite.rawValue == "ACCEPT_INVITE") #expect(NotificationActionIdentifier.declineInvite.rawValue == "DECLINE_INVITE") #expect(NotificationActionIdentifier.manageSubscription.rawValue == "MANAGE_SUBSCRIPTION") } @Test("NotificationCategorySetup categoryIdentifier maps types correctly") func categoryIdentifierMapping() { #expect(NotificationCategorySetup.categoryIdentifier(for: .alert) == "ALERT") #expect(NotificationCategorySetup.categoryIdentifier(for: .exposure) == "EXPOSURE") #expect(NotificationCategorySetup.categoryIdentifier(for: .scanComplete) == "SCAN_COMPLETE") #expect(NotificationCategorySetup.categoryIdentifier(for: .familyInvite) == "FAMILY_INVITE") #expect(NotificationCategorySetup.categoryIdentifier(for: .subscriptionRenewal) == "SUBSCRIPTION_RENEWAL") #expect(NotificationCategorySetup.categoryIdentifier(for: .marketing) == "MARKETING") #expect(NotificationCategorySetup.categoryIdentifier(for: .unknown) == "GENERAL") } } // MARK: - Notification Type Preferences Tests struct NotificationTypePreferenceTests { @Test("NotificationTypePreference has default values") func defaultValues() { let pref = NotificationTypePreference(type: .alert) #expect(pref.type == .alert) #expect(pref.isEnabled) #expect(pref.soundEnabled) #expect(pref.badgeEnabled) #expect(pref.id == "alert") } @Test("NotificationPreferences has default for all types") func defaultPreferences() { let prefs = NotificationPreferences.default #expect(prefs.globalEnabled) #expect(prefs.criticalAlertsEnabled) #expect(!prefs.quietHoursEnabled) #expect(prefs.groupByType) #expect(prefs.typePreferences.contains { $0.type == .alert }) #expect(prefs.typePreferences.contains { $0.type == .exposure }) #expect(prefs.typePreferences.contains { $0.type == .scanComplete }) #expect(prefs.typePreferences.contains { $0.type == .familyInvite }) #expect(prefs.typePreferences.contains { $0.type == .subscriptionRenewal }) #expect(prefs.typePreferences.contains { $0.type == .marketing }) // Unknown type should NOT be in default preferences #expect(!prefs.typePreferences.contains { $0.type == .unknown }) } @Test("NotificationPreferences Codable round-trip") func codableRoundTrip() throws { let original = NotificationPreferences.default let data = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(NotificationPreferences.self, from: data) #expect(original == decoded) } } // MARK: - Notification Route (new extension) Tests struct NotificationScreenRouteTests { @Test("Route init from notification screen") func initFromScreen() { #expect(Route(notificationScreen: "dashboard", id: nil) == .dashboard) #expect(Route(notificationScreen: "home", id: nil) == .dashboard) #expect(Route(notificationScreen: "alerts", id: "a1") == .alertDetail(id: "a1")) #expect(Route(notificationScreen: "alerts", id: nil) == .alerts) #expect(Route(notificationScreen: "settings", id: nil) == .settings) #expect(Route(notificationScreen: "family", id: nil) == .family) #expect(Route(notificationScreen: "billing", id: nil) == .billing) #expect(Route(notificationScreen: "scanComplete", id: nil) == .scanComplete) #expect(Route(notificationScreen: "notifications", id: nil) == .notificationSettings) #expect(Route(notificationScreen: "darkwatch", id: nil) == .serviceDetail(id: "darkwatch")) #expect(Route(notificationScreen: "voiceprint", id: nil) == .serviceDetail(id: "voiceprint")) #expect(Route(notificationScreen: "unknown_screen", id: nil) == .serviceDetail(id: "unknown_screen")) } } // MARK: - Token Refresh & Session Management Tests /// Creates a mock JWT token with a custom expiry time. /// The token is base64-encoded JSON payload with standard JWT structure. private func makeMockJWT(expiry: Date) -> String { let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" let payload = "{\"sub\":\"1\",\"exp\":\(Int(expiry.timeIntervalSince1970))}" let headerBase64 = header.data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "=", with: "").replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") let payloadBase64 = payload.data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "=", with: "").replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") let signature = "mock-signature".data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "=", with: "").replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") return "\(headerBase64).\(payloadBase64).\(signature)" } // MARK: - JWT Expiry Calculation Tests struct JWTExpiryTests { @MainActor private func makeService() -> AuthService { AuthService( keychain: MockKeychainService(), apiClient: MockAuthAPIClient() ) } @Test("calculateTokenExpiry parses valid JWT expiry") @MainActor func validExpiry() { let service = makeService() let expiry = Date(timeIntervalSinceNow: 3600) // 1 hour from now let token = makeMockJWT(expiry: expiry) let parsedExpiry = service.calculateTokenExpiry(from: token) #expect(parsedExpiry != nil) #expect(abs(parsedExpiry!.timeIntervalSince(expiry)) < 1.0) } @Test("calculateTokenExpiry returns nil for invalid token") @MainActor func invalidToken() { let service = makeService() let parsedExpiry = service.calculateTokenExpiry(from: "not-a-valid-token") #expect(parsedExpiry == nil) } @Test("calculateTokenExpiry returns nil for token without exp claim") @MainActor func noExpClaim() { let service = makeService() // Create token without exp claim let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" let payload = "{\"sub\":\"1\"}" let headerBase64 = header.data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "=", with: "").replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") let payloadBase64 = payload.data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "=", with: "").replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") let token = "\(headerBase64).\(payloadBase64).mock" let parsedExpiry = service.calculateTokenExpiry(from: token) #expect(parsedExpiry == nil) } @Test("calculateTokenExpiry handles base64url padding") @MainActor func base64urlPadding() { let service = makeService() let expiry = Date(timeIntervalSinceNow: 7200) let token = makeMockJWT(expiry: expiry) let parsedExpiry = service.calculateTokenExpiry(from: token) #expect(parsedExpiry != nil) } } // MARK: - Session State Tests struct SessionStateTests { @MainActor private func makeService() -> AuthService { AuthService( keychain: MockKeychainService(), apiClient: MockAuthAPIClient() ) } @Test("SessionState starts as unauthenticated") @MainActor func initialSessionState() { let service = makeService() #expect(service.sessionState == .unauthenticated) } @Test("currentSessionState returns unauthenticated when not logged in") @MainActor func unauthenticatedState() { let service = makeService() #expect(service.currentSessionState() == .unauthenticated) } @Test("currentSessionState returns valid with expiry after login") @MainActor func validStateAfterLogin() async { let keychain = MockKeychainService() let client = MockAuthAPIClient() client.shouldSucceed = true let service = AuthService(keychain: keychain, apiClient: client) await service.login(email: "test@example.com", password: "password123") #expect(service.state == .authenticated) // The mock token "mock-token" won't parse as JWT, so expiry will be nil let sessionState = service.currentSessionState() #expect(sessionState == .valid(expiry: nil)) } @Test("currentSessionState returns expiring when within buffer") @MainActor func expiringState() { // This tests the logic directly by manipulating internal state let service = makeService() service.state = .authenticated // Set expiry to 3 minutes from now (within 5-min buffer) service.tokenExpiry = Date(timeIntervalSinceNow: 3 * 60) let sessionState = service.currentSessionState() #expect(sessionState == .expiring(expiry: service.tokenExpiry!)) } @Test("currentSessionState returns expired when past expiry") @MainActor func expiredState() { let service = makeService() service.state = .authenticated service.tokenExpiry = Date(timeIntervalSinceNow: -60) // 1 minute ago let sessionState = service.currentSessionState() #expect(sessionState == .expired) } @Test("currentSessionState returns refreshing when refresh in progress") @MainActor func refreshingState() { let service = makeService() service.state = .authenticated service.tokenExpiry = Date(timeIntervalSinceNow: 3600) service.isRefreshing = true let sessionState = service.currentSessionState() #expect(sessionState == .refreshing) } } // MARK: - Token Refresh Logic Tests struct TokenRefreshTests { @MainActor private func makeService(shouldRefreshSucceed: Bool = true) -> (AuthService, MockAuthAPIClient, MockKeychainService) { let keychain = MockKeychainService() let client = MockAuthAPIClient() client.shouldSucceed = shouldRefreshSucceed let service = AuthService(keychain: keychain, apiClient: client) return (service, client, keychain) } @Test("attemptSilentRefresh succeeds and returns true") @MainActor func silentRefreshSuccess() async { let (service, client, keychain) = makeService(shouldRefreshSucceed: true) // Pre-populate refresh token try? keychain.store(key: "refreshToken", value: Data("valid-refresh-token".utf8)) let result = await service.attemptSilentRefresh() #expect(result == true) #expect(client.lastRefreshToken == "valid-refresh-token") } @Test("attemptSilentRefresh fails when no refresh token stored") @MainActor func silentRefreshNoToken() async { let (service, _, _) = makeService(shouldRefreshSucceed: true) let result = await service.attemptSilentRefresh() #expect(result == false) } @Test("attemptSilentRefresh fails when API returns error") @MainActor func silentRefreshAPIError() async { let (service, _, keychain) = makeService(shouldRefreshSucceed: false) // Pre-populate refresh token try? keychain.store(key: "refreshToken", value: Data("expired-refresh-token".utf8)) let result = await service.attemptSilentRefresh() #expect(result == false) } @Test("attemptSilentRefresh prevents concurrent attempts") @MainActor func concurrentRefreshPrevention() async { let (service, _, keychain) = makeService(shouldRefreshSucceed: true) try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) // Simulate refresh in progress service.isRefreshing = true let result = await service.attemptSilentRefresh() #expect(result == false) } @Test("attemptSilentRefresh updates APIClient auth token") @MainActor func silentRefreshUpdatesAuthToken() async { let (service, _, keychain) = makeService(shouldRefreshSucceed: true) try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) APIClient.shared.authToken = "old-token" _ = await service.attemptSilentRefresh() #expect(APIClient.shared.authToken == "mock-token") } @Test("attemptSilentRefresh stores new tokens in keychain") @MainActor func silentRefreshStoresNewTokens() async { let (service, _, keychain) = makeService(shouldRefreshSucceed: true) try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) _ = await service.attemptSilentRefresh() let newJwt = try? keychain.retrieve(key: "jwt") let newRefresh = try? keychain.retrieve(key: "refreshToken") #expect(newJwt == Data("mock-token".utf8)) #expect(newRefresh == Data("new-refresh-token".utf8)) } } // MARK: - TokenRefreshHandler Tests struct TokenRefreshHandlerTests { @MainActor private func makeService() -> (AuthService, MockAuthAPIClient, MockKeychainService) { let keychain = MockKeychainService() let client = MockAuthAPIClient() client.shouldSucceed = true let service = AuthService(keychain: keychain, apiClient: client) return (service, client, keychain) } @Test("handleTokenRefresh succeeds with silent refresh") @MainActor func handleRefreshSuccess() async throws { let (service, _, keychain) = makeService() try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) let token = try await service.handleTokenRefresh() #expect(token == "mock-token") } @Test("handleTokenRefresh throws when refresh fails") @MainActor func handleRefreshFailure() async { let keychain = MockKeychainService() let client = MockAuthAPIClient() client.shouldSucceed = false let service = AuthService(keychain: keychain, apiClient: client) await #expect(throws: APIError.unauthorized) { _ = try await service.handleTokenRefresh() } } @Test("handleSessionExpired triggers onSessionExpired callback") @MainActor func sessionExpiredCallback() { let service = makeService().0 var callbackFired = false service.onSessionExpired = { callbackFired = true } service.handleSessionExpired() #expect(callbackFired) #expect(service.state == .unauthenticated) } } // MARK: - Foreground Refresh Tests struct ForegroundRefreshTests { @MainActor private func makeService() -> (AuthService, MockAuthAPIClient, MockKeychainService) { let keychain = MockKeychainService() let client = MockAuthAPIClient() client.shouldSucceed = true let service = AuthService(keychain: keychain, apiClient: client) return (service, client, keychain) } @Test("refreshOnForeground skips when token is far from expiry") @MainActor func skipWhenFarFromExpiry() async { let (service, client, _) = makeService() service.state = .authenticated service.tokenExpiry = Date(timeIntervalSinceNow: 60 * 60) // 1 hour await service.refreshOnForeground() // Should NOT have called refresh since token is valid for 1 hour #expect(client.lastRefreshToken == nil) } @Test("refreshOnForeground refreshes when within buffer") @MainActor func refreshWhenWithinBuffer() async { let (service, client, keychain) = makeService() service.state = .authenticated service.tokenExpiry = Date(timeIntervalSinceNow: 3 * 60) // 3 minutes (within 5-min buffer) try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) await service.refreshOnForeground() // Should have triggered a refresh #expect(client.lastRefreshToken == "refresh-token") } @Test("refreshOnForeground refreshes when expired") @MainActor func refreshWhenExpired() async { let (service, client, keychain) = makeService() service.state = .authenticated service.tokenExpiry = Date(timeIntervalSinceNow: -60) // 1 minute ago try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) await service.refreshOnForeground() #expect(client.lastRefreshToken == "refresh-token") } @Test("refreshOnForeground refreshes when no expiry known") @MainActor func refreshWhenNoExpiry() async { let (service, client, keychain) = makeService() service.state = .authenticated service.tokenExpiry = nil try? keychain.store(key: "refreshToken", value: Data("refresh-token".utf8)) await service.refreshOnForeground() #expect(client.lastRefreshToken == "refresh-token") } @Test("refreshOnForeground does nothing when unauthenticated") @MainActor func skipWhenUnauthenticated() async { let (service, client, _) = makeService() service.state = .unauthenticated await service.refreshOnForeground() #expect(client.lastRefreshToken == nil) } } // MARK: - Session Expiry Tests struct SessionExpiryTests { @MainActor private func makeService() -> AuthService { AuthService( keychain: MockKeychainService(), apiClient: MockAuthAPIClient() ) } @Test("forceLogout clears all session state") @MainActor func forceLogoutClearsState() { let service = makeService() service.state = .authenticated service.currentUser = User(id: "1", name: "Test", email: "test@example.com") service.sessionState = .valid(expiry: Date(timeIntervalSinceNow: 3600)) service.forceLogout() #expect(service.state == .unauthenticated) #expect(service.sessionState == .unauthenticated) #expect(service.currentUser == nil) #expect(service.tokenExpiry == nil) #expect(service.isRefreshing == false) } @Test("forceLogout calls onSessionExpired callback") @MainActor func forceLogoutCallsCallback() { let service = makeService() var callbackFired = false service.onSessionExpired = { callbackFired = true } service.forceLogout() #expect(callbackFired) } @Test("logout clears APIClient auth token") @MainActor func logoutClearsAuthToken() async { let (service, client, keychain) = ( AuthService(keychain: MockKeychainService(), apiClient: MockAuthAPIClient()), MockAuthAPIClient(), MockKeychainService() ) client.shouldSucceed = true service.state = .authenticated APIClient.shared.authToken = "some-token" service.logout() // Give it time to complete try? await Task.sleep(nanoseconds: 100_000_000) #expect(APIClient.shared.authToken == nil) } } // MARK: - APIClient Token Refresh Interceptor Tests struct APIClientTokenRefreshTests { private func makeSession() -> URLSession { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] return URLSession(configuration: config) } @Test("APIClient queues requests during token refresh") func queuesRequestsDuringRefresh() async throws { let session = makeSession() let client = APIClient(session: session) client.authToken = "expired-token" // Set up a mock refresh handler var refreshCalled = false client.tokenRefreshHandler = object_any: TokenRefreshHandler { // We'll use a simple mock return MockTokenRefreshHandler { refreshCalled = true return "new-token" } } var requestCount = 0 MockURLProtocol.requestHandler = { _ in requestCount += 1 if requestCount == 1 { // First request gets 401 let response = HTTPURLResponse(url: URL(string: "http://test")!, statusCode: 401, httpVersion: nil, headerFields: nil)! return (response, Data()) } // Subsequent requests succeed let response = HTTPURLResponse(url: URL(string: "http://test")!, statusCode: 200, httpVersion: nil, headerFields: nil)! return (response, Data("\"ok\"".utf8)) } do { let _: String = try await client.rawRequest("/test", method: "GET") #expect(refreshCalled) } catch { // If refresh handler isn't set up, it will fail — that's expected in this test // The key thing is that the 401 detection and refresh attempt logic runs } } @Test("APIClient detects 401 and throws unauthorized") func detects401() async throws { let session = makeSession() let client = APIClient(session: session) client.authToken = "expired-token" MockURLProtocol.requestHandler = { _ in let response = HTTPURLResponse(url: URL(string: "http://test")!, statusCode: 401, httpVersion: nil, headerFields: nil)! return (response, Data()) } // Without a refresh handler, the 401 should propagate as unauthorized // (after the failed refresh attempt) do { _ = try await client.rawRequest("/test", method: "GET") #expect(false, "Expected unauthorized error") } catch APIError.unauthorized { #expect(true) } catch { // Refresh failure also acceptable — handler not configured #expect(true) } } @Test("APIClient retry count prevents infinite refresh loops") func noInfiniteRefreshLoop() async throws { let session = makeSession() let config = APIConfig(maxRetries: 3) let client = APIClient(config: config, session: session) client.authToken = "expired-token" var attemptCount = 0 MockURLProtocol.requestHandler = { _ in attemptCount += 1 let response = HTTPURLResponse(url: URL(string: "http://test")!, statusCode: 401, httpVersion: nil, headerFields: nil)! return (response, Data()) } do { _ = try await client.rawRequest("/test", method: "GET") } catch { // Should not loop infinitely — max attempts is respected #expect(attemptCount <= 3) } } } // MARK: - Mock Token Refresh Handler private class MockTokenRefreshHandler: TokenRefreshHandler { private let handler: () -> String init(_ handler: @escaping () -> String) { self.handler = handler } func handleTokenRefresh() async throws -> String { handler() } func handleSessionExpired() { // No-op for tests } } // MARK: - Session Restoration Tests struct SessionRestorationTests { @MainActor private func makeService() -> (AuthService, MockAuthAPIClient, MockKeychainService) { let keychain = MockKeychainService() let client = MockAuthAPIClient() let service = AuthService(keychain: keychain, apiClient: client) return (service, client, keychain) } @Test("restoreSession restores authenticated state from keychain") @MainActor func restoreAuthenticatedState() { let (service, _, keychain) = makeService() // Simulate stored session try? keychain.store(key: "jwt", value: Data("stored-token".utf8)) let userData = try! JSONEncoder().encode(User(id: "1", name: "Test", email: "test@example.com")) try? keychain.store(key: "currentUser", value: userData) service.restoreSession() #expect(service.state == .authenticated) #expect(service.currentUser?.id == "1") } @Test("restoreSession stays unauthenticated without stored token") @MainActor func restoreNoToken() { let (service, _, _) = makeService() service.restoreSession() #expect(service.state == .unauthenticated) #expect(service.sessionState == .unauthenticated) } @Test("restoreSession sets APIClient auth token") @MainActor func restoreSetsAuthToken() { let (service, _, keychain) = makeService() try? keychain.store(key: "jwt", value: Data("stored-token".utf8)) service.restoreSession() #expect(APIClient.shared.authToken == "stored-token") } @Test("restoreSession restores token expiry") @MainActor func restoreTokenExpiry() { let (service, _, keychain) = makeService() try? keychain.store(key: "jwt", value: Data("stored-token".utf8)) let expiry = Date(timeIntervalSinceNow: 3600) let formatter = ISO8601DateFormatter() try? keychain.store(key: "tokenExpiry", value: Data(formatter.string(from: expiry).utf8)) service.restoreSession() #expect(service.tokenExpiry != nil) #expect(abs(service.tokenExpiry!.timeIntervalSince(expiry)) < 1.0) } } // MARK: - Session Callback Tests struct SessionCallbackTests { @MainActor private func makeService() -> AuthService { AuthService( keychain: MockKeychainService(), apiClient: MockAuthAPIClient() ) } @Test("onSessionExpiring callback fires when scheduling near expiry") @MainActor func onSessionExpiringCallback() { let service = makeService() var callbackFired = false service.onSessionExpiring = { callbackFired = true } // Schedule a refresh with an expiry that's within the buffer let expiry = Date(timeIntervalSinceNow: 4 * 60) // 4 minutes (within 5-min buffer) service.scheduleTokenRefresh(expiry: expiry) #expect(callbackFired) } @Test("onSessionExpiring callback does not fire when far from expiry") @MainActor func noExpiringCallbackWhenFar() { let service = makeService() var callbackFired = false service.onSessionExpiring = { callbackFired = true } // Schedule a refresh with an expiry far in the future let expiry = Date(timeIntervalSinceNow: 60 * 60) // 1 hour service.scheduleTokenRefresh(expiry: expiry) #expect(!callbackFired) } }