Files
wristivo/Adapters/WatchBridge/Tests/iPhone/PhoneRelayBridgeTests.swift
T

157 lines
6.9 KiB
Swift

import Foundation
import Testing
import RemoteCore
import WatchBridgeCommon
import WatchBridgePhone
@MainActor
private final class FakePhoneSession: RelaySessionSending {
var activationState: RelayActivationState = .notActivated
var isReachable = true
var sent: [[String: Any]] = []
var contexts: [[String: Any]] = []
var reply: (([String: Any]) -> Void)?
var error: ((Error) -> Void)?
func activate() { activationState = .activated }
func sendMessage(_ message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void,
errorHandler: @escaping (Error) -> Void) {
sent.append(message)
reply = replyHandler
error = errorHandler
}
func updateApplicationContext(_ context: [String: Any]) throws { contexts.append(context) }
func answer(_ value: [String: Any]) { reply?(value) }
}
private actor RecordingPhoneCoordinator: PhoneCommandCoordinating {
var activated: [WatchPeerGeneration] = []
var deactivated: [WatchPeerGeneration] = []
var commands: [CommandEnvelope] = []
let sessionID = UUID()
let televisionID = UUID()
func watchBridgeDidActivate(peer: WatchPeerGeneration) { activated.append(peer) }
func watchBridgeDidDeactivate(peer: WatchPeerGeneration) { deactivated.append(peer) }
func beginWatchSession(_ request: BeginRemoteSessionRequest, peer: WatchPeerGeneration)
-> Result<RemoteSessionGrant, RelayFailure> {
.success(RemoteSessionGrant(sessionID: sessionID, televisionID: televisionID,
issuedAt: Date(timeIntervalSince1970: 1_700_000_000)))
}
func endWatchSession(_ request: EndRemoteSessionRequest, peer: WatchPeerGeneration)
-> Result<RemoteSessionEnded, RelayFailure> {
.success(RemoteSessionEnded(sessionID: request.sessionID))
}
func submitWatchCommand(_ envelope: CommandEnvelope, peer: WatchPeerGeneration) -> CommandReceipt {
commands.append(envelope)
return CommandReceipt(id: envelope.id, status: .sentToTransport)
}
}
@MainActor
@Test func phoneWaitsForActorActivationAndForwardsCommandWithoutAdmission() async throws {
let session = FakePhoneSession()
let actor = RecordingPhoneCoordinator()
let bridge = PhoneRelayBridge(session: session, dependency: PhoneRelayDependency(coordinator: actor))
bridge.activate()
bridge.activationDidComplete(error: nil)
await Task.yield()
#expect({
if case .ready = bridge.state { return true }
return false
}())
let begin = try RelayContainer(kind: .beginSession, value: BeginRemoteSessionRequest()).dictionary()
bridge.receive(message: begin) { _ in }
await Task.yield()
#expect(session.sent.isEmpty)
let peerActivated = await actor.activated
#expect(peerActivated.count == 1)
bridge.receive(message: begin) { _ in }
await Task.yield()
#expect(session.sent.isEmpty)
}
@MainActor
@Test func phoneRejectsMalformedAndWrongDirectionRequests() async throws {
let session = FakePhoneSession()
let actor = RecordingPhoneCoordinator()
let bridge = PhoneRelayBridge(session: session, dependency: PhoneRelayDependency(coordinator: actor))
bridge.activate(); bridge.activationDidComplete(error: nil)
await Task.yield()
var replies = 0
bridge.receive(message: ["schemaVersion": true, "kind": "command", "payload": Data()]) { _ in
replies += 1
}
await Task.yield()
#expect(replies == 1)
}
private actor SuspendedCoordinator: PhoneCommandCoordinating {
var begins = 0
var active = false
var waiting: [CheckedContinuation<Result<RemoteSessionGrant, RelayFailure>, Never>] = []
func watchBridgeDidActivate(peer: WatchPeerGeneration) { active = true }
func watchBridgeDidDeactivate(peer: WatchPeerGeneration) { active = false }
func beginWatchSession(_ request: BeginRemoteSessionRequest, peer: WatchPeerGeneration) async -> Result<RemoteSessionGrant, RelayFailure> {
begins += 1
return await withCheckedContinuation { waiting.append($0) }
}
func endWatchSession(_ request: EndRemoteSessionRequest, peer: WatchPeerGeneration) -> Result<RemoteSessionEnded, RelayFailure> { .failure(RelayFailure(code: .unavailable)) }
func submitWatchCommand(_ envelope: CommandEnvelope, peer: WatchPeerGeneration) -> CommandReceipt { CommandReceipt(id: envelope.id, status: .sentToTransport) }
func release() { for continuation in waiting { continuation.resume(returning: .failure(RelayFailure(code: .unavailable))) }; waiting.removeAll() }
}
@MainActor
@Test func timeoutDoesNotWaitForUncooperativeActorAndBoundsOutstandingCalls() async throws {
let session = FakePhoneSession()
let coordinator = SuspendedCoordinator()
let bridge = PhoneRelayBridge(session: session, dependency: PhoneRelayDependency(coordinator: coordinator))
bridge.activate(); bridge.activationDidComplete(error: nil)
try await Task.sleep(for: .milliseconds(30))
let message = try RelayContainer(kind: .beginSession, value: BeginRemoteSessionRequest()).dictionary()
var replies = 0
for _ in 0..<20 { bridge.receive(message: message) { _ in replies += 1 } }
try await Task.sleep(for: .milliseconds(1_150))
#expect(replies == 20)
#expect(await coordinator.begins == 16)
bridge.receive(message: message) { _ in replies += 1 }
#expect(replies == 21)
#expect(await coordinator.begins == 16)
await coordinator.release()
try await Task.sleep(for: .milliseconds(30))
#expect(replies == 21)
}
@MainActor
@Test func deactivationCompletesPendingRepliesBeforeActorReturns() async throws {
let session = FakePhoneSession()
let coordinator = SuspendedCoordinator()
let bridge = PhoneRelayBridge(session: session, dependency: PhoneRelayDependency(coordinator: coordinator))
bridge.activate(); bridge.activationDidComplete(error: nil)
try await Task.sleep(for: .milliseconds(30))
var replies = 0
bridge.receive(message: try RelayContainer(kind: .beginSession, value: BeginRemoteSessionRequest()).dictionary()) { _ in replies += 1 }
try await Task.sleep(for: .milliseconds(30))
bridge.didDeactivate()
#expect(replies == 1)
await coordinator.release()
try await Task.sleep(for: .milliseconds(30))
#expect(replies == 1)
}
@MainActor
@Test func latestContextPublishedBeforeActivationIsFlushed() async throws {
let session = FakePhoneSession()
let coordinator = RecordingPhoneCoordinator()
let bridge = PhoneRelayBridge(session: session, dependency: PhoneRelayDependency(coordinator: coordinator))
let context = RelayApplicationContext(selectedTelevisionID: UUID(), displayName: "TV", isConnectionReady: true)
bridge.publishContext(context)
#expect(session.contexts.isEmpty)
bridge.activate(); bridge.activationDidComplete(error: nil)
try await Task.sleep(for: .milliseconds(30))
#expect(session.contexts.count == 1)
#expect(try RelayApplicationContext(dictionary: session.contexts[0]) == context)
}