Files
Kordant/iOS/KordantTests/SiriIntentsTests.swift
Michael Freno e33ddf3002 feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI
- Add Apple Sign-In backend (JWKS verification, account linking, session management)
- Implement push notification deep linking with NotificationDeepLinkRouter
- Add jailbreak detection, runtime integrity monitoring, secure enclave service
- Implement OAuth social login, token refresh, and secure logout flows
- Add image caching (memory/disk), optimizer, upload queue, async semaphore
- Implement notification analytics, type preferences, and category setup
- Expand UI test suite with UITestBase, accessibility, auth flow, performance tests
- Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks
- Restructure Xcode project to manual groups with KordantWidgets target
- Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies
- Update project.yml for XcodeGen with new targets and configurations
2026-06-02 15:01:38 -04:00

531 lines
20 KiB
Swift

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<T: Decodable>(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<WatchlistItemTypeEnum> = [
.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)
}
}