summaryrefslogtreecommitdiff
path: root/DomainDigTests/DomainDataPortabilityServiceTests.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-24 19:27:24 -0500
committerChristian Cleberg <[email protected]>2026-07-24 22:52:53 -0500
commita7d154e1db9fd9904b2255db889144ad15aac2f2 (patch)
tree36e9d636821952ca1a4a0b2d072065687abc1960 /DomainDigTests/DomainDataPortabilityServiceTests.swift
parent22ad96331a4456662f740c48882cab8aadd6d0b4 (diff)
downloaddomain-dig-a7d154e1db9fd9904b2255db889144ad15aac2f2.tar.gz
domain-dig-a7d154e1db9fd9904b2255db889144ad15aac2f2.tar.bz2
domain-dig-a7d154e1db9fd9904b2255db889144ad15aac2f2.zip
test: establish unit-test net for the deterministic core (v5 step 1)
Stands up the DomainDigTests unit-test target the v5.0.0 roadmap flags as the mandatory first move before decomposing the god-files and locking external contracts. The project had no XCTest unit target — only the DomainDigUITests accessibility suite. - New DomainDigTests target (unit_test_bundle), hosted by the app so @testable import DomainDig links. Mirrors the UITests build settings (SWIFT_VERSION 5.0 + MainActor default isolation) to avoid the XCTest override-isolation issue recorded for the Swift 6 targets. Wired into the shared DomainDig scheme's Test action, so it runs in CI and the pre-push audit hook automatically (both invoke the whole scheme). - SnapshotFixture builds the deep LookupSnapshot/DomainReport models through their real initializer and the DomainReportBuilder, exposing only the fields the tests vary. - 36 characterization tests across the four deterministic units the roadmap names: - DiffService: change classification, case/whitespace normalization, DNS record reorder-vs-change, summary phrasing, resolver context note, certificate-warning thresholds. - DomainReportBuilder: snapshot -> report field mapping, primary-IP and DNSSEC derivation, TLS status, partial-snapshot/validation passthrough. - DomainReportExporter: format dispatch, JSON round-trip (the machine contract), CSV/markdown/text/PDF structural invariants, timeline. - DomainDataPortabilityService: CSV round-trip and the merge/dedup semantics (case-insensitive collapse, OR-merged pin state, min-created/max-updated, recency sort) via an ephemeral UserDefaults suite. - build.yml comment updated to reflect the second test target.
Diffstat (limited to 'DomainDigTests/DomainDataPortabilityServiceTests.swift')
-rw-r--r--DomainDigTests/DomainDataPortabilityServiceTests.swift124
1 files changed, 124 insertions, 0 deletions
diff --git a/DomainDigTests/DomainDataPortabilityServiceTests.swift b/DomainDigTests/DomainDataPortabilityServiceTests.swift
new file mode 100644
index 0000000..ca2c72c
--- /dev/null
+++ b/DomainDigTests/DomainDataPortabilityServiceTests.swift
@@ -0,0 +1,124 @@
+import XCTest
+@testable import DomainDig
+
+/// Characterization tests for the portability layer's deterministic core: the
+/// CSV round-trip and the merge/dedup semantics that `load*/save*` apply. Storage
+/// is exercised through an ephemeral `UserDefaults` suite so nothing touches the
+/// real app domain.
+final class DomainDataPortabilityServiceTests: XCTestCase {
+ private let suiteName = "DomainDigTests.portability"
+ 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()
+ }
+
+ // MARK: - CSV round-trip
+
+ func testTrackedDomainCSVRoundTripPreservesFields() throws {
+ let original = TrackedDomain(
+ domain: "csv.example",
+ createdAt: base,
+ updatedAt: base.addingTimeInterval(3_600),
+ note: "keep an eye on this",
+ isPinned: true,
+ monitoringEnabled: false,
+ lastKnownAvailability: .registered,
+ certificateWarningLevel: .warning,
+ certificateDaysRemaining: 12
+ )
+
+ let csv = DataPortabilityCSV.trackedDomains([original])
+ let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv)
+
+ XCTAssertEqual(parsed.count, 1)
+ let restored = try XCTUnwrap(parsed.first)
+ XCTAssertEqual(restored.domain, "csv.example")
+ XCTAssertEqual(restored.note, "keep an eye on this")
+ XCTAssertTrue(restored.isPinned)
+ XCTAssertFalse(restored.monitoringEnabled)
+ XCTAssertEqual(restored.lastKnownAvailability, .registered)
+ XCTAssertEqual(restored.certificateWarningLevel, .warning)
+ XCTAssertEqual(restored.certificateDaysRemaining, 12)
+ }
+
+ func testTrackedDomainCSVParseSkipsRowsWithoutDomain() throws {
+ let csv = """
+ domain,isPinned,monitoringEnabled
+ ,true,true
+ valid.example,false,true
+ """
+
+ let parsed = try DataPortabilityCSV.parseTrackedDomains(from: csv)
+
+ XCTAssertEqual(parsed.map(\.domain), ["valid.example"])
+ }
+
+ // MARK: - Merge / dedup
+
+ func testSaveLoadDeduplicatesSameDomainCaseInsensitively() {
+ let older = TrackedDomain(
+ domain: "example.com",
+ createdAt: base.addingTimeInterval(-100),
+ updatedAt: base.addingTimeInterval(-50),
+ isPinned: false,
+ monitoringEnabled: false
+ )
+ let newer = TrackedDomain(
+ domain: "EXAMPLE.com",
+ createdAt: base.addingTimeInterval(-80),
+ updatedAt: base.addingTimeInterval(-10),
+ isPinned: true,
+ monitoringEnabled: false
+ )
+
+ DomainDataPortabilityService.saveTrackedDomains([older, newer], defaults: defaults)
+ let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults)
+
+ XCTAssertEqual(loaded.count, 1, "same domain differing only in case must collapse to one entry")
+ let merged = loaded[0]
+ XCTAssertEqual(merged.domain, "example.com")
+ XCTAssertTrue(merged.isPinned, "pinned state is OR-merged")
+ XCTAssertFalse(merged.monitoringEnabled)
+ XCTAssertEqual(merged.createdAt, base.addingTimeInterval(-100), "createdAt takes the earliest")
+ XCTAssertEqual(merged.updatedAt, base.addingTimeInterval(-10), "updatedAt takes the latest")
+ }
+
+ func testSaveLoadKeepsDistinctDomainsSortedByRecency() {
+ let domains = [
+ TrackedDomain(domain: "old.example", updatedAt: base.addingTimeInterval(-300)),
+ TrackedDomain(domain: "new.example", updatedAt: base.addingTimeInterval(-10)),
+ TrackedDomain(domain: "mid.example", updatedAt: base.addingTimeInterval(-100))
+ ]
+
+ DomainDataPortabilityService.saveTrackedDomains(domains, defaults: defaults)
+ let loaded = DomainDataPortabilityService.loadTrackedDomains(defaults: defaults)
+
+ XCTAssertEqual(loaded.map(\.domain), ["new.example", "mid.example", "old.example"])
+ }
+
+ func testLoadTrackedDomainsEmptyWhenUnset() {
+ XCTAssertTrue(DomainDataPortabilityService.loadTrackedDomains(defaults: defaults).isEmpty)
+ }
+
+ // MARK: - Recent searches
+
+ func testRecentSearchesRoundTripAndTruncateToTwenty() {
+ let values = (0..<30).map { "domain\($0).example" }
+
+ DomainDataPortabilityService.saveRecentSearches(values, defaults: defaults)
+ let loaded = DomainDataPortabilityService.loadRecentSearches(defaults: defaults)
+
+ XCTAssertEqual(loaded.count, 20, "recent searches are capped at 20")
+ XCTAssertEqual(loaded, Array(values.prefix(20)), "order is preserved")
+ }
+}