From cb1ea5ea4f163d87053285d3fb7999b12a3558b9 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sun, 12 Apr 2026 00:15:13 -0500 Subject: harden system status and app reliability --- HutchTests/SystemStatusRepositoryTests.swift | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 HutchTests/SystemStatusRepositoryTests.swift (limited to 'HutchTests/SystemStatusRepositoryTests.swift') 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 = #""" +
+ git.sr.ht + Disrupted +
+ """# + + static let cachedOperationalHTML = #""" +
+ meta.sr.ht + Operational +
+ """# + + static let cachedIncidentRSS = #""" + + + + builds.sr.ht outage + https://status.sr.ht/issues/1/ + Thu, 01 Jan 1970 00:03:20 +0000 + incident-1 + <p>Builds are failing.</p> + + + + """# + + static let emptyRSS = #""" + + + + """# +} -- cgit v1.2.3