diff options
Diffstat (limited to 'HutchTests')
| -rw-r--r-- | HutchTests/AppStateTests.swift | 11 | ||||
| -rw-r--r-- | HutchTests/SystemStatusRepositoryTests.swift | 147 | ||||
| -rw-r--r-- | HutchTests/SystemStatusServiceTests.swift | 63 |
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><p>Builds are failing.</p></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><p>Use the new feed endpoint.</p></description> + </item> + </channel> + </rss> + """# } |
