Commit e6258f1dc1

e6258f1dc1307fc8def2d88a78dcc46ec8e06506

parent: 2dae2979da

Unsigned

cmc <hello@cleberg.net> · 2026-07-16 00:19 UTC

test: repair stale expectations and request-body capture

These tests drifted from the code and went unnoticed because CI never ran
them. All four are test-side errors; no app behavior is involved.

- TicketListViewModelTests asserted lowercase "resolved"/"fixed"/"reported"
  against TicketStatus/TicketResolution rawValues, which are uppercase to
  match the todo.sr.ht GraphQL enums.
- HomeViewModelTests expected failedBuilds in ascending id order. Ordering
  moved to newest-first when sortBuildItemsForTriage landed in 165f0cf; the
  filtering the test covers is unchanged.
- SettingsViewModelTests read request.httpBody inside a URLProtocol, where it
  is always nil because URLSession moves the body onto httpBodyStream. The
  stub now reads the body off the stream at capture time.

Layout: unified · split

HutchTests/HomeViewModelTests.swift +1 −1
@@ -15,7 +15,7 @@ struct HomeViewModelTests {
15 15
16 let failedBuilds = HomeViewModel.failedBuilds(from: jobs) 16 let failedBuilds = HomeViewModel.failedBuilds(from: jobs)
17 17
18 #expect(failedBuilds.map(\.job.id) == [2, 3]) 18 #expect(failedBuilds.map(\.job.id) == [3, 2])
19 } 19 }
20 20
21 @Test 21 @Test
HutchTests/SettingsViewModelTests.swift +24 −2
@@ -4,12 +4,14 @@ import Testing
4 4
5private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @unchecked Sendable { 5private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @unchecked Sendable {
6 nonisolated(unsafe) static var capturedRequests: [URLRequest] = [] 6 nonisolated(unsafe) static var capturedRequests: [URLRequest] = []
7 nonisolated(unsafe) static var capturedBodies: [Data] = []
7 8
8 override class func canInit(with _: URLRequest) -> Bool { true } 9 override class func canInit(with _: URLRequest) -> Bool { true }
9 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } 10 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
10 11
11 override func startLoading() { 12 override func startLoading() {
12 Self.capturedRequests.append(request) 13 Self.capturedRequests.append(request)
14 Self.capturedBodies.append(Self.readBody(from: request))
13 15
14 let response = HTTPURLResponse( 16 let response = HTTPURLResponse(
15 url: request.url!, 17 url: request.url!,
@@ -26,6 +28,26 @@ private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @uncheck
26 // No cleanup is needed because the stub responds immediately in `startLoading()`. 28 // No cleanup is needed because the stub responds immediately in `startLoading()`.
27 } 29 }
28 30
31 /// `URLSession` moves `httpBody` onto `httpBodyStream` before handing a request
32 /// to a `URLProtocol`, so `request.httpBody` is always nil here and the body has
33 /// to be read back off the stream while it is still open.
34 private static func readBody(from request: URLRequest) -> Data {
35 if let body = request.httpBody { return body }
36 guard let stream = request.httpBodyStream else { return Data() }
37
38 stream.open()
39 defer { stream.close() }
40
41 var data = Data()
42 var buffer = [UInt8](repeating: 0, count: 4096)
43 while stream.hasBytesAvailable {
44 let read = stream.read(&buffer, maxLength: buffer.count)
45 guard read > 0 else { break }
46 data.append(buffer, count: read)
47 }
48 return data
49 }
50
29 static func makeSession() -> URLSession { 51 static func makeSession() -> URLSession {
30 let config = URLSessionConfiguration.ephemeral 52 let config = URLSessionConfiguration.ephemeral
31 config.protocolClasses = [Self.self] 53 config.protocolClasses = [Self.self]
@@ -73,6 +95,7 @@ struct SettingsViewModelTests {
73 @MainActor 95 @MainActor
74 func loadProfileDoesNotRequestSSHKeyFingerprintField() async throws { 96 func loadProfileDoesNotRequestSSHKeyFingerprintField() async throws {
75 SettingsViewModelCapturingURLProtocol.capturedRequests = [] 97 SettingsViewModelCapturingURLProtocol.capturedRequests = []
98 SettingsViewModelCapturingURLProtocol.capturedBodies = []
76 99
77 let client = SRHTClient( 100 let client = SRHTClient(
78 session: SettingsViewModelCapturingURLProtocol.makeSession(), 101 session: SettingsViewModelCapturingURLProtocol.makeSession(),
@@ -82,8 +105,7 @@ struct SettingsViewModelTests {
82 105
83 await viewModel.loadProfile() 106 await viewModel.loadProfile()
84 107
85 let request = try #require(SettingsViewModelCapturingURLProtocol.capturedRequests.first) 108 let body = try #require(SettingsViewModelCapturingURLProtocol.capturedBodies.first)
86 let body = try #require(request.httpBody)
87 let jsonObject = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any]) 109 let jsonObject = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
88 let query = try #require(jsonObject["query"] as? String) 110 let query = try #require(jsonObject["query"] as? String)
89 111
HutchTests/TicketListViewModelTests.swift +3 −3
@@ -162,8 +162,8 @@ struct TicketListViewModelTests {
162 "status": TicketStatus.resolved.rawValue, 162 "status": TicketStatus.resolved.rawValue,
163 "resolution": TicketResolution.fixed.rawValue 163 "resolution": TicketResolution.fixed.rawValue
164 ] 164 ]
165 #expect(input["status"] as? String == "resolved") 165 #expect(input["status"] as? String == "RESOLVED")
166 #expect(input["resolution"] as? String == "fixed") 166 #expect(input["resolution"] as? String == "FIXED")
167 } 167 }
168 168
169 @Test 169 @Test
@@ -172,7 +172,7 @@ struct TicketListViewModelTests {
172 let input: [String: any Sendable] = [ 172 let input: [String: any Sendable] = [
173 "status": TicketStatus.reported.rawValue 173 "status": TicketStatus.reported.rawValue
174 ] 174 ]
175 #expect(input["status"] as? String == "reported") 175 #expect(input["status"] as? String == "REPORTED")
176 #expect(input["resolution"] == nil) 176 #expect(input["resolution"] == nil)
177 } 177 }
178 178