status
This commit is contained in:
1 parent
c9c25f86eb
commit
40aaf4fea7
56 files changed
+5683
-179
No files matched your search
@@ -0,0 +1,125 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user