Christopher Hotchkiss Christopher Hotchkiss
Login

Code

remindwall / RWPureSwift / Tests / ScreenControlTests / DdcdClientTests.swift

Back to remindwall — Current RemindWall

Download this file

import Foundation
import Testing

@testable import ScreenControl

/// Intercepts DdcdClient's URLSession traffic. One static handler at a time —
/// the suite is serialized.
final class StubProtocol: URLProtocol {
    struct Recorded: Sendable {
        let method: String
        let path: String
        let guardHeader: String?
        let body: Data?
    }

    nonisolated(unsafe) static var handler: (@Sendable (Recorded) throws -> (status: Int, body: Data))?
    nonisolated(unsafe) static var recordings: [Recorded] = []

    override class func canInit(with request: URLRequest) -> Bool { true }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
    override func stopLoading() {}

    override func startLoading() {
        // URLSession moves httpBody into a stream before URLProtocol sees it.
        var bodyData: Data?
        if let body = request.httpBody {
            bodyData = body
        } else if let stream = request.httpBodyStream {
            var data = Data()
            stream.open()
            defer { stream.close() }
            let bufferSize = 1024
            let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
            defer { buffer.deallocate() }
            while stream.hasBytesAvailable {
                let read = stream.read(buffer, maxLength: bufferSize)
                if read <= 0 { break }
                data.append(buffer, count: read)
            }
            bodyData = data
        }

        let recorded = Recorded(
            method: request.httpMethod ?? "?",
            path: request.url?.path ?? "?",
            guardHeader: request.value(forHTTPHeaderField: DdcdClient.guardHeader),
            body: bodyData
        )
        Self.recordings.append(recorded)

        guard let handler = Self.handler else {
            client?.urlProtocol(self, didFailWithError: URLError(.unsupportedURL))
            return
        }
        do {
            let (status, body) = try handler(recorded)
            let response = HTTPURLResponse(
                url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil
            )!
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: body)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
}

@Suite("DdcdClient", .serialized)
struct DdcdClientTests {
    private func makeClient() -> DdcdClient {
        StubProtocol.handler = nil
        StubProtocol.recordings = []
        let config = URLSessionConfiguration.ephemeral
        config.protocolClasses = [StubProtocol.self]
        return DdcdClient(
            baseURL: URL(string: "http://127.0.0.1:9999")!,
            session: URLSession(configuration: config)
        )
    }

    @Test("getBrightness parses the daemon response and sends the guard header")
    func getBrightness() async throws {
        let client = makeClient()
        StubProtocol.handler = { _ in
            (200, Data(#"{"brightness":0.43,"raw":43,"max":100}"#.utf8))
        }

        let value = try await client.getBrightness()
        #expect(abs(value - 0.43) < 1e-9)

        let recorded = try #require(StubProtocol.recordings.first)
        #expect(recorded.method == "GET")
        #expect(recorded.path == "/brightness")
        #expect(recorded.guardHeader == "1")
    }

    @Test("daemon errors surface with status and message, never a fabricated value")
    func daemonErrorSurfaces() async {
        let client = makeClient()
        StubProtocol.handler = { _ in
            (502, Data(#"{"error":"Could not find a suitable external display."}"#.utf8))
        }

        do {
            _ = try await client.getBrightness()
            Issue.record("expected a throw")
        } catch let ScreenControlError.daemonError(status, message) {
            #expect(status == 502)
            #expect(message.contains("external display"))
        } catch {
            Issue.record("wrong error: \(error)")
        }
    }

    @Test("setBrightness PUTs scaled JSON")
    func setBrightness() async throws {
        let client = makeClient()
        StubProtocol.handler = { _ in (204, Data()) }

        try await client.setBrightness(0.5)

        let recorded = try #require(StubProtocol.recordings.first)
        #expect(recorded.method == "PUT")
        #expect(recorded.path == "/brightness")
        let body = try #require(recorded.body)
        let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Double])
        #expect(json["brightness"] == 0.5)
    }

    @Test("setDisplayPower PUTs the on flag to /display")
    func setDisplayPower() async throws {
        let client = makeClient()
        StubProtocol.handler = { _ in (204, Data()) }

        try await client.setDisplayPower(on: false)

        let recorded = try #require(StubProtocol.recordings.first)
        #expect(recorded.method == "PUT")
        #expect(recorded.path == "/display")
        let body = try #require(recorded.body)
        let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Bool])
        #expect(json["on"] == false)
    }

    @Test("healthy requires ok status AND a present binary")
    func healthy() async {
        let client = makeClient()

        StubProtocol.handler = { _ in
            (200, Data(#"{"status":"ok","m1ddc_present":true}"#.utf8))
        }
        #expect(await client.healthy() == true)

        StubProtocol.handler = { _ in
            (200, Data(#"{"status":"ok","m1ddc_present":false}"#.utf8))
        }
        #expect(await client.healthy() == false)

        StubProtocol.handler = { _ in throw URLError(.cannotConnectToHost) }
        #expect(await client.healthy() == false)
    }

    @Test("unreachable daemon throws daemonUnreachable")
    func unreachable() async {
        let client = makeClient()
        StubProtocol.handler = { _ in throw URLError(.cannotConnectToHost) }

        do {
            _ = try await client.getBrightness()
            Issue.record("expected a throw")
        } catch ScreenControlError.daemonUnreachable {
            // expected
        } catch {
            Issue.record("wrong error: \(error)")
        }
    }
}