krz/hutch

an ios client for sourcehut

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

4346adc50f01be7f7322249d32745d119c757ff2

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-07-16T00:19:43Z

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 5f6d545; 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.
 HutchTests/HomeViewModelTests.swift       |  2 +-
 HutchTests/SettingsViewModelTests.swift   | 26 ++++++++++++++++++++++++--
 HutchTests/TicketListViewModelTests.swift |  6 +++---
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/HutchTests/HomeViewModelTests.swift b/HutchTests/HomeViewModelTests.swift
index e4f6d9a..851b520 100644
--- a/HutchTests/HomeViewModelTests.swift
+++ b/HutchTests/HomeViewModelTests.swift
@@ -15,7 +15,7 @@ struct HomeViewModelTests {
 
         let failedBuilds = HomeViewModel.failedBuilds(from: jobs)
 
-        #expect(failedBuilds.map(\.job.id) == [2, 3])
+        #expect(failedBuilds.map(\.job.id) == [3, 2])
     }
 
     @Test
diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift
index 9e4b912..906f33f 100644
--- a/HutchTests/SettingsViewModelTests.swift
+++ b/HutchTests/SettingsViewModelTests.swift
@@ -4,12 +4,14 @@ import Testing
 
 private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @unchecked Sendable {
     nonisolated(unsafe) static var capturedRequests: [URLRequest] = []
+    nonisolated(unsafe) static var capturedBodies: [Data] = []
 
     override class func canInit(with _: URLRequest) -> Bool { true }
     override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
 
     override func startLoading() {
         Self.capturedRequests.append(request)
+        Self.capturedBodies.append(Self.readBody(from: request))
 
         let response = HTTPURLResponse(
             url: request.url!,
@@ -26,6 +28,26 @@ private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @uncheck
         // No cleanup is needed because the stub responds immediately in `startLoading()`.
     }
 
+    /// `URLSession` moves `httpBody` onto `httpBodyStream` before handing a request
+    /// to a `URLProtocol`, so `request.httpBody` is always nil here and the body has
+    /// to be read back off the stream while it is still open.
+    private static func readBody(from request: URLRequest) -> Data {
+        if let body = request.httpBody { return body }
+        guard let stream = request.httpBodyStream else { return Data() }
+
+        stream.open()
+        defer { stream.close() }
+
+        var data = Data()
+        var buffer = [UInt8](repeating: 0, count: 4096)
+        while stream.hasBytesAvailable {
+            let read = stream.read(&buffer, maxLength: buffer.count)
+            guard read > 0 else { break }
+            data.append(buffer, count: read)
+        }
+        return data
+    }
+
     static func makeSession() -> URLSession {
         let config = URLSessionConfiguration.ephemeral
         config.protocolClasses = [Self.self]
@@ -73,6 +95,7 @@ struct SettingsViewModelTests {
     @MainActor
     func loadProfileDoesNotRequestSSHKeyFingerprintField() async throws {
         SettingsViewModelCapturingURLProtocol.capturedRequests = []
+        SettingsViewModelCapturingURLProtocol.capturedBodies = []
 
         let client = SRHTClient(
             session: SettingsViewModelCapturingURLProtocol.makeSession(),
@@ -82,8 +105,7 @@ struct SettingsViewModelTests {
 
         await viewModel.loadProfile()
 
-        let request = try #require(SettingsViewModelCapturingURLProtocol.capturedRequests.first)
-        let body = try #require(request.httpBody)
+        let body = try #require(SettingsViewModelCapturingURLProtocol.capturedBodies.first)
         let jsonObject = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
         let query = try #require(jsonObject["query"] as? String)
 
diff --git a/HutchTests/TicketListViewModelTests.swift b/HutchTests/TicketListViewModelTests.swift
index 7fa5449..a203091 100644
--- a/HutchTests/TicketListViewModelTests.swift
+++ b/HutchTests/TicketListViewModelTests.swift
@@ -162,8 +162,8 @@ struct TicketListViewModelTests {
             "status": TicketStatus.resolved.rawValue,
             "resolution": TicketResolution.fixed.rawValue
         ]
-        #expect(input["status"] as? String == "resolved")
-        #expect(input["resolution"] as? String == "fixed")
+        #expect(input["status"] as? String == "RESOLVED")
+        #expect(input["resolution"] as? String == "FIXED")
     }
 
     @Test
@@ -172,7 +172,7 @@ struct TicketListViewModelTests {
         let input: [String: any Sendable] = [
             "status": TicketStatus.reported.rawValue
         ]
-        #expect(input["status"] as? String == "reported")
+        #expect(input["status"] as? String == "REPORTED")
         #expect(input["resolution"] == nil)
     }