From a7d154e1db9fd9904b2255db889144ad15aac2f2 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 24 Jul 2026 19:27:24 -0500 Subject: test: establish unit-test net for the deterministic core (v5 step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- DomainDigTests/DiffServiceTests.swift | 163 ++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 DomainDigTests/DiffServiceTests.swift (limited to 'DomainDigTests/DiffServiceTests.swift') diff --git a/DomainDigTests/DiffServiceTests.swift b/DomainDigTests/DiffServiceTests.swift new file mode 100644 index 0000000..a3a367c --- /dev/null +++ b/DomainDigTests/DiffServiceTests.swift @@ -0,0 +1,163 @@ +import XCTest +@testable import DomainDig + +/// Characterization tests for the field-level diff engine. `DiffService` is a +/// pure function over two `DomainReport`s, so every case here pins observable +/// behavior (change classification, normalization, summary phrasing) against a +/// deterministic fixture. +final class DiffServiceTests: XCTestCase { + + func testIdenticalReportsProduceNoChanges() { + let report = SnapshotFixture.report( + availability: .registered, + ownership: DomainOwnership(registrar: "Example Registrar") + ) + + let diff = DiffService.compare(from: report, to: report) + + XCTAssertEqual(diff.changeCount, 0) + XCTAssertTrue(diff.changedSectionTitles.isEmpty) + XCTAssertFalse(diff.sections.contains { $0.hasChanges }) + } + + func testAvailabilityTransitionIsReportedAsChanged() { + let old = SnapshotFixture.report(availability: .available) + let new = SnapshotFixture.report(availability: .registered) + + let diff = DiffService.compare(from: old, to: new) + + let availability = try? XCTUnwrap(diff.sections.first { $0.id == "availability" }) + let item = availability?.items.first { $0.id == "availability" } + XCTAssertEqual(item?.changeType, .changed) + XCTAssertEqual(item?.oldValue, "Available") + XCTAssertEqual(item?.newValue, "Registered") + XCTAssertTrue(diff.changedSectionTitles.contains("Domain / Availability")) + } + + func testPrimaryIPChangeSurfacesInAvailabilitySection() { + let old = SnapshotFixture.report( + availability: .registered, + dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.10"])] + ) + let new = SnapshotFixture.report( + availability: .registered, + dnsSections: [SnapshotFixture.dnsSection(type: .A, values: ["203.0.113.20"])] + ) + + let diff = DiffService.compare(from: old, to: new) + let item = diff.sections + .first { $0.id == "availability" }? + .items.first { $0.id == "primary-ip" } + + XCTAssertEqual(item?.changeType, .changed) + XCTAssertEqual(item?.oldValue, "203.0.113.10") + XCTAssertEqual(item?.newValue, "203.0.113.20") + } + + func testCaseAndWhitespaceDifferencesAreNotChanges() { + let old = SnapshotFixture.report(ownership: DomainOwnership(registrar: "GoDaddy")) + let new = SnapshotFixture.report(ownership: DomainOwnership(registrar: " godaddy ")) + + let diff = DiffService.compare(from: old, to: new) + let item = diff.sections + .first { $0.id == "ownership" }? + .items.first { $0.id == "registrar" } + + XCTAssertEqual(item?.changeType, .unchanged, "case- and whitespace-only differences must not register as changes") + } + + func testAddedAndRemovedOwnershipFields() { + let absent = SnapshotFixture.report(ownership: nil) + let present = SnapshotFixture.report(ownership: DomainOwnership(registrar: "Example Registrar")) + + let added = DiffService.compare(from: absent, to: present) + .sections.first { $0.id == "ownership" }? + .items.first { $0.id == "registrar" } + XCTAssertEqual(added?.changeType, .added) + XCTAssertNil(added?.oldValue) + XCTAssertEqual(added?.newValue, "Example Registrar") + + let removed = DiffService.compare(from: present, to: absent) + .sections.first { $0.id == "ownership" }? + .items.first { $0.id == "registrar" } + XCTAssertEqual(removed?.changeType, .removed) + XCTAssertEqual(removed?.oldValue, "Example Registrar") + XCTAssertNil(removed?.newValue) + } + + func testDNSRecordValueChangeIsDetected() { + let old = SnapshotFixture.report( + dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns2.example.com"])] + ) + let new = SnapshotFixture.report( + dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns3.example.com"])] + ) + + let dns = DiffService.compare(from: old, to: new).sections.first { $0.id == "dns" } + let records = dns?.items.first { $0.id == "dns-ns-records" } + XCTAssertEqual(records?.changeType, .changed) + } + + func testDNSRecordReorderIsNotAChange() { + // Values are normalized (sorted, lowercased) before comparison, so a pure + // reorder must diff as unchanged. + let old = SnapshotFixture.report( + dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["ns1.example.com", "ns2.example.com"])] + ) + let new = SnapshotFixture.report( + dnsSections: [SnapshotFixture.dnsSection(type: .NS, values: ["NS2.example.com", "NS1.example.com"])] + ) + + let records = DiffService.compare(from: old, to: new) + .sections.first { $0.id == "dns" }? + .items.first { $0.id == "dns-ns-records" } + XCTAssertEqual(records?.changeType, .unchanged) + } + + func testSummaryMessagePhrasing() { + XCTAssertEqual(DiffService.summaryMessage(from: [], changeCount: 0), "No meaningful changes") + XCTAssertEqual(DiffService.summaryMessage(from: ["DNS"], changeCount: 1), "DNS changed") + XCTAssertEqual( + DiffService.summaryMessage(from: ["DNS", "Ownership"], changeCount: 3), + "DNS and ownership changed (3 items)" + ) + } + + func testContextNoteFlagsDifferentResolvers() { + let old = SnapshotFixture.report(resolverURLString: "https://one.example/dns-query") + let new = SnapshotFixture.report(resolverURLString: "https://two.example/dns-query") + + let note = DiffService.comparisonContextNote(from: old, to: new) + XCTAssertEqual(note, "Compared snapshots used different DNS resolvers.") + } + + func testContextNoteNilWhenResolversMatch() { + let report = SnapshotFixture.report() + XCTAssertNil(DiffService.comparisonContextNote(from: report, to: report)) + } + + func testCertificateWarningLevelThresholds() { + func level(daysUntilExpiry days: Int?) -> CertificateWarningLevel { + let ssl = days.map { SnapshotFixture.certificate(daysUntilExpiry: $0) } + return DiffService.certificateWarningLevel(for: SnapshotFixture.snapshot(sslInfo: ssl)) + } + + XCTAssertEqual(level(daysUntilExpiry: nil), .none) + XCTAssertEqual(level(daysUntilExpiry: 45), .none) + XCTAssertEqual(level(daysUntilExpiry: 29), .warning) + XCTAssertEqual(level(daysUntilExpiry: 14), .warning) + XCTAssertEqual(level(daysUntilExpiry: 13), .critical) + XCTAssertEqual(level(daysUntilExpiry: 0), .critical) + } + + func testCrossDomainComparisonPairsBothDomains() { + let a = SnapshotFixture.report(domain: "alpha.example", availability: .registered) + let b = SnapshotFixture.report(domain: "beta.example", availability: .available) + + let result = DiffService.compare(domainA: a, domainB: b) + + XCTAssertEqual(result.domainA, "alpha.example") + XCTAssertEqual(result.domainB, "beta.example") + XCTAssertTrue(result.changeCount > 0) + } +} -- cgit v1.2.3