import Foundation import Testing @testable import Voice private final class PlaybackProtocol: URLProtocol, @unchecked Sendable { static let lock = NSLock() nonisolated(unsafe) static var status = 200 nonisolated(unsafe) static var requests: [URLRequest] = [] override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { Self.lock.lock() Self.requests.append(request) let code = Self.status Self.lock.unlock() let response = HTTPURLResponse(url: request.url!, statusCode: code, httpVersion: nil, headerFields: ["Content-Type": "application/json"])! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: Data("{\"ok\":true}".utf8)) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} } @Suite(.serialized) @MainActor struct MediaPlaybackTests { private func controller(status: Int = 200) -> MediaPlayerController { PlaybackProtocol.lock.withLock { PlaybackProtocol.status = status PlaybackProtocol.requests = [] } let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [PlaybackProtocol.self] let url = URL(string: "https://media.test")! let api = MediaAPI(config: ServerConfig(baseURL: url, livekitURL: url), session: URLSession(configuration: configuration)) return MediaPlayerController(api: api, settings: SettingsStore()) } @Test("standalone playback sends stop to the default server room") func standaloneStop() async throws { let player = controller() await player.stop() let calls = PlaybackProtocol.lock.withLock { PlaybackProtocol.requests } let stop = try #require(calls.first { $0.url?.path == "/media/api/control" }) // URLSession may expose the encoded body as an InputStream to URLProtocol. let body: Data if let data = stop.httpBody { body = data } else { let stream = try #require(stop.httpBodyStream) stream.open() defer { stream.close() } var data = Data() var buffer = [UInt8](repeating: 0, count: 1024) while stream.hasBytesAvailable { let count = stream.read(&buffer, maxLength: buffer.count) if count <= 0 { break } data.append(contentsOf: buffer.prefix(count)) } body = data } let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any]) #expect(json["room"] as? String == "default") #expect(json["action"] as? String == "stop") } @Test("failed play requests are visible and a successful retry clears the error") func failureAndRetry() async { let player = controller(status: 503) await player.playTrack(id: "song") #expect(player.playbackError != nil) PlaybackProtocol.lock.withLock { PlaybackProtocol.status = 200 } await player.playTrack(id: "song") #expect(player.playbackError == nil) } @Test("metadata refreshes when a download becomes ready") func readyMetadata() { let player = controller() func state(_ status: String) -> MediaState { MediaState(room: "default", track: MediaTrack(id: "song", kind: "youtube", videoID: nil, title: status, channel: nil, duration: 180, status: status, error: nil, audioURL: nil, artURL: nil), desired: "paused", reported: nil, seekHint: nil, choices: [], pending: [], version: 1) } player.reconcile(state("downloading")) player.reconcile(state("ready")) #expect(player.track?.status == "ready") #expect(player.track?.title == "ready") player.resetScope() #expect(player.track == nil) #expect(player.player.currentItem == nil) } }