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