krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.15.0: HutchTests/SystemStatusServiceTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5struct SystemStatusServiceTests {
6
7 @Test
8 func parsesCurrentStatusHTMLIntoServicesAndActiveIncidents() throws {
9 let snapshot = try SystemStatusService.parseSnapshotHTML(Self.sampleHTML, fetchedAt: Date(timeIntervalSince1970: 100))
10
11 #expect(snapshot.services.count == 3)
12 #expect(snapshot.services[0].name == "git.sr.ht")
13 #expect(snapshot.services[0].status == .degraded)
14 #expect(snapshot.services[1].status == .operational)
15 #expect(snapshot.hasDisruption)
16 #expect(snapshot.activeIncidents.count == 1)
17 #expect(snapshot.activeIncidents[0].title == "SourceHut disrupted due to DDoS attack")
18 #expect(snapshot.activeIncidents[0].summary == "SourceHut was disrupted by a DDoS attack.")
19 #expect(snapshot.activeIncidents[0].url?.absoluteString == "https://status.sr.ht/issues/2026-04-06-ddos-attack/")
20 }
21
22 @Test
23 func parsesStatusHTMLWithClassOrderChangesAndTimeElements() throws {
24 let snapshot = try SystemStatusService.parseSnapshotHTML(Self.variantHTML, fetchedAt: .now)
25
26 #expect(snapshot.services.count == 2)
27 #expect(snapshot.services[0].status == .operational)
28 #expect(snapshot.services[1].status == .majorOutage)
29 #expect(snapshot.activeIncidents.count == 1)
30 #expect(snapshot.activeIncidents[0].title == "builds.sr.ht outage")
31 #expect(snapshot.activeIncidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T12:00:00Z"))
32 }
33
34 @Test
35 func parsesIncidentFeedRSS() async throws {
36 let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.sampleRSS.utf8))
37
38 #expect(incidents.count == 2)
39 #expect(incidents[0].title == "SourceHut disrupted due to DDoS attack")
40 #expect(incidents[0].isActive == true)
41 #expect(incidents[0].summary == "SourceHut was disrupted by a DDoS attack.")
42 #expect(incidents[1].title == "Planned maintenance on all services")
43 #expect(incidents[1].isActive == false)
44 #expect(incidents[1].updatedAt != nil)
45 }
46
47 @Test
48 func parsesIncidentFeedWithISO8601Dates() async throws {
49 let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.variantRSS.utf8))
50
51 #expect(incidents.count == 1)
52 #expect(incidents[0].title == "Status feed moved")
53 #expect(incidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T15:30:00Z"))
54 }
55
56 @Test
57 func bannerSummaryPrefersSpecificServiceThenCount() {
58 let operational = SystemStatusSnapshot(
59 services: [
60 StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .operational, description: nil)
61 ],
62 activeIncidents: [],
63 lastUpdated: .now
64 )
65
66 let oneDisrupted = SystemStatusSnapshot(
67 services: [
68 StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil),
69 StatusServiceState(id: "hg.sr.ht", name: "hg.sr.ht", slug: "hg.sr.ht", status: .operational, description: nil)
70 ],
71 activeIncidents: [],
72 lastUpdated: .now
73 )
74
75 let multipleDisrupted = SystemStatusSnapshot(
76 services: [
77 StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil),
78 StatusServiceState(id: "builds.sr.ht", name: "builds.sr.ht", slug: "builds.sr.ht", status: .majorOutage, description: nil)
79 ],
80 activeIncidents: [],
81 lastUpdated: .now
82 )
83
84 #expect(operational.hasDisruption == false)
85 #expect(oneDisrupted.bannerSummary == "git.sr.ht disrupted")
86 #expect(multipleDisrupted.bannerSummary == "2 services disrupted")
87 }
88
89 private static let sampleHTML = #"""
90 <!DOCTYPE html>
91 <html>
92 <body class="status-homepage status-disrupted">
93 <div class="announcement-box" style="border-bottom: 0">
94 <div class="padding">
95 <p>
96 <a href="/issues/2026-04-06-ddos-attack/"><strong class="bold">SourceHut disrupted due to DDoS attack →</strong></a>
97 </p>
98 <p><small><a href="/affected/git.sr.ht/" class="tag no-underline">git.sr.ht</a></small></p>
99 <p><strong>SourceHut was disrupted by a DDoS attack</strong>.</p>
100 </div>
101 <hr class="clean announcement-box">
102 </div>
103 <div class="components">
104 <div class="component" data-status="disrupted">
105 <a href="/affected/git.sr.ht/" class="no-underline">git.sr.ht</a>
106 <span class="component-status">Disrupted</span>
107 </div>
108 <div class="component" data-status="ok">
109 <a href="/affected/hg.sr.ht/" class="no-underline">hg.sr.ht</a>
110 <span class="component-status">Operational</span>
111 </div>
112 <div class="component" data-status="notice">
113 <a href="/affected/man.sr.ht/" class="no-underline">man.sr.ht</a>
114 <span class="component-status">Maintenance</span>
115 </div>
116 </div>
117 <a href="https://status.sr.ht/issues/2026-04-06-ddos-attack/" class="issue no-underline">
118 <small class="date float-right " title="Apr 5 10:00:00 2026 UTC">April 5, 2026 at 10:00 AM UTC</small>
119 <h3>SourceHut disrupted due to DDoS attack</h3>
120 <strong class="error">▲ This issue is not resolved yet</strong>
121 </a>
122 </body>
123 </html>
124 """#
125
126 private static let variantHTML = #"""
127 <html>
128 <body>
129 <div class="component extra" data-status="ok">
130 <a class="no-underline" href="/affected/meta.sr.ht/">meta.sr.ht</a>
131 <small class="component-status secondary">All systems operational</small>
132 </div>
133 <div data-status="down" class="extra component">
134 <a href="/affected/builds.sr.ht/" class="link">builds.sr.ht</a>
135 <div class="component-status badge">Outage</div>
136 </div>
137 <div class="announcement-box">
138 <div class="padding">
139 <p><a href="/issues/2026-04-07-builds-outage/"><strong>builds.sr.ht outage</strong></a></p>
140 <p><strong>Build jobs are currently failing.</strong></p>
141 </div>
142 </div>
143 <a class="issue no-underline urgent" href="/issues/2026-04-07-builds-outage/">
144 <time class="date" datetime="2026-04-07T12:00:00Z">Apr 7</time>
145 <h4>builds.sr.ht outage</h4>
146 <span>Investigating elevated failures</span>
147 </a>
148 </body>
149 </html>
150 """#
151
152 private static let sampleRSS = #"""
153 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
154 <rss version="2.0">
155 <channel>
156 <title>sr.ht status</title>
157 <item>
158 <title>SourceHut disrupted due to DDoS attack</title>
159 <link>https://status.sr.ht/issues/2026-04-06-ddos-attack/</link>
160 <pubDate>Sun, 05 Apr 2026 10:00:00 +0000</pubDate>
161 <guid>https://status.sr.ht/issues/2026-04-06-ddos-attack/</guid>
162 <category></category>
163 <description><p><strong>SourceHut was disrupted by a DDoS attack</strong>.</p></description>
164 </item>
165 <item>
166 <title>[Resolved] Planned maintenance on all services</title>
167 <link>https://status.sr.ht/issues/2025-10-22-planned-maintenance/</link>
168 <pubDate>Wed, 22 Oct 2025 11:00:00 +0000</pubDate>
169 <guid>https://status.sr.ht/issues/2025-10-22-planned-maintenance/</guid>
170 <category>2025-10-22 12:25:00</category>
171 <description><p><strong>The maintenance is complete</strong>.</p></description>
172 </item>
173 </channel>
174 </rss>
175 """#
176
177 private static let variantRSS = #"""
178 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
179 <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
180 <channel>
181 <title>sr.ht status</title>
182 <item>
183 <title>Status feed moved</title>
184 <link>https://status.sr.ht/issues/2026-04-07-feed-moved/</link>
185 <dc:date>2026-04-07T15:30:00Z</dc:date>
186 <guid>https://status.sr.ht/issues/2026-04-07-feed-moved/</guid>
187 <description><p>Use the new feed endpoint.</p></description>
188 </item>
189 </channel>
190 </rss>
191 """#
192}