krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
main: DomainDigTests/DataMigrationServiceTests.swift · raw
1import XCTest
2@testable import DomainDig
3
4/// Characterization tests for the versioned store-migration runner. They drive
5/// `migrateIfNeeded` against legacy on-disk fixtures in an ephemeral
6/// `UserDefaults` suite and pin the policy: forward-only, idempotent, and
7/// non-destructive to a store written by a newer build.
8///
9/// The raw storage-key strings ("trackedDomains", "watchedDomains", the legacy
10/// marker) are duplicated here on purpose — they are the on-disk contract, and
11/// hard-coding them means an accidental rename shows up as a failing migration.
12final class DataMigrationServiceTests: XCTestCase {
13 private let suiteName = "DomainDigTests.migration"
14 private var defaults: UserDefaults!
15 private let base = Date(timeIntervalSince1970: 1_700_000_000)
16
17 override func setUp() {
18 super.setUp()
19 defaults = UserDefaults(suiteName: suiteName)
20 defaults.removePersistentDomain(forName: suiteName)
21 }
22
23 override func tearDown() {
24 defaults.removePersistentDomain(forName: suiteName)
25 defaults = nil
26 super.tearDown()
27 }
28
29 func testFreshStoreIsStampedAtCurrentVersion() {
30 XCTAssertEqual(DataMigrationService.storeSchemaVersion(defaults: defaults), 0)
31
32 DataMigrationService.migrateIfNeeded(defaults: defaults)
33
34 XCTAssertEqual(
35 DataMigrationService.storeSchemaVersion(defaults: defaults),
36 DataMigrationService.currentStoreSchemaVersion
37 )
38 }
39
40 func testLegacyWatchedDomainsAreMigratedAndTheOldKeyIsDropped() throws {
41 let legacy = [WatchedDomain(domain: "legacy.example", createdAt: base, lastKnownAvailability: .registered)]
42 defaults.set(try JSONEncoder().encode(legacy), forKey: "watchedDomains")
43
44 DataMigrationService.migrateIfNeeded(defaults: defaults)
45
46 let tracked = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults)
47 XCTAssertEqual(tracked.map(\.domain), ["legacy.example"])
48 XCTAssertNil(defaults.data(forKey: "watchedDomains"), "legacy key is dropped after migration")
49 XCTAssertNotNil(defaults.data(forKey: "trackedDomains"), "data is rewritten under the current key")
50 XCTAssertEqual(DataMigrationService.storeSchemaVersion(defaults: defaults), 1)
51 }
52
53 func testMigrationDeduplicatesTheStoredBlobInPlace() throws {
54 let dupes = [
55 TrackedDomain(domain: "dupe.example", updatedAt: base.addingTimeInterval(-10)),
56 TrackedDomain(domain: "DUPE.example", updatedAt: base)
57 ]
58 defaults.set(try JSONEncoder().encode(dupes), forKey: "trackedDomains")
59
60 DataMigrationService.migrateIfNeeded(defaults: defaults)
61
62 let data = try XCTUnwrap(defaults.data(forKey: "trackedDomains"))
63 let stored = try JSONDecoder().decode([TrackedDomain].self, from: data)
64 XCTAssertEqual(stored.count, 1, "the persisted blob is deduplicated, not just the load result")
65 }
66
67 func testMigrationIsIdempotent() throws {
68 defaults.set(
69 try JSONEncoder().encode([TrackedDomain(domain: "a.example", updatedAt: base)]),
70 forKey: "trackedDomains"
71 )
72
73 DataMigrationService.migrateIfNeeded(defaults: defaults)
74 let afterFirst = defaults.data(forKey: "trackedDomains")
75
76 DataMigrationService.migrateIfNeeded(defaults: defaults)
77 let afterSecond = defaults.data(forKey: "trackedDomains")
78
79 XCTAssertEqual(afterFirst, afterSecond, "a second run makes no further changes")
80 XCTAssertEqual(
81 DataMigrationService.storeSchemaVersion(defaults: defaults),
82 DataMigrationService.currentStoreSchemaVersion
83 )
84 }
85
86 func testLegacyBooleanMarkerCountsAsVersionOne() {
87 defaults.set(true, forKey: "data.migrations.v3_4_0")
88
89 XCTAssertEqual(DataMigrationService.storeSchemaVersion(defaults: defaults), 1)
90
91 // Already at v1, so the v1 step must not re-run: a leftover legacy blob
92 // is left exactly as found.
93 defaults.set(Data("x".utf8), forKey: "watchedDomains")
94 DataMigrationService.migrateIfNeeded(defaults: defaults)
95 XCTAssertNotNil(defaults.data(forKey: "watchedDomains"), "v1 is treated as already applied; no re-run")
96 }
97
98 func testNewerStoreVersionIsNeverDowngradedOrRewritten() throws {
99 let future = DataMigrationService.currentStoreSchemaVersion + 1
100 defaults.set(future, forKey: DataMigrationService.storeSchemaVersionKey)
101 let blob = try JSONEncoder().encode([TrackedDomain(domain: "keep.example", updatedAt: base)])
102 defaults.set(blob, forKey: "trackedDomains")
103
104 DataMigrationService.migrateIfNeeded(defaults: defaults)
105
106 XCTAssertEqual(DataMigrationService.storeSchemaVersion(defaults: defaults), future, "must never downgrade")
107 XCTAssertEqual(defaults.data(forKey: "trackedDomains"), blob, "future-version data is left byte-for-byte")
108 }
109}