krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.3.1: 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}