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
|
import Foundation
import Testing
@testable import Hutch
struct SystemStatusServiceTests {
@Test
func parsesCurrentStatusHTMLIntoServicesAndActiveIncidents() throws {
let snapshot = try SystemStatusService.parseSnapshotHTML(Self.sampleHTML, fetchedAt: Date(timeIntervalSince1970: 100))
#expect(snapshot.services.count == 3)
#expect(snapshot.services[0].name == "git.sr.ht")
#expect(snapshot.services[0].status == .degraded)
#expect(snapshot.services[1].status == .operational)
#expect(snapshot.hasDisruption)
#expect(snapshot.activeIncidents.count == 1)
#expect(snapshot.activeIncidents[0].title == "SourceHut disrupted due to DDoS attack")
#expect(snapshot.activeIncidents[0].summary == "SourceHut was disrupted by a DDoS attack.")
#expect(snapshot.activeIncidents[0].url?.absoluteString == "https://status.sr.ht/issues/2026-04-06-ddos-attack/")
}
@Test
func parsesIncidentFeedRSS() async throws {
let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.sampleRSS.utf8))
#expect(incidents.count == 2)
#expect(incidents[0].title == "SourceHut disrupted due to DDoS attack")
#expect(incidents[0].isActive == true)
#expect(incidents[0].summary == "SourceHut was disrupted by a DDoS attack.")
#expect(incidents[1].title == "Planned maintenance on all services")
#expect(incidents[1].isActive == false)
#expect(incidents[1].updatedAt != nil)
}
@Test
func bannerSummaryPrefersSpecificServiceThenCount() {
let operational = SystemStatusSnapshot(
services: [
StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .operational, description: nil)
],
activeIncidents: [],
lastUpdated: .now
)
let oneDisrupted = SystemStatusSnapshot(
services: [
StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil),
StatusServiceState(id: "hg.sr.ht", name: "hg.sr.ht", slug: "hg.sr.ht", status: .operational, description: nil)
],
activeIncidents: [],
lastUpdated: .now
)
let multipleDisrupted = SystemStatusSnapshot(
services: [
StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil),
StatusServiceState(id: "builds.sr.ht", name: "builds.sr.ht", slug: "builds.sr.ht", status: .majorOutage, description: nil)
],
activeIncidents: [],
lastUpdated: .now
)
#expect(operational.hasDisruption == false)
#expect(oneDisrupted.bannerSummary == "git.sr.ht disrupted")
#expect(multipleDisrupted.bannerSummary == "2 services disrupted")
}
private static let sampleHTML = #"""
<!DOCTYPE html>
<html>
<body class="status-homepage status-disrupted">
<div class="announcement-box" style="border-bottom: 0">
<div class="padding">
<p>
<a href="/issues/2026-04-06-ddos-attack/"><strong class="bold">SourceHut disrupted due to DDoS attack →</strong></a>
</p>
<p><small><a href="/affected/git.sr.ht/" class="tag no-underline">git.sr.ht</a></small></p>
<p><strong>SourceHut was disrupted by a DDoS attack</strong>.</p>
</div>
<hr class="clean announcement-box">
</div>
<div class="components">
<div class="component" data-status="disrupted">
<a href="/affected/git.sr.ht/" class="no-underline">git.sr.ht</a>
<span class="component-status">Disrupted</span>
</div>
<div class="component" data-status="ok">
<a href="/affected/hg.sr.ht/" class="no-underline">hg.sr.ht</a>
<span class="component-status">Operational</span>
</div>
<div class="component" data-status="notice">
<a href="/affected/man.sr.ht/" class="no-underline">man.sr.ht</a>
<span class="component-status">Maintenance</span>
</div>
</div>
<a href="https://status.sr.ht/issues/2026-04-06-ddos-attack/" class="issue no-underline">
<small class="date float-right " title="Apr 5 10:00:00 2026 UTC">April 5, 2026 at 10:00 AM UTC</small>
<h3>SourceHut disrupted due to DDoS attack</h3>
<strong class="error">▲ This issue is not resolved yet</strong>
</a>
</body>
</html>
"""#
private static let sampleRSS = #"""
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0">
<channel>
<title>sr.ht status</title>
<item>
<title>SourceHut disrupted due to DDoS attack</title>
<link>https://status.sr.ht/issues/2026-04-06-ddos-attack/</link>
<pubDate>Sun, 05 Apr 2026 10:00:00 +0000</pubDate>
<guid>https://status.sr.ht/issues/2026-04-06-ddos-attack/</guid>
<category></category>
<description><p><strong>SourceHut was disrupted by a DDoS attack</strong>.</p></description>
</item>
<item>
<title>[Resolved] Planned maintenance on all services</title>
<link>https://status.sr.ht/issues/2025-10-22-planned-maintenance/</link>
<pubDate>Wed, 22 Oct 2025 11:00:00 +0000</pubDate>
<guid>https://status.sr.ht/issues/2025-10-22-planned-maintenance/</guid>
<category>2025-10-22 12:25:00</category>
<description><p><strong>The maintenance is complete</strong>.</p></description>
</item>
</channel>
</rss>
"""#
}
|