- 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
230 lines
7.4 KiB
Swift
230 lines
7.4 KiB
Swift
import Testing
|
|
@testable import Kordant
|
|
import AppTrackingTransparency
|
|
|
|
// MARK: - ATTService Tests
|
|
|
|
struct ATTServiceTests {
|
|
|
|
/// Creates an ATTService instance with an isolated UserDefaults suite for testing.
|
|
@MainActor
|
|
private func makeService() -> ATTService {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
return ATTService(defaults: defaults)
|
|
}
|
|
|
|
// MARK: - Initial State
|
|
|
|
@Test("ATTService starts with notDetermined status and no permission requested")
|
|
@MainActor
|
|
func initialState() {
|
|
let service = makeService()
|
|
#expect(service.trackingStatus == .notDetermined)
|
|
#expect(service.hasRequestedPermission == false)
|
|
#expect(service.hasShownExplanation == false)
|
|
#expect(service.analyticsMode == .anonymous)
|
|
#expect(service.shouldShowATTPrompt() == true)
|
|
#expect(service.shouldShowExplanation() == true)
|
|
}
|
|
|
|
@Test("ATTService restores persisted permission-requested state")
|
|
@MainActor
|
|
func persistedRequestState() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
defaults.set(true, forKey: "kordant.att.requested")
|
|
let service = ATTService(defaults: defaults)
|
|
|
|
#expect(service.hasRequestedPermission == true)
|
|
#expect(service.shouldShowATTPrompt() == false)
|
|
}
|
|
|
|
@Test("ATTService restores persisted explanation-shown state")
|
|
@MainActor
|
|
func persistedExplanationState() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
defaults.set(true, forKey: "kordant.att.explanationShown")
|
|
let service = ATTService(defaults: defaults)
|
|
|
|
#expect(service.hasShownExplanation == true)
|
|
#expect(service.shouldShowExplanation() == false)
|
|
}
|
|
|
|
// MARK: - Authorization Status Checks
|
|
|
|
@Test("ATTService isTrackingAuthorized returns true only for .authorized")
|
|
@MainActor
|
|
func trackingAuthorized() {
|
|
let service = makeService()
|
|
#expect(service.isTrackingAuthorized() == false)
|
|
|
|
// Simulate authorized state
|
|
service.trackingStatus = .authorized
|
|
#expect(service.isTrackingAuthorized() == true)
|
|
|
|
service.trackingStatus = .denied
|
|
#expect(service.isTrackingAuthorized() == false)
|
|
}
|
|
|
|
@Test("ATTService isTrackingDenied returns true only for .denied")
|
|
@MainActor
|
|
func trackingDenied() {
|
|
let service = makeService()
|
|
|
|
service.trackingStatus = .denied
|
|
#expect(service.isTrackingDenied() == true)
|
|
|
|
service.trackingStatus = .authorized
|
|
#expect(service.isTrackingDenied() == false)
|
|
}
|
|
|
|
@Test("ATTService isTrackingRestricted returns true only for .restricted")
|
|
@MainActor
|
|
func trackingRestricted() {
|
|
let service = makeService()
|
|
|
|
service.trackingStatus = .restricted
|
|
#expect(service.isTrackingRestricted() == true)
|
|
|
|
service.trackingStatus = .authorized
|
|
#expect(service.isTrackingRestricted() == false)
|
|
}
|
|
|
|
// MARK: - Analytics Mode
|
|
|
|
@Test("ATTService analytics mode is anonymous for all non-authorized states")
|
|
@MainActor
|
|
func analyticsModeForDenied() {
|
|
let service = makeService()
|
|
|
|
service.trackingStatus = .denied
|
|
service.refreshStatus()
|
|
#expect(service.analyticsMode == .anonymous)
|
|
#expect(service.analyticsMode.usesIDFA == false)
|
|
}
|
|
|
|
@Test("ATTService analytics mode is anonymous for restricted state")
|
|
@MainActor
|
|
func analyticsModeForRestricted() {
|
|
let service = makeService()
|
|
|
|
service.trackingStatus = .restricted
|
|
service.refreshStatus()
|
|
#expect(service.analyticsMode == .anonymous)
|
|
#expect(service.analyticsMode.usesIDFA == false)
|
|
}
|
|
|
|
@Test("ATTService analytics mode is full for authorized state")
|
|
@MainActor
|
|
func analyticsModeForAuthorized() {
|
|
let service = makeService()
|
|
|
|
service.trackingStatus = .authorized
|
|
service.refreshStatus()
|
|
#expect(service.analyticsMode == .full)
|
|
#expect(service.analyticsMode.usesIDFA == true)
|
|
}
|
|
|
|
@Test("ATTService analytics mode is anonymous for notDetermined")
|
|
@MainActor
|
|
func analyticsModeForNotDetermined() {
|
|
let service = makeService()
|
|
#expect(service.analyticsMode == .anonymous)
|
|
#expect(service.analyticsMode.usesIDFA == false)
|
|
}
|
|
|
|
// MARK: - Prompt Logic
|
|
|
|
@Test("ATTService shouldShowATTPrompt returns false after permission requested")
|
|
@MainActor
|
|
func promptNotShownAfterRequest() {
|
|
let service = makeService()
|
|
#expect(service.shouldShowATTPrompt() == true)
|
|
|
|
service.hasRequestedPermission = true
|
|
#expect(service.shouldShowATTPrompt() == false)
|
|
}
|
|
|
|
@Test("ATTService shouldShowATTPrompt returns false if already authorized")
|
|
@MainActor
|
|
func promptNotShownIfAuthorized() {
|
|
let service = makeService()
|
|
service.trackingStatus = .authorized
|
|
#expect(service.shouldShowATTPrompt() == false)
|
|
}
|
|
|
|
@Test("ATTService shouldShowATTPrompt returns false if denied previously")
|
|
@MainActor
|
|
func promptNotShownIfDenied() {
|
|
let service = makeService()
|
|
service.hasRequestedPermission = true
|
|
service.trackingStatus = .denied
|
|
#expect(service.shouldShowATTPrompt() == false)
|
|
}
|
|
|
|
@Test("ATTService shouldShowExplanation returns true only before explanation shown")
|
|
@MainActor
|
|
func explanationLogic() {
|
|
let service = makeService()
|
|
#expect(service.shouldShowExplanation() == true)
|
|
|
|
service.markExplanationShown()
|
|
#expect(service.shouldShowExplanation() == false)
|
|
#expect(service.hasShownExplanation == true)
|
|
}
|
|
|
|
// MARK: - State Management
|
|
|
|
@Test("ATTService markExplanationShown persists state")
|
|
@MainActor
|
|
func markExplanationPersists() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let service = ATTService(defaults: defaults)
|
|
|
|
service.markExplanationShown()
|
|
|
|
// Create a new instance and verify persistence
|
|
let service2 = ATTService(defaults: defaults)
|
|
#expect(service2.hasShownExplanation == true)
|
|
}
|
|
|
|
@Test("ATTService refreshStatus updates tracking status")
|
|
@MainActor
|
|
func refreshStatus() {
|
|
let service = makeService()
|
|
#expect(service.trackingStatus == .notDetermined)
|
|
|
|
// Simulate status change (in a real scenario the system updates this)
|
|
service.trackingStatus = .denied
|
|
#expect(service.analyticsMode == .denied) // indirect: analyticsMode should become anonymous
|
|
}
|
|
|
|
@Test("ATTService resetPermissionState clears request flag")
|
|
@MainActor
|
|
func resetPermissionState() {
|
|
let defaults = UserDefaults(suiteName: UUID().uuidString)!
|
|
let service = ATTService(defaults: defaults)
|
|
|
|
service.hasRequestedPermission = true
|
|
defaults.set(true, forKey: "kordant.att.requested")
|
|
|
|
service.resetPermissionState()
|
|
#expect(service.hasRequestedPermission == false)
|
|
}
|
|
}
|
|
|
|
// MARK: - AnalyticsMode Tests
|
|
|
|
struct AnalyticsModeTests {
|
|
@Test("AnalyticsMode usesIDFA is true only for full mode")
|
|
func usesIDFA() {
|
|
#expect(AnalyticsMode.anonymous.usesIDFA == false)
|
|
#expect(AnalyticsMode.full.usesIDFA == true)
|
|
}
|
|
|
|
@Test("AnalyticsMode raw values are correct")
|
|
func rawValues() {
|
|
#expect(AnalyticsMode.anonymous.rawValue == "anonymous")
|
|
#expect(AnalyticsMode.full.rawValue == "full")
|
|
}
|
|
}
|