krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

remove-splash-highlighter: 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>&lt;p&gt;&lt;strong&gt;SourceHut was disrupted by a DDoS attack&lt;/strong&gt;.&lt;/p&gt;</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>&lt;p&gt;&lt;strong&gt;The maintenance is complete&lt;/strong&gt;.&lt;/p&gt;</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>&lt;p&gt;Use the new feed endpoint.&lt;/p&gt;</description>
191        </item>
192      </channel>
193    </rss>
194    """#
195}