krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.8.0: HutchTests/SettingsViewModelTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5private final class SettingsViewModelCapturingURLProtocol: URLProtocol, @unchecked Sendable {
6 nonisolated(unsafe) static var capturedRequests: [URLRequest] = []
7 nonisolated(unsafe) static var capturedBodies: [Data] = []
8
9 override class func canInit(with _: URLRequest) -> Bool { true }
10 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
11
12 override func startLoading() {
13 Self.capturedRequests.append(request)
14 Self.capturedBodies.append(Self.readBody(from: request))
15
16 let response = HTTPURLResponse(
17 url: request.url!,
18 statusCode: 401,
19 httpVersion: nil,
20 headerFields: nil
21 )!
22 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
23 client?.urlProtocol(self, didLoad: Data())
24 client?.urlProtocolDidFinishLoading(self)
25 }
26
27 override func stopLoading() {
28 // No cleanup is needed because the stub responds immediately in `startLoading()`.
29 }
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
51 static func makeSession() -> URLSession {
52 let config = URLSessionConfiguration.ephemeral
53 config.protocolClasses = [Self.self]
54 return URLSession(configuration: config)
55 }
56}
57
58private struct DeletePGPKeyEnvelope: Decodable {
59 let deletePGPKey: DeleteResultPayload?
60}
61
62private struct DeleteResultPayload: Decodable {
63 let id: Int?
64}
65
66@Suite(.serialized)
67struct SettingsViewModelTests {
68
69 @Test
70 @MainActor
71 func deletePGPKeyResponseDecodesNullPayloadWithGraphQLErrors() throws {
72 let json = """
73 {
74 "errors": [
75 {
76 "message": "PGP key ID 13629 is set as the user's preferred PGP key - it must be unset before removing the key"
77 }
78 ],
79 "data": {
80 "deletePGPKey": null
81 }
82 }
83 """
84
85 let decoded = try JSONDecoder().decode(
86 GraphQLResponse<DeletePGPKeyEnvelope>.self,
87 from: Data(json.utf8)
88 )
89
90 #expect(decoded.data?.deletePGPKey == nil)
91 #expect(decoded.errors?.first?.message.contains("preferred PGP key") == true)
92 }
93
94 @Test
95 @MainActor
96 func loadProfileDoesNotRequestSSHKeyFingerprintField() async throws {
97 SettingsViewModelCapturingURLProtocol.capturedRequests = []
98 SettingsViewModelCapturingURLProtocol.capturedBodies = []
99
100 let client = SRHTClient(
101 session: SettingsViewModelCapturingURLProtocol.makeSession(),
102 token: "test-token"
103 )
104 let viewModel = SettingsViewModel(client: client)
105
106 await viewModel.loadProfile()
107
108 let body = try #require(SettingsViewModelCapturingURLProtocol.capturedBodies.first)
109 let jsonObject = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
110 let query = try #require(jsonObject["query"] as? String)
111
112 #expect(query.contains("sshKeys"))
113 #expect(!query.contains("results { id fingerprint comment created lastUsed }"))
114 #expect(!query.contains("fingerprint comment created lastUsed"))
115 }
116}