krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.2: 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}