krz/hutch

an ios client for sourcehut

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

v3.2.0: 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)
 43struct BundleUserAgentTests {
 44
 45    // MARK: Bundle extension
 46
 47    @Test
 48    func hutchUserAgentHasNameSlashVersion() {
 49        let ua = Bundle.main.hutchUserAgent
 50        let parts = ua.split(separator: "/", maxSplits: 1)
 51        #expect(parts.count == 2)
 52        #expect(parts[0] == "Hutch")
 53        #expect(!parts[1].isEmpty)
 54    }
 55
 56    @Test
 57    func hutchUserAgentContainsNoParenthesizedContext() {
 58        // The old SystemStatusService user-agent appended "(System Status)".
 59        // The shared agent should be plain "Hutch/<version>".
 60        #expect(!Bundle.main.hutchUserAgent.contains("("))
 61    }
 62
 63    // MARK: SRHTClient
 64
 65    @Test
 66    func sRHTClientSetsUserAgentOnExecute() async {
 67        CapturingURLProtocol.capturedRequests = []
 68        let client = SRHTClient(session: CapturingURLProtocol.makeSession(), token: "test-token")
 69
 70        _ = try? await client.execute(
 71            service: .builds,
 72            query: "{ jobs { results { id } } }",
 73            responseType: [String: String].self
 74        )
 75
 76        guard let captured = CapturingURLProtocol.capturedRequests.first else {
 77            Issue.record("No request was captured by SRHTClient.execute.")
 78            return
 79        }
 80        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
 81    }
 82
 83    @Test
 84    func sRHTClientSetsUserAgentOnFetchText() async throws {
 85        CapturingURLProtocol.capturedRequests = []
 86        let client = SRHTClient(session: CapturingURLProtocol.makeSession(), token: "test-token")
 87        let url = try #require(URL(string: "https://builds.sr.ht/~test/job/1/log"))
 88
 89        _ = try? await client.fetchText(url: url)
 90
 91        guard let captured = CapturingURLProtocol.capturedRequests.first else {
 92            Issue.record("No request was captured by SRHTClient.fetchText.")
 93            return
 94        }
 95        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
 96    }
 97
 98    // MARK: SystemStatusService
 99
100    @Test
101    func systemStatusServiceSetsUserAgent() async {
102        CapturingURLProtocol.capturedRequests = []
103        let service = SystemStatusService(session: CapturingURLProtocol.makeSession())
104
105        _ = try? await service.fetchSnapshotHTML()
106
107        guard let captured = CapturingURLProtocol.capturedRequests.first else {
108            Issue.record("No request was captured by SystemStatusService.")
109            return
110        }
111        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
112    }
113
114    // MARK: HutchStatsService
115
116    @Test
117    func hutchStatsServiceSetsUserAgent() async {
118        CapturingURLProtocol.capturedRequests = []
119        let service = HutchStatsService(
120            session: CapturingURLProtocol.makeSession(),
121            configuration: AppConfiguration(environment: [:])
122        )
123
124        _ = try? await service.fetchContributionCalendar(actor: "testuser", endingOn: .now)
125
126        guard let captured = CapturingURLProtocol.capturedRequests.first else {
127            Issue.record("No request was captured by HutchStatsService.")
128            return
129        }
130        #expect(captured.value(forHTTPHeaderField: "User-Agent") == Bundle.main.hutchUserAgent)
131    }
132}