| @@ -4,12 +4,14 @@ import Testing |
| 4 | |
4 | |
| 5 | private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @unchecked Sendable { |
5 | private 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 | |