import Testing @testable import Kordant import AppIntents import Foundation // MARK: - Mock TRPCalling for Intent Tests final class IntentMockTRPCalling: TRPCalling { var shouldSucceed = true var stubbedAlerts: [Alert] = [] var stubbedExposures: [Exposure] = [] var stubbedWatchlist: [WatchlistItem] = [] var stubbedSpamResult = SpamCheckResult( phone: "+15551234567", isSpam: true, confidence: 0.92, category: "telemarketer", reportCount: 156 ) var addedWatchlistTerm: String? var addedWatchlistType: WatchlistItemType? var scannedExposures: [Exposure]? var errorToThrow: Error? init() {} func callProcedure(path: String, input: (any Encodable)?) async throws -> T { if let error = errorToThrow { throw error } throw APIError.notImplemented } func userMe() async throws -> User { throw APIError.notImplemented } func getSubscription() async throws -> Subscription { throw APIError.notImplemented } func getWatchlist() async throws -> [WatchlistItem] { if let error = errorToThrow { throw error } return stubbedWatchlist } func getExposures() async throws -> [Exposure] { if let error = errorToThrow { throw error } return stubbedExposures } func getAlerts() async throws -> [Alert] { if let error = errorToThrow { throw error } return stubbedAlerts } func getVoiceEnrollments() async throws -> [VoiceEnrollment] { throw APIError.notImplemented } func getVoiceAnalyses() async throws -> [VoiceAnalysis] { throw APIError.notImplemented } func getSpamRules() async throws -> [SpamRule] { throw APIError.notImplemented } func getPropertyWatchlist() async throws -> [PropertyWatchlistItem] { throw APIError.notImplemented } func getRemovalRequests() async throws -> [RemovalRequest] { throw APIError.notImplemented } func getBrokerListings() async throws -> [BrokerListing] { throw APIError.notImplemented } func getNormalizedAlerts() async throws -> [NormalizedAlert] { throw APIError.notImplemented } func getCorrelationGroups() async throws -> [CorrelationGroup] { throw APIError.notImplemented } func getSecurityReports() async throws -> [SecurityReport] { throw APIError.notImplemented } func addWatchlistItem(term: String, type: WatchlistItemType) async throws -> WatchlistItem { if let error = errorToThrow { throw error } addedWatchlistTerm = term addedWatchlistType = type return WatchlistItem(id: "new-1", userId: "1", term: term, type: type, status: "active", createdAt: nil) } func deleteWatchlistItem(id: String) async throws { if let error = errorToThrow { throw error } } func scanForExposures() async throws -> [Exposure] { if let error = errorToThrow { throw error } return scannedExposures ?? [] } func deleteVoiceEnrollment(id: String) async throws { if let error = errorToThrow { throw error } } func createSpamRule(pattern: String, action: SpamRuleAction, priority: Int, enabled: Bool) async throws -> SpamRule { throw APIError.notImplemented } func updateSpamRule(id: String, enabled: Bool) async throws -> SpamRule { throw APIError.notImplemented } func deleteSpamRule(id: String) async throws { throw APIError.notImplemented } func addProperty(address: String, city: String, state: String, zipCode: String) async throws -> PropertyWatchlistItem { throw APIError.notImplemented } func deleteProperty(id: String) async throws { throw APIError.notImplemented } func startRemoval(exposureId: String, notes: String?) async throws -> RemovalRequest { throw APIError.notImplemented } func checkPhoneNumber(_ number: String) async throws -> SpamCheckResult { if let error = errorToThrow { throw error } return stubbedSpamResult } func resolveAlert(id: String) async throws { if let error = errorToThrow { throw error } } func reportFalsePositive(id: String) async throws { if let error = errorToThrow { throw error } } func updateNotificationPreferences(enabled: Bool) async throws { if let error = errorToThrow { throw error } } func updateProfile(name: String, email: String) async throws -> User { throw APIError.notImplemented } func registerDevice(token: String) async throws { if let error = errorToThrow { throw error } } func createVoiceEnrollment(audioData: Data) async throws -> VoiceEnrollment { throw APIError.notImplemented } // Call Recording func analyzeCallRecording(input: AnalyzeCallRecordingInput) async throws -> CallAudioUploader.CallAnalysisResult { throw APIError.notImplemented } func getCallAnalyses(page: Int, limit: Int, status: String?) async throws -> CallAnalysisListResponse { throw APIError.notImplemented } func getCallAnalysis(callRecordingId: String) async throws -> CallRecord { throw APIError.notImplemented } func getCallAnalysisSettings() async throws -> CallAnalysisSettings { throw APIError.notImplemented } func updateCallAnalysisSettings(_ settings: CallAnalysisSettings) async throws -> CallAnalysisSettings { throw APIError.notImplemented } func emergencyHangup(callRecordingId: String, phoneNumber: String) async throws -> EmergencyHangupResult { throw APIError.notImplemented } // Security Events func reportSecurityEvent( eventType: SecurityEventType, severity: SecuritySeverity, indicators: [String], violations: [String], deviceInfo: DeviceSecurityInfo ) async throws { if let error = errorToThrow { throw error } } } // MARK: - CheckThreatScoreIntent Tests @MainActor struct CheckThreatScoreIntentTests { private func makeMock(alerts: [Alert] = [], exposures: [Exposure] = []) -> IntentMockTRPCalling { let mock = IntentMockTRPCalling() mock.stubbedAlerts = alerts mock.stubbedExposures = exposures return mock } @Test("CheckThreatScoreIntent returns low score with no alerts") func lowScoreNoAlerts() async throws { // Tests the threat score calculation logic used by CheckThreatScoreIntent let mock = makeMock(alerts: [], exposures: []) let alerts = mock.stubbedAlerts let exposures = mock.stubbedExposures let unreadCritical = alerts.filter { !$0.read && $0.isCritical }.count let newExposures = exposures.filter { $0.status == .new }.count let score = min(Double(unreadCritical * 25 + newExposures * 10), 100) #expect(score == 0) #expect(alerts.isEmpty) #expect(exposures.isEmpty) } @Test("CheckThreatScoreIntent calculates score with critical alerts") func scoreWithCriticalAlerts() async throws { let mock = makeMock( alerts: [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "Critical", message: "!", read: false, createdAt: nil) ], exposures: [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: nil, severity: "high", discoveredAt: nil, status: .new) ] ) let unreadCritical = mock.stubbedAlerts.filter { !$0.read && $0.isCritical }.count let newExposures = mock.stubbedExposures.filter { $0.status == .new }.count let score = min(Double(unreadCritical * 25 + newExposures * 10), 100) #expect(unreadCritical == 1) #expect(newExposures == 1) #expect(score == 35) } @Test("CheckThreatScoreIntent caps at 100") func scoreCappedAt100() async throws { let mock = makeMock( alerts: [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil), Alert(id: "2", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil), Alert(id: "3", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil), Alert(id: "4", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil) ], exposures: [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: nil, severity: "high", discoveredAt: nil, status: .new), Exposure(id: "2", userId: "1", source: .darkWeb, dataType: "email", exposedData: nil, severity: "high", discoveredAt: nil, status: .new) ] ) let unreadCritical = mock.stubbedAlerts.filter { !$0.read && $0.isCritical }.count let newExposures = mock.stubbedExposures.filter { $0.status == .new }.count let score = min(Double(unreadCritical * 25 + newExposures * 10), 100) #expect(score == 100) } @Test("CheckThreatScoreIntent intent parameter default is includeDetails") func intentParameterDefaults() { let _ = CheckThreatScoreIntent() #expect(Bool(true)) } } // MARK: - CheckAlertsIntent Tests @MainActor struct CheckAlertsIntentTests { @Test("CheckAlertsIntent returns no alerts when empty") func noAlerts() async throws { let mock = IntentMockTRPCalling() mock.stubbedAlerts = [] let alerts = try await mock.getAlerts() let unreadAlerts = alerts.filter { !$0.read } #expect(unreadAlerts.isEmpty) #expect(alerts.isEmpty) } @Test("CheckAlertsIntent returns unread alerts") func unreadAlerts() async throws { let mock = IntentMockTRPCalling() mock.stubbedAlerts = [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "Data Breach", message: "Your data was exposed", read: false, createdAt: nil), Alert(id: "2", userId: "1", type: .exposure, severity: .low, title: "Exposure", message: "Email found", read: false, createdAt: nil), Alert(id: "3", userId: "1", type: .login, severity: .low, title: "Read Alert", message: "Already seen", read: true, createdAt: nil) ] let alerts = try await mock.getAlerts() let unreadAlerts = alerts.filter { !$0.read } #expect(alerts.count == 3) #expect(unreadAlerts.count == 2) #expect(unreadAlerts[0].title == "Data Breach") #expect(unreadAlerts[1].title == "Exposure") } @Test("CheckAlertsIntent only critical filter") func onlyCriticalFilter() async throws { let mock = IntentMockTRPCalling() mock.stubbedAlerts = [ Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "Critical", message: "!", read: false, createdAt: nil), Alert(id: "2", userId: "1", type: .exposure, severity: .medium, title: "Medium", message: "?", read: false, createdAt: nil), Alert(id: "3", userId: "1", type: .exposure, severity: .low, title: "Low", message: ".", read: false, createdAt: nil), Alert(id: "4", userId: "1", type: .breach, severity: .critical, title: "Critical 2", message: "!", read: true, createdAt: nil) ] let criticalUnread = mock.stubbedAlerts.filter { $0.isCritical && !$0.read } #expect(criticalUnread.count == 1) #expect(criticalUnread[0].title == "Critical") } } // MARK: - AddWatchlistItemIntent Tests @MainActor struct AddWatchlistItemIntentTests { @Test("AddWatchlistItemIntent adds email to watchlist") func addEmail() async throws { let mock = IntentMockTRPCalling() let term = "user@example.com" let type = WatchlistItemType.email let item = try await mock.addWatchlistItem(term: term, type: type) #expect(item.term == "user@example.com") #expect(item.type == .email) #expect(mock.addedWatchlistTerm == term) #expect(mock.addedWatchlistType == type) } @Test("AddWatchlistItemIntent adds phone to watchlist") func addPhone() async throws { let mock = IntentMockTRPCalling() let term = "+15551234567" let item = try await mock.addWatchlistItem(term: term, type: .phone) #expect(item.term == term) #expect(item.type == .phone) } @Test("AddWatchlistItemIntent adds name to watchlist") func addName() async throws { let mock = IntentMockTRPCalling() let term = "John Doe" let item = try await mock.addWatchlistItem(term: term, type: .name) #expect(item.term == term) #expect(item.type == .name) } @Test("AddWatchlistItemIntent throws on empty term") func emptyTerm() async throws { let trimmed = " ".trimmingCharacters(in: .whitespacesAndNewlines) #expect(trimmed.isEmpty) } } // MARK: - CheckSpamNumberIntent Tests @MainActor struct CheckSpamNumberIntentTests { @Test("CheckSpamNumberIntent returns spam result") func spamCheck() async throws { let mock = IntentMockTRPCalling() mock.stubbedSpamResult = SpamCheckResult( phone: "+15551234567", isSpam: true, confidence: 0.92, category: "telemarketer", reportCount: 156 ) let result = try await mock.checkPhoneNumber("+15551234567") #expect(result.isSpam == true) #expect(result.phone == "+15551234567") #expect(result.confidence == 0.92) #expect(result.category == "telemarketer") #expect(result.reportCount == 156) } @Test("CheckSpamNumberIntent returns safe for unknown number") func safeNumber() async throws { let mock = IntentMockTRPCalling() mock.stubbedSpamResult = SpamCheckResult( phone: "+15559876543", isSpam: false, confidence: 0.0, category: nil, reportCount: 0 ) let result = try await mock.checkPhoneNumber("+15559876543") #expect(result.isSpam == false) #expect(result.confidence == 0.0) #expect(result.category == nil) #expect(result.reportCount == 0) } @Test("CheckSpamNumberIntent phone number cleaning") func phoneCleaning() { let raw = " (555) 123-4567 " let cleaned = raw .trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: CharacterSet.decimalDigits.inverted) .joined() #expect(cleaned == "5551234567") #expect(!cleaned.isEmpty) } @Test("CheckSpamNumberIntent handles invalid phone number") func invalidPhone() { let raw = "abc" let cleaned = raw .trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: CharacterSet.decimalDigits.inverted) .joined() #expect(cleaned.isEmpty) } } // MARK: - RunSecurityScanIntent Tests @MainActor struct RunSecurityScanIntentTests { @Test("RunSecurityScanIntent returns exposures after scan") func scanReturnsExposures() async throws { let mock = IntentMockTRPCalling() mock.scannedExposures = [ Exposure(id: "1", userId: "1", source: .darkWeb, dataType: "email", exposedData: "test@example.com", severity: "high", discoveredAt: nil, status: .new) ] let exposures = try await mock.scanForExposures() #expect(exposures.count == 1) #expect(exposures[0].dataType == "email") } @Test("RunSecurityScanIntent returns empty when no exposures") func scanReturnsEmpty() async throws { let mock = IntentMockTRPCalling() mock.scannedExposures = [] let exposures = try await mock.scanForExposures() #expect(exposures.isEmpty) } } // MARK: - WatchlistItemTypeEnum Tests struct WatchlistItemTypeEnumTests { @Test("WatchlistItemTypeEnum maps to model type correctly") func enumMapping() { #expect(WatchlistItemTypeEnum.email.toModelType() == .email) #expect(WatchlistItemTypeEnum.phone.toModelType() == .phone) #expect(WatchlistItemTypeEnum.name.toModelType() == .name) #expect(WatchlistItemTypeEnum.ssn.toModelType() == .ssn) #expect(WatchlistItemTypeEnum.address.toModelType() == .address) #expect(WatchlistItemTypeEnum.domain.toModelType() == .domain) #expect(WatchlistItemTypeEnum.username.toModelType() == .username) } @Test("WatchlistItemTypeEnum has all expected cases") func allCases() { let allCases: Set = [ .email, .phone, .name, .ssn, .address, .domain, .username ] #expect(allCases.count == 7) } } // MARK: - IntentDonationManager Tests @MainActor struct IntentDonationManagerTests { @Test("KordantIntentDonationManager singleton") func singleton() { let manager1 = KordantIntentDonationManager.shared let manager2 = KordantIntentDonationManager.shared #expect(manager1 === manager2) } @Test("KordantIntentDonationManager has all 5 intents") func allIntentsCount() { let manager = KordantIntentDonationManager.shared #expect(manager.allIntents.count == 5) } @Test("KordantIntentDonationManager all intents have titles") func allIntentsHaveTitles() { let manager = KordantIntentDonationManager.shared for intent in manager.allIntents { #expect(!intent.title.isEmpty) #expect(!intent.description.isEmpty) #expect(!intent.icon.isEmpty) } } @Test("DonatedIntent raw values match donation keys") func donatedIntentRawValues() { #expect(KordantIntentDonationManager.DonatedIntent.checkThreatScore.rawValue == "checkThreatScore") #expect(KordantIntentDonationManager.DonatedIntent.runSecurityScan.rawValue == "runSecurityScan") #expect(KordantIntentDonationManager.DonatedIntent.checkAlerts.rawValue == "checkAlerts") #expect(KordantIntentDonationManager.DonatedIntent.addWatchlistItem.rawValue == "addWatchlistItem") #expect(KordantIntentDonationManager.DonatedIntent.checkSpamNumber.rawValue == "checkSpamNumber") } @Test("DonateOnFirstLaunch donates expected intents") func donateOnFirstLaunch() { let manager = KordantIntentDonationManager.shared manager.donateOnFirstLaunch() #expect(true) } @Test("DonateAfterOnboarding donates expected intents") func donateAfterOnboarding() { let manager = KordantIntentDonationManager.shared manager.donateAfterOnboarding() #expect(true) } } // MARK: - App Shortcuts Provider Tests struct AppShortcutsProviderTests { @Test("KordantShortcutsProvider provides all 5 app shortcuts") func providerHasAllShortcuts() { #expect(KordantShortcutsProvider.self is AppShortcutsProvider.Type) } } // MARK: - KordantIntentError Tests struct KordantIntentErrorTests { @Test("KordantIntentError has correct localized strings") func localizedStrings() { #expect(KordantIntentError.notAuthenticated.localizedStringResource == "You need to sign in to Kordant first.") #expect(KordantIntentError.invalidInput("test").localizedStringResource == "Invalid input: test") #expect(KordantIntentError.networkError("timeout").localizedStringResource == "Network error: timeout") #expect(KordantIntentError.operationFailed("error").localizedStringResource == "Operation failed: error") } @Test("KordantIntentError conforms to Error") func conformsToError() { let error: any Error = KordantIntentError.notAuthenticated #expect(error is KordantIntentError) } } // MARK: - Alert Model Tests for Intents struct AlertModelForIntentsTests { @Test("Alert isCritical computed property works") func isCriticalProperty() { let critical = Alert(id: "1", userId: "1", type: .breach, severity: .critical, title: "!", message: "!", read: false, createdAt: nil) let nonCritical = Alert(id: "2", userId: "1", type: .exposure, severity: .low, title: "?", message: "?", read: false, createdAt: nil) #expect(critical.isCritical == true) #expect(nonCritical.isCritical == false) } @Test("Alert severities are comparable") func severityComparable() { #expect(AlertSeverity.low.rawValue < AlertSeverity.critical.rawValue) } }