116 lines
5.7 KiB
Swift
116 lines
5.7 KiB
Swift
import XCTest
|
|
@testable import TVAnarchyCore
|
|
|
|
/// GAP-5 regression: the schema must stay strictly additive, because
|
|
/// `loadOrSeed()` OVERWRITES hosts.json on any decode failure. Old configs must
|
|
/// still decode; new ones decode with defaults; round-trips preserve fields.
|
|
final class DeviceConfigDecodeTests: XCTestCase {
|
|
func testLegacyConfigStillDecodes() throws {
|
|
let json = #"""
|
|
{"hosts":[
|
|
{"id":"plum-vlc","name":"Plum VLC","kind":"vlc","vlc":{"host":"127.0.0.1","port":8080}},
|
|
{"id":"black","name":"Black TV","kind":"blacktv",
|
|
"ssh":{"endpoints":["lilith@10.0.0.11","lilith@10.9.0.4"],"bin":"/usr/local/bin/black-tv"}}
|
|
]}
|
|
"""#
|
|
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
|
|
XCTAssertEqual(cfg.devices.count, 2)
|
|
XCTAssertEqual(cfg.devices[0].kind, .vlc)
|
|
XCTAssertEqual(cfg.devices[1].kind, .blacktv)
|
|
XCTAssertEqual(cfg.devices[1].ssh?.bin, "/usr/local/bin/black-tv")
|
|
}
|
|
|
|
func testMpvIPCConfigDecodesWithDefaults() throws {
|
|
// Minimal mpv block — socket/sudo/socat/volumeScale should default.
|
|
let json = #"""
|
|
{"hosts":[
|
|
{"id":"black","name":"Black TV","kind":"mpv-ipc",
|
|
"mpv":{"endpoints":["lilith@10.9.0.4"]},
|
|
"commands":{"launchShow":["btv","play-show","{query}"],"stop":["btv","stop"]}}
|
|
]}
|
|
"""#
|
|
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
|
|
let h = cfg.devices[0]
|
|
XCTAssertEqual(h.kind, .mpvIPC)
|
|
XCTAssertEqual(h.mpv?.socket, "/tmp/mpv.sock")
|
|
XCTAssertEqual(h.mpv?.sudo, true)
|
|
XCTAssertEqual(h.mpv?.socat, "socat")
|
|
XCTAssertEqual(h.mpv?.volumeScale, 130)
|
|
XCTAssertEqual(h.commands?.stop, ["btv", "stop"])
|
|
XCTAssertNil(h.commands?.releases) // unspecified capability → nil
|
|
}
|
|
|
|
@MainActor
|
|
func testLegacyBlacktvMigratesToMpvTarget() {
|
|
// An old `blacktv` host must still build a working target — the generic
|
|
// MpvTarget, with delegated commands derived from the old `bin`.
|
|
let h = DeviceConfig(id: "black", name: "Black", kind: .blacktv,
|
|
ssh: SSHConn(endpoints: ["lilith@10.9.0.4"], bin: "/usr/local/bin/black-tv"))
|
|
let t = PlayerController.makeTarget(h)
|
|
XCTAssertNotNil(t)
|
|
XCTAssertEqual(t?.kind, .mpvIPC)
|
|
XCTAssertTrue(t is MpvTarget)
|
|
}
|
|
|
|
func testSeedRoundTrips() throws {
|
|
let seed = DevicesConfig.seeded()
|
|
let data = try JSONEncoder().encode(seed)
|
|
let back = try JSONDecoder().decode(DevicesConfig.self, from: data)
|
|
XCTAssertEqual(back.devices.map(\.kind), seed.devices.map(\.kind))
|
|
XCTAssertEqual(back.devices.first(where: { $0.kind == .mpvIPC })?.commands?.launchFile,
|
|
["/usr/local/bin/black-tv", "play", "{path}"])
|
|
XCTAssertEqual(back.devices.map(\.type), [.laptop, .storage])
|
|
}
|
|
|
|
// MARK: device type + services (Part B)
|
|
|
|
/// A pre-`type` config must infer each device's type from its player backend,
|
|
/// NOT silently default to the first enum case: a local VLC → laptop, an
|
|
/// mpv/blacktv (black) → storage. Services follow the inferred type's preset.
|
|
func testLegacyConfigInfersDeviceType() throws {
|
|
let json = #"""
|
|
{"hosts":[
|
|
{"id":"plum-vlc","name":"Plum VLC","kind":"vlc","vlc":{"host":"127.0.0.1","port":8080}},
|
|
{"id":"black","name":"Black TV","kind":"mpv-ipc","mpv":{"endpoints":["lilith@10.0.0.11"]}}
|
|
]}
|
|
"""#
|
|
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
|
|
XCTAssertEqual(cfg.devices[0].type, .laptop) // local player
|
|
XCTAssertEqual(cfg.devices[1].type, .storage) // mpv-over-ssh server
|
|
XCTAssertTrue(cfg.devices[0].services.stream) // laptop streams
|
|
XCTAssertTrue(cfg.devices[1].services.stream) // storage ALSO streams (black)
|
|
XCTAssertTrue(cfg.devices[1].services.custody) // …and holds copies
|
|
}
|
|
|
|
func testDeviceTypePresets() {
|
|
XCTAssertEqual(DeviceType.cellphone.defaultServices,
|
|
DeviceServices(stream: true, offlineCache: true))
|
|
XCTAssertEqual(DeviceType.laptop.defaultServices,
|
|
DeviceServices(stream: true, offlineCache: true, ttlSeed: true))
|
|
// storage + seed: custody and stream are independent, not either/or.
|
|
XCTAssertTrue(DeviceType.storage.defaultServices.stream)
|
|
XCTAssertTrue(DeviceType.storage.defaultServices.custody)
|
|
XCTAssertFalse(DeviceType.seed.defaultServices.stream)
|
|
XCTAssertTrue(DeviceType.seed.defaultServices.publicSwarmFace)
|
|
// broadcast = the mesh anchor: registry + relay + swarm face, no streaming.
|
|
let b = DeviceType.broadcast.defaultServices
|
|
XCTAssertTrue(b.meshAnchor && b.f2fRelay && b.publicSwarmFace)
|
|
XCTAssertFalse(b.stream)
|
|
XCTAssertEqual(DeviceType.broadcast.fleetClass, "broadcast")
|
|
}
|
|
|
|
/// An explicit, overridden services set is preserved (not re-derived from type).
|
|
func testExplicitServicesOverrideSurvivesDecode() throws {
|
|
let json = #"""
|
|
{"devices":[
|
|
{"id":"phone","name":"Phone","kind":"quicktime","type":"cellphone",
|
|
"services":{"stream":true,"offlineCache":false,"ttlSeed":true}}
|
|
]}
|
|
"""#
|
|
let cfg = try JSONDecoder().decode(DevicesConfig.self, from: Data(json.utf8))
|
|
let s = cfg.devices[0].services
|
|
XCTAssertEqual(cfg.devices[0].type, .cellphone)
|
|
XCTAssertFalse(s.offlineCache) // override beat the preset's `true`
|
|
XCTAssertTrue(s.ttlSeed)
|
|
}
|
|
}
|