90 lines
4.0 KiB
Swift
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)
|
|
}
|
|
}
|