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}