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