151 lines
5.9 KiB
Swift
151 lines
5.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
import RemoteCore
|
|
import WatchBridgeCommon
|
|
import WatchBridgeWatch
|
|
|
|
@MainActor
|
|
private final class FakeWatchSession: RelaySessionSending {
|
|
var activationState: RelayActivationState = .notActivated
|
|
var isReachable = true
|
|
var sent: [[String: Any]] = []
|
|
var reply: (([String: Any]) -> Void)?
|
|
var error: ((Error) -> Void)?
|
|
var replyTwice = false
|
|
|
|
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 {}
|
|
|
|
func answer(_ value: [String: Any]) {
|
|
reply?(value)
|
|
if replyTwice { reply?(value); error?(RelayTestError.failed) }
|
|
}
|
|
}
|
|
|
|
private enum RelayTestError: Error { case failed }
|
|
|
|
@MainActor
|
|
@Test func watchNegotiatesBeforeSendingCommandAndUsesMilliseconds() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate()
|
|
bridge.activationDidComplete(error: nil)
|
|
bridge.beginSession()
|
|
#expect(fake.sent.count == 1)
|
|
let request = try RelayContainer(dictionary: fake.sent[0])
|
|
#expect(request.kind == .beginSession)
|
|
|
|
let sessionID = UUID(), tvID = UUID()
|
|
let grant = RemoteSessionGrant(sessionID: sessionID, televisionID: tvID,
|
|
issuedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
maximumCommandLifetimeMilliseconds: 500)
|
|
fake.answer(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
bridge.sendCommand(.right, now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
#expect(fake.sent.count == 2)
|
|
let command = try RelayContainer(dictionary: fake.sent[1])
|
|
let envelope = try command.decode(CommandEnvelope.self, expectedKind: .command)
|
|
#expect(envelope.sessionID == sessionID)
|
|
#expect(envelope.televisionID == tvID)
|
|
#expect(envelope.sequence == 0)
|
|
#expect(envelope.expiresAt.timeIntervalSince1970 == 1_700_000_000.5)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func watchDropsCommandsWhenUnreachableAndDuplicateRepliesCompleteOnce() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate()
|
|
bridge.activationDidComplete(error: nil)
|
|
fake.isReachable = false
|
|
bridge.beginSession()
|
|
#expect(fake.sent.isEmpty)
|
|
|
|
fake.isReachable = true
|
|
bridge.beginSession()
|
|
fake.replyTwice = true
|
|
let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date())
|
|
fake.answer(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
#expect(bridge.state == .armed(sessionID: grant.sessionID, televisionID: grant.televisionID))
|
|
}
|
|
|
|
@MainActor
|
|
@Test func watchDeactivationDisarmsAndClearsGrant() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate()
|
|
bridge.activationDidComplete(error: nil)
|
|
bridge.beginSession()
|
|
let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date())
|
|
fake.answer(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
bridge.didDeactivate()
|
|
#expect(bridge.state == .inactive)
|
|
#expect(fake.sent.count == 1)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func watchReachabilityLossRevokesArmedGrantAndNeverReplays() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate(); bridge.activationDidComplete(error: nil); bridge.beginSession()
|
|
let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date())
|
|
fake.answer(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
#expect(bridge.grant != nil)
|
|
fake.isReachable = false; bridge.reachabilityDidChange()
|
|
#expect(bridge.grant == nil)
|
|
#expect(bridge.state == .ready(reachable: false))
|
|
fake.isReachable = true; bridge.reachabilityDidChange()
|
|
bridge.sendCommand(.right)
|
|
#expect(fake.sent.count == 1)
|
|
}
|
|
|
|
@MainActor
|
|
@Test func lateGrantAfterStopDoesNotRearmAndRepeatedConnectDoesNotDuplicate() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate(); bridge.activationDidComplete(error: nil)
|
|
bridge.beginSession(); bridge.beginSession()
|
|
#expect(fake.sent.count == 1)
|
|
let lateReply = fake.reply
|
|
bridge.endSession()
|
|
let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date())
|
|
lateReply?(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
#expect(bridge.grant == nil)
|
|
#expect(bridge.state == .ready(reachable: true))
|
|
}
|
|
|
|
@MainActor
|
|
@Test func invalidationCompletesCommandOnceAndDiscardsLateSuccess() async throws {
|
|
let fake = FakeWatchSession()
|
|
let bridge = WatchRelayBridge(session: fake)
|
|
bridge.activate(); bridge.activationDidComplete(error: nil); bridge.beginSession()
|
|
let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date())
|
|
fake.answer(try RelayContainer(kind: .session, value: grant).dictionary())
|
|
await Task.yield()
|
|
var completions = 0
|
|
var succeeded = false
|
|
bridge.sendCommand(.right) { result in
|
|
completions += 1
|
|
if case .success = result { succeeded = true }
|
|
}
|
|
let command = try RelayContainer(dictionary: fake.sent.last!).decode(CommandEnvelope.self, expectedKind: .command)
|
|
let lateReply = fake.reply
|
|
bridge.didDeactivate()
|
|
lateReply?(try RelayContainer(kind: .receipt, value: CommandReceipt(id: command.id, status: .sentToTransport)).dictionary())
|
|
for _ in 0..<10 { await Task.yield() }
|
|
#expect(completions == 1)
|
|
#expect(!succeeded)
|
|
#expect(bridge.grant == nil)
|
|
}
|