krz/hutch

an ios client for sourcehut

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

v2.15.1: HutchTests/SystemStatusServiceTests.swift · raw

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