Files
hope-voice-api/Tests/MediaPlaybackTests.swift
T

90 lines
4.0 KiB
Swift

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)
}
}