import Foundation import Testing @testable import Voice /// Minimal HTTP/1.1 stub built on BSD sockets that answers every request with a /// successful JSON object body (mirroring the real media server's /// `{"ok": true, ...}` shape). Used to prove MediaAPI no-response POSTs succeed /// against an object body instead of throwing decodeFailed. @MainActor final class MediaAPIStubServer { private(set) var url: URL! nonisolated(unsafe) private var serverFD: Int32 = -1 nonisolated(unsafe) private var acceptThread: Thread? nonisolated(unsafe) private var running = false nonisolated(unsafe) private var lastPath: String? func start() throws { let fd = socket(AF_INET, SOCK_STREAM, 0) guard fd >= 0 else { throw MediaAPIError.decodeFailed } var yes: Int32 = 1 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout.size)) var addr = sockaddr_in() addr.sin_family = sa_family_t(AF_INET) addr.sin_port = in_port_t(0).bigEndian addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1")) let bindResult = withUnsafePointer(to: &addr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in bind(fd, sa, socklen_t(MemoryLayout.size)) } } guard bindResult == 0 else { close(fd); throw MediaAPIError.decodeFailed } guard listen(fd, 8) == 0 else { close(fd); throw MediaAPIError.decodeFailed } var bound = sockaddr_in() var len = socklen_t(MemoryLayout.size) withUnsafeMutablePointer(to: &bound) { ptr in _ = ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in getsockname(fd, sa, &len) } } let port = Int(UInt16(bigEndian: bound.sin_port)) serverFD = fd running = true url = URL(string: "http://127.0.0.1:\(port)")! let thread = Thread { [weak self] in self?.acceptLoop() } acceptThread = thread thread.start() } func stop() { running = false if serverFD >= 0 { close(serverFD) } serverFD = -1 acceptThread?.cancel() } var lastRequestPath: String? { lastPath } nonisolated private func acceptLoop() { while running { var clientAddr = sockaddr_in() var len = socklen_t(MemoryLayout.size) let clientFD = withUnsafeMutablePointer(to: &clientAddr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in accept(serverFD, sa, &len) } } guard clientFD >= 0 else { continue } handleClient(clientFD) } } nonisolated private func handleClient(_ clientFD: Int32) { defer { close(clientFD) } var buffer = [UInt8](repeating: 0, count: 4096) let bytesRead = read(clientFD, &buffer, buffer.count) guard bytesRead > 0 else { return } guard let request = String(bytes: buffer[0..= 2 { path = String(parts[1]) } } lastPath = path let body = #"{"ok": true, "room": "test", "desired": "stopped"}"# let response = """ HTTP/1.1 200 OK\r Content-Type: application/json\r Content-Length: \(body.utf8.count)\r Connection: close\r \(body) """ _ = response.withCString { cstr in write(clientFD, cstr, response.utf8.count) } } } @MainActor struct MediaAPITests { @Test("successful JSON object response does not throw decodeFailed") func successfulObjectResponse() async throws { let stub = MediaAPIStubServer() try stub.start() defer { stub.stop() } // Bypass the https-only parse path: build the config directly. let base = stub.url! let config = ServerConfig(baseURL: base, livekitURL: base) let api = MediaAPI(config: config) try await api.control(room: "test", action: "pause") try await api.report(room: "test", state: "paused", trackID: nil, position: 0, duration: nil) try await api.play(room: "test", videoID: nil, title: nil, channel: nil, trackID: nil, startPaused: nil) #expect(stub.lastRequestPath == "/media/api/play") } }