199 lines
7.2 KiB
Swift
199 lines
7.2 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindInfrastructure
|
|
import Testing
|
|
|
|
@Suite @MainActor
|
|
struct DiagnosisTests {
|
|
@Test
|
|
func firstLaunchWaitsForAnExplicitChoice() async throws {
|
|
try await withDefaults { defaults in
|
|
let reporter = RecordingReporter()
|
|
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
|
|
#expect(controller.needsConsent)
|
|
#expect(!controller.isEnabled)
|
|
#expect(controller.consent == nil)
|
|
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) == nil)
|
|
#expect(reporter.values == [false])
|
|
}
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func bothChoicesPersistWithoutAskingAgain(enabled: Bool) async throws {
|
|
try await withDefaults { defaults in
|
|
let reporter = RecordingReporter()
|
|
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
|
|
controller.setEnabled(enabled)
|
|
controller.setEnabled(enabled)
|
|
#expect(!controller.needsConsent)
|
|
#expect(controller.isEnabled == enabled)
|
|
#expect(reporter.values == [false, enabled])
|
|
let nextLaunch = DiagnosisController(defaults: defaults, reporter: reporter)
|
|
#expect(!nextLaunch.needsConsent)
|
|
#expect(nextLaunch.consent == enabled)
|
|
#expect(reporter.values == [false, enabled, enabled])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func revocationPersistsBeforeApplyingItToTheSDK() async throws {
|
|
try await withDefaults { defaults in
|
|
defaults.set(true, forKey: "diagnostics.crashReportingConsent")
|
|
let reporter = RecordingReporter()
|
|
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
|
|
reporter.didChange = { enabled in
|
|
#expect(defaults.object(forKey: SettingsKey.diagnosisConsent) as? Bool == enabled)
|
|
}
|
|
controller.setEnabled(false)
|
|
#expect(reporter.values == [true, false])
|
|
#expect(!DiagnosisController(defaults: defaults).needsConsent)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testAndPreviewControllersDoNotPresentConsent() {
|
|
#expect(!DiagnosisController().needsConsent)
|
|
#expect(!DiagnosisController().isEnabled)
|
|
}
|
|
|
|
@Test
|
|
func unknownAndDeclinedConsentNeverInitializeFirebase() async throws {
|
|
try await withDefaults { defaults in
|
|
var initializations = 0
|
|
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) {
|
|
initializations += 1
|
|
return nil
|
|
}
|
|
let controller = DiagnosisController(defaults: defaults, reporter: reporter)
|
|
controller.setEnabled(false)
|
|
#expect(initializations == 0)
|
|
controller.setEnabled(true)
|
|
#expect(initializations == 1)
|
|
// A missing developer configuration does not lose the user's choice or block startup.
|
|
#expect(controller.isEnabled && !controller.needsConsent)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func optedInLaunchSendsPendingDiagnosticDataOnce() async throws {
|
|
try await withDefaults { defaults in
|
|
let client = DeferredDiagnosticDataClient()
|
|
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
|
|
reporter.setEnabled(true)
|
|
await client.waitForCheck()
|
|
client.finishCheck(hasData: true)
|
|
await client.waitForAction()
|
|
reporter.setEnabled(true)
|
|
#expect(client.sends == 1)
|
|
#expect(client.deletes == 0)
|
|
#expect(client.checks == 1)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func consentRevokedDuringSDKCheckDeletesInsteadOfUploading() async throws {
|
|
try await withDefaults { defaults in
|
|
let client = DeferredDiagnosticDataClient()
|
|
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
|
|
reporter.setEnabled(true)
|
|
await client.waitForCheck()
|
|
reporter.setEnabled(false)
|
|
client.finishCheck(hasData: true)
|
|
await client.waitForAction()
|
|
#expect(client.sends == 0)
|
|
#expect(client.deletes == 1)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func optingBackInDiscardsDiagnosticDataFromBeforeConsentWasRestored() async throws {
|
|
try await withDefaults { defaults in
|
|
let previousLaunch = FirebaseDiagnosticDataReporter(defaults: defaults) { nil }
|
|
previousLaunch.setEnabled(false)
|
|
#expect(defaults.bool(forKey: "diagnostics.discardPendingCrashReports"))
|
|
let client = DeferredDiagnosticDataClient()
|
|
let reporter = FirebaseDiagnosticDataReporter(defaults: defaults) { client }
|
|
reporter.setEnabled(true)
|
|
await client.waitForCheck()
|
|
client.finishCheck(hasData: true)
|
|
await client.waitForAction()
|
|
#expect(client.sends == 0 && client.deletes == 1)
|
|
|
|
let nextClient = DeferredDiagnosticDataClient()
|
|
let nextLaunch = FirebaseDiagnosticDataReporter(defaults: defaults) { nextClient }
|
|
nextLaunch.setEnabled(true)
|
|
await nextClient.waitForCheck()
|
|
nextClient.finishCheck(hasData: true)
|
|
await nextClient.waitForAction()
|
|
#expect(nextClient.sends == 1 && nextClient.deletes == 0)
|
|
}
|
|
}
|
|
|
|
private func withDefaults(_ body: (UserDefaults) async throws -> Void) async throws {
|
|
let domain = "DiagnosisTests.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: domain))
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
try await body(defaults)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class RecordingReporter: DiagnosticDataReporting {
|
|
var values: [Bool] = []
|
|
var didChange: ((Bool) -> Void)?
|
|
|
|
func setEnabled(_ enabled: Bool) {
|
|
values.append(enabled)
|
|
didChange?(enabled)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class DeferredDiagnosticDataClient: DiagnosticDataClient {
|
|
var sends = 0
|
|
var deletes = 0
|
|
var checks = 0
|
|
private var pendingCheck: CheckedContinuation<Bool, Never>?
|
|
private var checkStarted: CheckedContinuation<Void, Never>?
|
|
private var actionFinished: CheckedContinuation<Void, Never>?
|
|
|
|
func hasUnsentDiagnosticData() async -> Bool {
|
|
checks += 1
|
|
return await withCheckedContinuation { continuation in
|
|
pendingCheck = continuation
|
|
checkStarted?.resume()
|
|
checkStarted = nil
|
|
}
|
|
}
|
|
|
|
func waitForCheck() async {
|
|
if pendingCheck != nil { return }
|
|
await withCheckedContinuation { checkStarted = $0 }
|
|
}
|
|
|
|
func finishCheck(hasData: Bool) {
|
|
pendingCheck?.resume(returning: hasData)
|
|
pendingCheck = nil
|
|
}
|
|
|
|
func sendUnsentDiagnosticData() {
|
|
sends += 1
|
|
actionFinished?.resume()
|
|
actionFinished = nil
|
|
}
|
|
|
|
func deleteUnsentDiagnosticData() {
|
|
deletes += 1
|
|
actionFinished?.resume()
|
|
actionFinished = nil
|
|
}
|
|
|
|
func waitForAction() async {
|
|
if sends + deletes > 0 { return }
|
|
await withCheckedContinuation { actionFinished = $0 }
|
|
}
|
|
}
|