147 lines
6.9 KiB
Swift
147 lines
6.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
import RemoteCore
|
|
import TVTransport
|
|
@testable import PhoneControl
|
|
|
|
private actor FakeTV: PhoneTVControlling {
|
|
var paired: [TelevisionTarget] = []
|
|
var connected: [TelevisionTarget] = []
|
|
var commands: [RemoteCommand] = []
|
|
var disconnects = 0
|
|
var pairHook: (@Sendable () async -> Void)?
|
|
func setPairHook(_ hook: @escaping @Sendable () async -> Void) { pairHook = hook }
|
|
var status: CommandReceipt.Status = .sentToTransport
|
|
func pair(target: TelevisionTarget, sessionID: UUID) async -> TVConnectionState {
|
|
paired.append(target)
|
|
if let pairHook { await pairHook(); return .untrusted(televisionID: target.id) }
|
|
return .ready(televisionID: target.id)
|
|
}
|
|
func connect(target: TelevisionTarget, sessionID: UUID) async -> TVConnectionState {
|
|
connected.append(target); return .ready(televisionID: target.id)
|
|
}
|
|
func disconnect() async { disconnects += 1 }
|
|
func stateUpdates() async -> AsyncStream<TVConnectionState> { AsyncStream { $0.finish() } }
|
|
func sendPhoneCommand(_ command: RemoteCommand) async -> CommandReceipt {
|
|
commands.append(command); return .init(id: UUID(), status: status)
|
|
}
|
|
func failWrites() { status = .unavailable }
|
|
}
|
|
|
|
private actor MemorySelection: SelectedTelevisionStoring {
|
|
var value: SelectedTelevision?
|
|
var failSave = false
|
|
init(_ value: SelectedTelevision? = nil, failSave: Bool = false) { self.value = value; self.failSave = failSave }
|
|
func load() async throws -> SelectedTelevision? { value }
|
|
func save(_ television: SelectedTelevision) async throws {
|
|
if failSave { throw PhoneControlError.storageUnavailable }; value = television
|
|
}
|
|
func delete() async throws { value = nil }
|
|
}
|
|
|
|
private actor MemoryAuthorization: PhoneAuthorizationManaging {
|
|
var forgotten: [UUID] = []
|
|
var approvals = 0
|
|
let failDelete: Bool
|
|
init(failDelete: Bool = false) { self.failDelete = failDelete }
|
|
func approveCertificate(sha256: Data, for televisionID: UUID, approvedAt: Date) async throws { approvals += 1 }
|
|
func forgetTelevision(_ televisionID: UUID) async throws {
|
|
if failDelete { throw PhoneControlError.storageUnavailable }; forgotten.append(televisionID)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
struct PhoneControlTests {
|
|
@Test func restorationUsesSavedIdentityAndNeverPairs() async {
|
|
let selected = SelectedTelevision(host: "tv.local")
|
|
let tv = FakeTV()
|
|
let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(selected))
|
|
await controller.start()
|
|
#expect(controller.television == selected)
|
|
#expect(controller.isReady)
|
|
#expect(await tv.paired.isEmpty)
|
|
#expect(await tv.connected.map(\.id) == [selected.id])
|
|
await controller.pair(address: selected.host)
|
|
#expect(await tv.paired.map(\.id) == [selected.id])
|
|
}
|
|
|
|
@Test func storageFailurePreventsPairingAndDifferentTargetRequiresForget() async {
|
|
let tv = FakeTV()
|
|
let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(failSave: true))
|
|
await controller.pair(address: "tv.local")
|
|
#expect(await tv.paired.isEmpty)
|
|
#expect(controller.error != nil)
|
|
let existing = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(.init(host: "old.local")))
|
|
await existing.pair(address: "new.local")
|
|
#expect(await tv.paired.isEmpty)
|
|
#expect(existing.television?.host == "old.local")
|
|
}
|
|
|
|
@Test func partialForgetKeepsIdentityForDeletionRetryAndDisconnects() async throws {
|
|
let selected = SelectedTelevision(host: "tv.local")
|
|
let store = MemorySelection(selected)
|
|
let tv = FakeTV()
|
|
let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(failDelete: true), selection: store)
|
|
await controller.start()
|
|
await controller.forget()
|
|
#expect(!controller.isReady)
|
|
#expect(controller.error != nil)
|
|
#expect(try await store.load() == selected)
|
|
#expect(await tv.disconnects == 1)
|
|
}
|
|
|
|
@Test func forgetDeletesSecurityAndSelection() async throws {
|
|
let selected = SelectedTelevision(host: "tv.local")
|
|
let store = MemorySelection(selected)
|
|
let auth = MemoryAuthorization()
|
|
let controller = LivePhoneController(transport: FakeTV(), authorization: auth, selection: store)
|
|
await controller.start(); await controller.forget()
|
|
#expect(controller.television == nil)
|
|
#expect(try await store.load() == nil)
|
|
#expect(await auth.forgotten == [selected.id])
|
|
}
|
|
|
|
@Test func ambiguousWriteIsNotReplayedAndDemoDisconnectsLiveRoute() async {
|
|
let tv = FakeTV()
|
|
await tv.failWrites()
|
|
let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(.init(host: "tv.local")))
|
|
await controller.start(); await controller.send(.right)
|
|
#expect(await tv.commands == [.right])
|
|
#expect(controller.error?.contains("uncertain") == true)
|
|
await controller.setDemo(true); await controller.send(.left)
|
|
#expect(controller.isDemo && !controller.isReady)
|
|
#expect(await tv.commands == [.right])
|
|
#expect(await tv.disconnects == 1)
|
|
}
|
|
|
|
@Test func certificateRequiresExplicitApprovalAndDoesNotRetryPairing() async throws {
|
|
let auth = MemoryAuthorization()
|
|
let tv = FakeTV()
|
|
let controller = LivePhoneController(transport: tv, authorization: auth, selection: MemorySelection())
|
|
await tv.setPairHook {
|
|
await controller.receiveUntrustedCertificate(host: "other.local", sha256: Data(repeating: 1, count: 32))
|
|
await controller.receiveUntrustedCertificate(host: "tv.local", sha256: Data(repeating: 2, count: 32))
|
|
}
|
|
await controller.pair(address: "tv.local")
|
|
let candidate = try #require(controller.certificate)
|
|
#expect(candidate.sha256 == Data(repeating: 2, count: 32))
|
|
#expect(await auth.approvals == 0)
|
|
await controller.approveCertificate(candidate)
|
|
#expect(await auth.approvals == 1)
|
|
#expect(await tv.paired.count == 1)
|
|
#expect(!controller.isReady)
|
|
#expect(controller.certificate == nil)
|
|
await controller.approveCertificate(candidate)
|
|
#expect(await auth.approvals == 1)
|
|
}
|
|
|
|
@Test func unsolicitedCertificateIsNotOfferedOrApproved() async {
|
|
let auth = MemoryAuthorization()
|
|
let controller = LivePhoneController(transport: FakeTV(), authorization: auth, selection: MemorySelection(.init(host: "tv.local")))
|
|
await controller.start()
|
|
controller.receiveUntrustedCertificate(host: "tv.local", sha256: Data(repeating: 1, count: 32))
|
|
#expect(controller.certificate == nil)
|
|
#expect(await auth.approvals == 0)
|
|
}
|
|
}
|