krz/hutch

an ios client for sourcehut

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

main: HutchTests/BundleUserAgentTests.swift · raw

  1import Foundation
  2import Testing
  3@testable import Hutch
  4
  5// MARK: - URLProtocol stub
  6
  7/// Captures outgoing URLRequests and returns a minimal 401 so callers fail fast
  8/// without touching the real network.
  9private final class CapturingURLProtocol: URLProtocol, @unchecked Sendable {
 10    nonisolated(unsafe) static var capturedRequests: [URLRequest] = []
 11
 12    override class func canInit(with _: URLRequest) -> Bool { true }
 13    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
 14
 15    override func startLoading() {
 16        CapturingURLProtocol.capturedRequests.append(request)
 17        let response = HTTPURLResponse(
 18            url: request.url!,
 19            statusCode: 401,
 20            httpVersion: nil,
 21            headerFields: nil
 22        )!
 23        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
 24        client?.urlProtocol(self, didLoad: Data())
 25        client?.urlProtocolDidFinishLoading(self)
 26    }
 27
 28    override func stopLoading() {
 29        // No cleanup is needed because the stub responds immediately in `startLoading()`.
 30    }
 31
 32    static func makeSession() -> URLSession {
 33        let config = URLSessionConfiguration.ephemeral
 34        config.protocolClasses = [CapturingURLProtocol.self]
 35        return URLSession(configuration: config)
 36    }
 37}
 38
 39// MARK: - Tests
 40
 41/// Tests are serialized because CapturingURLProtocol uses shared static state.
 42@Suite(.serialized)
 43@MainActor
 44struct BundleUserAgentTests {
 45
 46    // MARK: Bundle extension
 47
 48    @Test
 49    func hutchUserAgentHasNameSlashVersion() {
 50        let ua = Bundle.main.hutchUserAgent
 51        let parts = ua.split(separator: "/", maxSplits: 1)
 52        #expect(parts.count == 2)
 53        #expect(parts[0] == "Hutch")
 54        #expect(!parts[1].isEmpty)
 55    }
 56
 57    @Test
 58    func hutchUserAgentContainsNoParenthesizedContext() {
 59        // The old SystemStatusService user-agent appended "(System Status)".
 60        // The shared agent should be plain "Hutch/<version>".
 61        #expect(!Bundle.main.hutchUserAgent.contains("("))
 62    }
 63
 64    // MARK: SRHTClient
 65
 66    @Test
 67    func sRHTClientSetsUserAgentOnExecute() async {
 68        CapturingURLProtocol.capturedRequests = []
 69        let client = SRHTClient(session: CapturingURLProtocol.makeSession(), token: "test-token")
 70
 71        _ = try? await client.execute(
 72            service: .builds,
 73            query: "{ jobs { results { id } } }",
 74            responseType: [String: String].self
 75        )
 76
 77        guard let captured = CapturingURLProtocol.capturedRequests.first else {
 78            Issue.record("No request was captured by SRHTClient.execute.")
 79            return
 80        }
 81        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
 82    }
 83
 84    @Test
 85    func sRHTClientSetsUserAgentOnFetchText() async throws {
 86        CapturingURLProtocol.capturedRequests = []
 87        let client = SRHTClient(session: CapturingURLProtocol.makeSession(), token: "test-token")
 88        let url = try #require(URL(string: "https://builds.sr.ht/~test/job/1/log"))
 89
 90        _ = try? await client.fetchText(url: url)
 91
 92        guard let captured = CapturingURLProtocol.capturedRequests.first else {
 93            Issue.record("No request was captured by SRHTClient.fetchText.")
 94            return
 95        }
 96        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
 97    }
 98
 99    // MARK: SystemStatusService
100
101    @Test
102    func systemStatusServiceSetsUserAgent() async {
103        CapturingURLProtocol.capturedRequests = []
104        let service = SystemStatusService(session: CapturingURLProtocol.makeSession())
105
106        _ = try? await service.fetchSnapshotHTML()
107
108        guard let captured = CapturingURLProtocol.capturedRequests.first else {
109            Issue.record("No request was captured by SystemStatusService.")
110            return
111        }
112        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
113    }
114
115    // MARK: HutchStatsService
116
117    @Test
118    func hutchStatsServiceSetsUserAgent() async {
119        CapturingURLProtocol.capturedRequests = []
120        let service = HutchStatsService(
121            session: CapturingURLProtocol.makeSession(),
122            configuration: AppConfiguration(environment: [:])
123        )
124
125        _ = try? await service.fetchContributionCalendar(actor: "testuser", endingOn: .now)
126
127        guard let captured = CapturingURLProtocol.capturedRequests.first else {
128            Issue.record("No request was captured by HutchStatsService.")
129            return
130        }
131        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
132    }
133}