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