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
This commit is contained in:
229
iOS/KordantTests/ATTServiceTests.swift
Normal file
229
iOS/KordantTests/ATTServiceTests.swift
Normal file
@@ -0,0 +1,229 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user