krz/hutch

an ios client for sourcehut

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

v3.9.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}