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