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