Establish watch relay contracts and platform package wiring
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import RemoteCore
|
||||
|
||||
@Test func wireDatesUseUnixMillisecondsForGrantAndCommand() throws {
|
||||
let session = UUID(), tv = UUID()
|
||||
let issued = Date(timeIntervalSince1970: 1_700_000_000.125)
|
||||
let grant = RemoteSessionGrant(sessionID: session, televisionID: tv, issuedAt: issued)
|
||||
let data = try RelayJSON.encode(grant)
|
||||
let object = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
#expect(object["issuedAt"] as? Double == 1_700_000_000_125)
|
||||
#expect(try RelayJSON.decode(RemoteSessionGrant.self, from: data) == grant)
|
||||
let command = CommandEnvelope(sessionID: session, televisionID: tv, sequence: 1,
|
||||
issuedAt: issued, expiresAt: issued.addingTimeInterval(0.5), command: .right)
|
||||
let commandData = try RelayJSON.encode(command)
|
||||
let commandObject = try #require(JSONSerialization.jsonObject(with: commandData) as? [String: Any])
|
||||
#expect(commandObject["expiresAt"] as? Double == 1_700_000_000_625)
|
||||
#expect(try RelayJSON.decode(CommandEnvelope.self, from: commandData) == command)
|
||||
}
|
||||
|
||||
@Test func controlPayloadsRoundTripAndFailureIsAnError() throws {
|
||||
let session = UUID()
|
||||
let begin = BeginRemoteSessionRequest()
|
||||
let end = EndRemoteSessionRequest(sessionID: session)
|
||||
let ended = RemoteSessionEnded(sessionID: session)
|
||||
let failure = RelayFailure(code: .sessionInvalidated)
|
||||
#expect(try RelayJSON.decode(BeginRemoteSessionRequest.self, from: RelayJSON.encode(begin)) == begin)
|
||||
#expect(try RelayJSON.decode(EndRemoteSessionRequest.self, from: RelayJSON.encode(end)) == end)
|
||||
#expect(try RelayJSON.decode(RemoteSessionEnded.self, from: RelayJSON.encode(ended)) == ended)
|
||||
#expect(try RelayJSON.decode(RelayFailure.self, from: RelayJSON.encode(failure)) == failure)
|
||||
let result: Result<RemoteSessionGrant, RelayFailure> = .failure(failure)
|
||||
#expect(throws: failure) { try result.get() }
|
||||
}
|
||||
|
||||
@Test func malformedOrUnsupportedControlPayloadsFailClosed() throws {
|
||||
#expect(throws: RelayContractError.unsupportedSchema(2)) {
|
||||
try RelayJSON.decode(BeginRemoteSessionRequest.self, from: Data(#"{"schemaVersion":2}"#.utf8))
|
||||
}
|
||||
#expect(throws: (any Error).self) {
|
||||
try RelayJSON.decode(EndRemoteSessionRequest.self, from: Data(#"{"schemaVersion":1,"sessionID":"bad"}"#.utf8))
|
||||
}
|
||||
#expect(throws: (any Error).self) {
|
||||
try RelayJSON.decode(BeginRemoteSessionRequest.self, from: Data(#"{"schemaVersion":true}"#.utf8))
|
||||
}
|
||||
#expect(throws: RelayContractError.unsupportedSchema(9)) {
|
||||
try RelayJSON.encode(RemoteSessionEnded(sessionID: UUID(), schemaVersion: 9))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func invalidGrantLifetimeRejectedOnEncodeAndDecode() throws {
|
||||
for lifetime: UInt64 in [0, 1_001, .max] {
|
||||
let invalid = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(),
|
||||
issuedAt: Date(timeIntervalSince1970: 1_000), maximumCommandLifetimeMilliseconds: lifetime)
|
||||
#expect(throws: RelayContractError.invalidLifetime) { try RelayJSON.encode(invalid) }
|
||||
// Bypass the validating encoder to simulate an untrusted peer.
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .millisecondsSince1970
|
||||
let raw = try encoder.encode(invalid)
|
||||
#expect(throws: RelayContractError.invalidLifetime) {
|
||||
try RelayJSON.decode(RemoteSessionGrant.self, from: raw)
|
||||
}
|
||||
}
|
||||
let invalidDate = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(),
|
||||
issuedAt: Date(timeIntervalSince1970: .infinity))
|
||||
#expect(throws: RelayContractError.invalidTimestamp) { try RelayJSON.encode(invalidDate) }
|
||||
}
|
||||
|
||||
@Test func byteLimitIsCheckedBeforeDecodingAndAtEncoding() throws {
|
||||
// JSON string quotes use 2 bytes. Exactly 4096 bytes is permitted.
|
||||
let boundary = String(repeating: "a", count: RelayWire.maximumPayloadBytes - 2)
|
||||
let encoded = try RelayJSON.encode(boundary)
|
||||
#expect(encoded.count == RelayWire.maximumPayloadBytes)
|
||||
#expect(try RelayJSON.decode(String.self, from: encoded) == boundary)
|
||||
#expect(throws: RelayContractError.payloadTooLarge) { try RelayJSON.encode(boundary + "a") }
|
||||
#expect(throws: RelayContractError.payloadTooLarge) {
|
||||
try RelayJSON.decode(BeginRemoteSessionRequest.self, from: Data(repeating: 0, count: 4_097))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func commandDecodeDoesNotConsumeOrReplaceAdmission() throws {
|
||||
let session = UUID(), tv = UUID(), now = Date(timeIntervalSince1970: 1_000)
|
||||
let command = CommandEnvelope(sessionID: session, televisionID: tv, sequence: 1,
|
||||
issuedAt: now, expiresAt: now.addingTimeInterval(0.5), command: .left)
|
||||
let data = try RelayJSON.encode(command)
|
||||
var admission = CommandAdmission(sessionID: session, televisionID: tv)
|
||||
#expect(admission.admit(try RelayJSON.decode(CommandEnvelope.self, from: data), now: now) == .accepted)
|
||||
#expect(admission.admit(try RelayJSON.decode(CommandEnvelope.self, from: data), now: now) == .rejected(.duplicate))
|
||||
}
|
||||
Reference in New Issue
Block a user