Files
wristivo/Adapters/PhoneControl/Tests/PhoneControlTests/PhoneControlTests.swift
T

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