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}