krz/domain-dig

an ios app for DNS & SSL analysis

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

v5.0.0: DomainDigTests/LocalAPIContractTests.swift · raw

  1import XCTest
  2@testable import DomainDig
  3
  4/// Locks the Local API wire contract: the envelope shape, each payload's field
  5/// names, and the encoding conventions (`v1`, ISO-8601 dates, nil omission).
  6/// A rename or removed field fails here instead of silently breaking an external
  7/// consumer. Values are deliberately not asserted  only structure  so ordinary
  8/// behavior changes don't churn these tests.
  9final class LocalAPIContractTests: XCTestCase {
 10    private let encoder = LocalAPIContract.makeEncoder()
 11
 12    // MARK: - Helpers
 13
 14    private func json<T: Encodable>(_ value: T) throws -> [String: Any] {
 15        let data = try encoder.encode(value)
 16        let object = try JSONSerialization.jsonObject(with: data)
 17        return try XCTUnwrap(object as? [String: Any])
 18    }
 19
 20    private func keys<T: Encodable>(_ value: T) throws -> Set<String> {
 21        Set(try json(value).keys)
 22    }
 23
 24    // MARK: - Envelope
 25
 26    func testVersionIsV1() {
 27        XCTAssertEqual(LocalAPIContract.version, "v1")
 28    }
 29
 30    func testSuccessEnvelopeOmitsErrorAndReportsVersion() throws {
 31        let envelope = LocalAPIEnvelope(
 32            success: true,
 33            data: PortfolioPayload(summary: Self.sampleSummary),
 34            error: nil,
 35            version: LocalAPIContract.version
 36        )
 37        let object = try json(envelope)
 38
 39        XCTAssertEqual(Set(object.keys), ["success", "data", "version"], "nil error must be omitted from the envelope")
 40        XCTAssertEqual(object["success"] as? Bool, true)
 41        XCTAssertEqual(object["version"] as? String, "v1")
 42    }
 43
 44    func testErrorEnvelopeOmitsDataAndCarriesCodeAndMessage() throws {
 45        let envelope = LocalAPIEnvelope<EmptyPayload>(
 46            success: false,
 47            data: nil,
 48            error: LocalAPIErrorPayload(code: "not_found", message: "The requested Local API route does not exist."),
 49            version: LocalAPIContract.version
 50        )
 51        let object = try json(envelope)
 52
 53        XCTAssertEqual(Set(object.keys), ["success", "error", "version"], "nil data must be omitted from the envelope")
 54        XCTAssertEqual(object["success"] as? Bool, false)
 55        let error = try XCTUnwrap(object["error"] as? [String: Any])
 56        XCTAssertEqual(Set(error.keys), ["code", "message"])
 57        XCTAssertEqual(error["code"] as? String, "not_found")
 58    }
 59
 60    // MARK: - Payload field names
 61
 62    func testPortfolioSummaryFields() throws {
 63        XCTAssertEqual(
 64            try keys(Self.sampleSummary),
 65            ["totalDomains", "healthyCount", "warningCount", "criticalCount",
 66             "changedLast24h", "expiringSoonCount", "unreachableCount"]
 67        )
 68    }
 69
 70    func testPortfolioPayloadFields() throws {
 71        XCTAssertEqual(try keys(PortfolioPayload(summary: Self.sampleSummary)), ["summary"])
 72    }
 73
 74    func testDomainListPayloadFields() throws {
 75        let payload = DomainListPayload(domains: [TrackedDomain(domain: "example.com")])
 76        XCTAssertEqual(try keys(payload), ["domains"])
 77    }
 78
 79    func testDomainDetailPayloadFieldsWhenPopulated() throws {
 80        let payload = DomainDetailPayload(
 81            domain: "example.com",
 82            trackedDomain: TrackedDomain(domain: "example.com"),
 83            latestReport: SnapshotFixture.report(domain: "example.com")
 84        )
 85        XCTAssertEqual(try keys(payload), ["domain", "trackedDomain", "latestReport"])
 86    }
 87
 88    func testDomainDetailPayloadOmitsAbsentOptionals() throws {
 89        let payload = DomainDetailPayload(domain: "example.com", trackedDomain: nil, latestReport: nil)
 90        XCTAssertEqual(try keys(payload), ["domain"], "absent trackedDomain/latestReport are omitted, not null")
 91    }
 92
 93    func testDomainHistoryPayloadFields() throws {
 94        let payload = DomainHistoryPayload(domain: "example.com", history: [])
 95        XCTAssertEqual(try keys(payload), ["domain", "history"])
 96    }
 97
 98    func testRecentEventPayloadFields() throws {
 99        XCTAssertEqual(try keys(Self.sampleEvent), ["timestamp", "domain", "summary", "status", "severity"])
100    }
101
102    func testRecentEventsPayloadFields() throws {
103        XCTAssertEqual(try keys(RecentEventsPayload(events: [Self.sampleEvent])), ["events"])
104    }
105
106    func testMonitoringPayloadFieldsAndEnumEncoding() throws {
107        let payload = MonitoringPayload(
108            isEnabled: true,
109            scope: .allTracked,
110            alertsEnabled: true,
111            monitoredDomains: [Self.sampleMonitoringDomain]
112        )
113        let object = try json(payload)
114        XCTAssertEqual(Set(object.keys), ["isEnabled", "scope", "alertsEnabled", "monitoredDomains"])
115        XCTAssertEqual(object["scope"] as? String, "allTracked", "MonitoringScope encodes as its String raw value")
116    }
117
118    func testMonitoringDomainPayloadFieldsAndEnumEncoding() throws {
119        let object = try json(Self.sampleMonitoringDomain)
120        XCTAssertEqual(
121            Set(object.keys),
122            ["domain", "monitoringEnabled", "lastMonitoredAt", "lastAlertAt", "certificateWarningLevel"]
123        )
124        XCTAssertEqual(object["certificateWarningLevel"] as? String, "none")
125    }
126
127    func testMonitoringMutationPayloadFields() throws {
128        XCTAssertEqual(
129            try keys(MonitoringMutationPayload(domain: "example.com", monitoringEnabled: true)),
130            ["domain", "monitoringEnabled"]
131        )
132    }
133
134    func testInspectResponsePayloadFields() throws {
135        XCTAssertEqual(try keys(InspectResponsePayload(report: SnapshotFixture.report())), ["report"])
136    }
137
138    // MARK: - Encoding conventions
139
140    func testDatesEncodeAsISO8601() throws {
141        let event = RecentEventPayload(
142            timestamp: Date(timeIntervalSince1970: 1_700_000_000),
143            domain: "example.com",
144            summary: "changed",
145            status: "changed",
146            severity: "medium"
147        )
148        let object = try json(event)
149        XCTAssertEqual(object["timestamp"] as? String, "2023-11-14T22:13:20Z")
150    }
151
152    // MARK: - Fixtures
153
154    private static let sampleSummary = PortfolioSummary(
155        totalDomains: 3,
156        healthyCount: 1,
157        warningCount: 1,
158        criticalCount: 1,
159        changedLast24h: 2,
160        expiringSoonCount: 1,
161        unreachableCount: 0
162    )
163
164    private static let sampleEvent = RecentEventPayload(
165        timestamp: Date(timeIntervalSince1970: 1_700_000_000),
166        domain: "example.com",
167        summary: "Certificate is approaching expiry",
168        status: "changed",
169        severity: "medium"
170    )
171
172    private static let sampleMonitoringDomain = MonitoringDomainPayload(
173        domain: "example.com",
174        monitoringEnabled: true,
175        lastMonitoredAt: Date(timeIntervalSince1970: 1_700_000_000),
176        lastAlertAt: Date(timeIntervalSince1970: 1_700_000_000),
177        certificateWarningLevel: .none
178    )
179}