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