Files
Kordant/iOS/KordantTests/KordantTests.swift
2026-05-25 23:23:27 -04:00

1915 lines
64 KiB
Swift

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