- 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
531 lines
20 KiB
Swift
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)
|
|
}
|
|
}
|