summaryrefslogtreecommitdiff
path: root/HutchTests
diff options
context:
space:
mode:
Diffstat (limited to 'HutchTests')
-rw-r--r--HutchTests/AppStateTests.swift11
-rw-r--r--HutchTests/SystemStatusRepositoryTests.swift147
-rw-r--r--HutchTests/SystemStatusServiceTests.swift63
3 files changed, 221 insertions, 0 deletions
diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift
index ca34df2..9cf4e13 100644
--- a/HutchTests/AppStateTests.swift
+++ b/HutchTests/AppStateTests.swift
@@ -22,4 +22,15 @@ struct AppStateTests {
#expect(appState.deepLinkError == "The ticket could not be found or is inaccessible.")
}
+
+ @Test
+ @MainActor
+ func openSystemStatusSelectsMoreTabAndQueuesNavigation() {
+ let appState = AppState()
+
+ appState.openSystemStatus()
+
+ #expect(appState.selectedTab == .more)
+ #expect(appState.pendingTabNavigation == .systemStatus)
+ }
}
diff --git a/HutchTests/SystemStatusRepositoryTests.swift b/HutchTests/SystemStatusRepositoryTests.swift
new file mode 100644
index 0000000..fb08732
--- /dev/null
+++ b/HutchTests/SystemStatusRepositoryTests.swift
@@ -0,0 +1,147 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct SystemStatusRepositoryTests {
+
+ @Test
+ func fallsBackToPersistedSnapshotWhenRefreshFails() async throws {
+ let defaultsName = "SystemStatusRepositoryTests-\(UUID().uuidString)"
+ let defaults = try #require(UserDefaults(suiteName: defaultsName))
+ defaults.removePersistentDomain(forName: defaultsName)
+ defer { defaults.removePersistentDomain(forName: defaultsName) }
+
+ let cachedSnapshot = SystemStatusSnapshot(
+ services: [
+ StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil)
+ ],
+ activeIncidents: [],
+ lastUpdated: Date(timeIntervalSince1970: 120)
+ )
+ let cacheStore = SystemStatusCacheStore(defaults: defaults)
+ let initialRepository = SystemStatusRepository(
+ service: TestSystemStatusService(
+ snapshotHTMLHandler: { Self.cachedSnapshotHTML },
+ incidentsDataHandler: { Data(Self.emptyRSS.utf8) }
+ ),
+ cacheStore: cacheStore,
+ now: { Date(timeIntervalSince1970: 120) }
+ )
+
+ _ = try await initialRepository.snapshotResult(forceRefresh: true)
+
+ let fallbackRepository = SystemStatusRepository(
+ service: TestSystemStatusService(
+ snapshotHTMLHandler: { throw SRHTError.httpError(503) },
+ incidentsDataHandler: { Data(Self.emptyRSS.utf8) }
+ ),
+ ttl: 0,
+ cacheStore: cacheStore,
+ now: { Date(timeIntervalSince1970: 180) }
+ )
+
+ let result = try await fallbackRepository.snapshotResult(forceRefresh: true)
+
+ #expect(result.value == cachedSnapshot)
+ #expect(result.isStale)
+ #expect(result.lastSuccessfulAt == Date(timeIntervalSince1970: 120))
+ #expect(result.refreshErrorMessage != nil)
+ }
+
+ @Test
+ func fallsBackToPersistedIncidentsWhenRefreshFails() async throws {
+ let defaultsName = "SystemStatusRepositoryTests-\(UUID().uuidString)"
+ let defaults = try #require(UserDefaults(suiteName: defaultsName))
+ defaults.removePersistentDomain(forName: defaultsName)
+ defer { defaults.removePersistentDomain(forName: defaultsName) }
+
+ let cachedIncidents = [
+ StatusIncident(
+ id: "incident-1",
+ title: "builds.sr.ht outage",
+ summary: "Builds are failing.",
+ url: nil,
+ publishedAt: Date(timeIntervalSince1970: 200),
+ updatedAt: nil,
+ isActive: true
+ )
+ ]
+ let cacheStore = SystemStatusCacheStore(defaults: defaults)
+ let initialRepository = SystemStatusRepository(
+ service: TestSystemStatusService(
+ snapshotHTMLHandler: { Self.cachedOperationalHTML },
+ incidentsDataHandler: { Data(Self.cachedIncidentRSS.utf8) }
+ ),
+ cacheStore: cacheStore,
+ now: { Date(timeIntervalSince1970: 220) }
+ )
+
+ _ = try await initialRepository.recentIncidentsResult(forceRefresh: true)
+
+ let fallbackRepository = SystemStatusRepository(
+ service: TestSystemStatusService(
+ snapshotHTMLHandler: { Self.cachedOperationalHTML },
+ incidentsDataHandler: { throw SRHTError.httpError(504) }
+ ),
+ ttl: 0,
+ cacheStore: cacheStore,
+ now: { Date(timeIntervalSince1970: 260) }
+ )
+
+ let result = try await fallbackRepository.recentIncidentsResult(forceRefresh: true)
+
+ #expect(result.value == cachedIncidents)
+ #expect(result.isStale)
+ #expect(result.lastSuccessfulAt == Date(timeIntervalSince1970: 220))
+ #expect(result.refreshErrorMessage != nil)
+ }
+}
+
+private struct TestSystemStatusService: SystemStatusServing {
+ let snapshotHTMLHandler: @Sendable () async throws -> String
+ let incidentsDataHandler: @Sendable () async throws -> Data
+
+ func fetchSnapshotHTML() async throws -> String {
+ try await snapshotHTMLHandler()
+ }
+
+ func fetchIncidentFeedData() async throws -> Data {
+ try await incidentsDataHandler()
+ }
+}
+
+private extension SystemStatusRepositoryTests {
+ static let cachedSnapshotHTML = #"""
+ <div class="component" data-status="disrupted">
+ <a href="/affected/git.sr.ht/">git.sr.ht</a>
+ <span class="component-status">Disrupted</span>
+ </div>
+ """#
+
+ static let cachedOperationalHTML = #"""
+ <div class="component" data-status="ok">
+ <a href="/affected/meta.sr.ht/">meta.sr.ht</a>
+ <span class="component-status">Operational</span>
+ </div>
+ """#
+
+ static let cachedIncidentRSS = #"""
+ <rss version="2.0">
+ <channel>
+ <item>
+ <title>builds.sr.ht outage</title>
+ <link>https://status.sr.ht/issues/1/</link>
+ <pubDate>Thu, 01 Jan 1970 00:03:20 +0000</pubDate>
+ <guid>incident-1</guid>
+ <description>&lt;p&gt;Builds are failing.&lt;/p&gt;</description>
+ </item>
+ </channel>
+ </rss>
+ """#
+
+ static let emptyRSS = #"""
+ <rss version="2.0">
+ <channel></channel>
+ </rss>
+ """#
+}
diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift
index 58ace33..d1577b6 100644
--- a/HutchTests/SystemStatusServiceTests.swift
+++ b/HutchTests/SystemStatusServiceTests.swift
@@ -20,6 +20,18 @@ struct SystemStatusServiceTests {
}
@Test
+ func parsesStatusHTMLWithClassOrderChangesAndTimeElements() throws {
+ let snapshot = try SystemStatusService.parseSnapshotHTML(Self.variantHTML, fetchedAt: .now)
+
+ #expect(snapshot.services.count == 2)
+ #expect(snapshot.services[0].status == .operational)
+ #expect(snapshot.services[1].status == .majorOutage)
+ #expect(snapshot.activeIncidents.count == 1)
+ #expect(snapshot.activeIncidents[0].title == "builds.sr.ht outage")
+ #expect(snapshot.activeIncidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T12:00:00Z"))
+ }
+
+ @Test
func parsesIncidentFeedRSS() async throws {
let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.sampleRSS.utf8))
@@ -33,6 +45,15 @@ struct SystemStatusServiceTests {
}
@Test
+ func parsesIncidentFeedWithISO8601Dates() async throws {
+ let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.variantRSS.utf8))
+
+ #expect(incidents.count == 1)
+ #expect(incidents[0].title == "Status feed moved")
+ #expect(incidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T15:30:00Z"))
+ }
+
+ @Test
func bannerSummaryPrefersSpecificServiceThenCount() {
let operational = SystemStatusSnapshot(
services: [
@@ -102,6 +123,32 @@ struct SystemStatusServiceTests {
</html>
"""#
+ private static let variantHTML = #"""
+ <html>
+ <body>
+ <div class="component extra" data-status="ok">
+ <a class="no-underline" href="/affected/meta.sr.ht/">meta.sr.ht</a>
+ <small class="component-status secondary">All systems operational</small>
+ </div>
+ <div data-status="down" class="extra component">
+ <a href="/affected/builds.sr.ht/" class="link">builds.sr.ht</a>
+ <div class="component-status badge">Outage</div>
+ </div>
+ <div class="announcement-box">
+ <div class="padding">
+ <p><a href="/issues/2026-04-07-builds-outage/"><strong>builds.sr.ht outage</strong></a></p>
+ <p><strong>Build jobs are currently failing.</strong></p>
+ </div>
+ </div>
+ <a class="issue no-underline urgent" href="/issues/2026-04-07-builds-outage/">
+ <time class="date" datetime="2026-04-07T12:00:00Z">Apr 7</time>
+ <h4>builds.sr.ht outage</h4>
+ <span>Investigating elevated failures</span>
+ </a>
+ </body>
+ </html>
+ """#
+
private static let sampleRSS = #"""
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0">
@@ -126,4 +173,20 @@ struct SystemStatusServiceTests {
</channel>
</rss>
"""#
+
+ private static let variantRSS = #"""
+ <?xml version="1.0" encoding="utf-8" standalone="yes"?>
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <channel>
+ <title>sr.ht status</title>
+ <item>
+ <title>Status feed moved</title>
+ <link>https://status.sr.ht/issues/2026-04-07-feed-moved/</link>
+ <dc:date>2026-04-07T15:30:00Z</dc:date>
+ <guid>https://status.sr.ht/issues/2026-04-07-feed-moved/</guid>
+ <description>&lt;p&gt;Use the new feed endpoint.&lt;/p&gt;</description>
+ </item>
+ </channel>
+ </rss>
+ """#
}