126 lines
4.5 KiB
Swift
126 lines
4.5 KiB
Swift
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<Int32>.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<sockaddr_in>.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<sockaddr_in>.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<sockaddr_in>.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..<bytesRead], encoding: .utf8) else { return }
|
|
|
|
var path = ""
|
|
if let firstLine = request.split(separator: "\r\n").first {
|
|
let parts = firstLine.split(separator: " ")
|
|
if parts.count >= 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")
|
|
}
|
|
}
|