diff options
| author | Christian Cleberg <[email protected]> | 2026-07-24 19:27:24 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-24 22:52:53 -0500 |
| commit | a7d154e1db9fd9904b2255db889144ad15aac2f2 (patch) | |
| tree | 36e9d636821952ca1a4a0b2d072065687abc1960 /DomainDigTests/DomainDataPortabilityServiceTests.swift | |
| parent | 22ad96331a4456662f740c48882cab8aadd6d0b4 (diff) | |
| download | domain-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.swift | 124 |
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") + } +} |
