import XCTest @testable import MusicBridge @MainActor final class SystemPlayerTests: XCTestCase { private func state(_ changes: [String: Any] = [:]) throws -> MediaState { let url = try XCTUnwrap(Bundle(for: Self.self).url(forResource: "state-v1", withExtension: "json")) var json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: url)) as? [String: Any]) changes.forEach { json[$0.key] = $0.value } return try JSONDecoder().decode(MediaState.self, from: JSONSerialization.data(withJSONObject: json)) } func testSeekConvertsRelativePositionAndClampsToAllowedRange() throws { let state = try state(["startSeconds": 100, "endSeconds": 300, "minSeekSeconds": 110, "maxSeekSeconds": 280]) XCTAssertEqual(try SystemPlayerTransport.command(.seek, value: 30, state: state).value, 130) XCTAssertEqual(try SystemPlayerTransport.command(.seek, value: -50, state: state).value, 110) XCTAssertEqual(try SystemPlayerTransport.command(.seek, value: 500, state: state).value, 280) XCTAssertThrowsError(try SystemPlayerTransport.command(.seek, value: .nan, state: state)) } func testVolumeBoundsAndUnsupportedPlayback() throws { let state = try state() XCTAssertEqual(try SystemPlayerTransport.command(.volume, value: 2, state: state).value, 1) XCTAssertEqual(try SystemPlayerTransport.command(.volume, value: -1, state: state).value, 0) XCTAssertThrowsError(try SystemPlayerTransport.command(.volume, value: .infinity, state: state)) XCTAssertThrowsError(try SystemPlayerTransport.command(.play, value: nil, state: state)) XCTAssertThrowsError(try SystemPlayerTransport.command(.next, value: nil, state: self.state(["hasSession": false]))) } func testRemoteSnapshotContainsOnlyPublicPlaybackMetadata() throws { guard #available(iOS 27.0, *) else { throw XCTSkip("Requires NowPlaying") } let snapshot = SystemPlayerAttributes(id: "session", state: try state(["positionSeconds": 130, "startSeconds": 100, "endSeconds": 300])) XCTAssertEqual(snapshot.position, 30) XCTAssertEqual(snapshot.duration, 200) let json = try XCTUnwrap(JSONSerialization.jsonObject(with: JSONEncoder().encode(snapshot)) as? [String: Any]) XCTAssertEqual(Set(json.keys), Set(["id", "title", "artist", "source", "playing", "rate", "position", "duration", "timestamp", "capabilities", "volume", "hasSession"])) } func testRevokedSessionDoesNotSendCommandAndNextIsSentOnce() async throws { let url = try XCTUnwrap(Bundle(for: Self.self).url(forResource: "tls-fixture", withExtension: "json")) let fixtures = try JSONDecoder().decode([String: [String: String]].self, from: Data(contentsOf: url)) let f = try XCTUnwrap(fixtures["valid"]) let client = BridgeClient(try Connection(endpoint: XCTUnwrap(f["endpoint"]), fingerprint: XCTUnwrap(f["fingerprint"]), token: "live-system-next")) defer { client.close() } do { _ = try await SystemPlayerTransport.execute(.next, client: client) { throw BridgeError.unauthorized } XCTFail("Revoked session must be rejected") } catch BridgeError.unauthorized { } let unchanged = try await client.state() XCTAssertEqual(unchanged.title, "Track 0") let updated = try await SystemPlayerTransport.execute(.next, client: client) XCTAssertEqual(updated.title, "Track 1") } }