import Foundation import Testing @testable import RemoteCore private let now = Date(timeIntervalSince1970: 1_000) private func envelope(session: UUID, tv: UUID, sequence: UInt64 = 1, id: UUID = UUID(), issued: Date = now, expires: Date = now.addingTimeInterval(0.5), version: Int = 1) -> CommandEnvelope { CommandEnvelope(sessionID: session, televisionID: tv, sequence: sequence, issuedAt: issued, expiresAt: expires, command: .right, id: id, schemaVersion: version) } @Test func admitsExactlyOnceAndRequiresIncreasingSequence() { let session = UUID(), tv = UUID() var gate = CommandAdmission(sessionID: session, televisionID: tv) let first = envelope(session: session, tv: tv) #expect(gate.admit(first, now: now) == .accepted) #expect(gate.admit(first, now: now) == .rejected(.duplicate)) #expect(gate.admit(envelope(session: session, tv: tv), now: now) == .rejected(.outOfOrder)) #expect(gate.admit(envelope(session: session, tv: tv, sequence: 2), now: now) == .accepted) } @Test func rejectsWrongRoutingAndUnsupportedSchemaWithoutConsumingSequence() { let session = UUID(), tv = UUID() var gate = CommandAdmission(sessionID: session, televisionID: tv) #expect(gate.admit(envelope(session: UUID(), tv: tv), now: now) == .rejected(.wrongSession)) #expect(gate.admit(envelope(session: session, tv: UUID()), now: now) == .rejected(.wrongTelevision)) #expect(gate.admit(envelope(session: session, tv: tv, version: 2), now: now) == .rejected(.unsupportedSchema)) #expect(gate.admit(envelope(session: session, tv: tv), now: now) == .accepted) } @Test func rejectsStaleExpiredFutureAndInvalidLifetime() { let session = UUID(), tv = UUID() var gate = CommandAdmission(sessionID: session, televisionID: tv) #expect(gate.admit(envelope(session: session, tv: tv, issued: now.addingTimeInterval(-2), expires: now.addingTimeInterval(-1.5)), now: now) == .rejected(.stale)) #expect(gate.admit(envelope(session: session, tv: tv, issued: now.addingTimeInterval(-0.5), expires: now), now: now) == .rejected(.expired)) #expect(gate.admit(envelope(session: session, tv: tv, issued: now.addingTimeInterval(0.3), expires: now.addingTimeInterval(0.8)), now: now) == .rejected(.futureTimestamp)) #expect(gate.admit(envelope(session: session, tv: tv, expires: now.addingTimeInterval(2)), now: now) == .rejected(.invalidLifetime)) #expect(gate.admit(envelope(session: session, tv: tv, expires: now), now: now) == .rejected(.invalidLifetime)) #expect(gate.admit(envelope(session: session, tv: tv), now: Date(timeIntervalSince1970: .infinity)) == .rejected(.invalidTimestamp)) } @Test func acceptsSkewBoundaryAndMaximumLifetime() { let session = UUID(), tv = UUID() var gate = CommandAdmission(sessionID: session, televisionID: tv) let issued = now.addingTimeInterval(0.25) #expect(gate.admit(envelope(session: session, tv: tv, issued: issued, expires: issued.addingTimeInterval(1)), now: now) == .accepted) } @Test func resetUsesFreshSessionAndOldReplayCannotPassAfterIDEviction() { let session = UUID(), tv = UUID() var gate = CommandAdmission(sessionID: session, televisionID: tv) let first = envelope(session: session, tv: tv, sequence: 0) #expect(gate.admit(first, now: now) == .accepted) for value in 1...CommandAdmission.rememberedIDLimit { #expect(gate.admit(envelope(session: session, tv: tv, sequence: UInt64(value)), now: now) == .accepted) } #expect(gate.admit(first, now: now) == .rejected(.outOfOrder)) let newSession = gate.startNewSession(televisionID: tv) #expect(newSession != session) #expect(gate.admit(first, now: now) == .rejected(.wrongSession)) #expect(gate.admit(envelope(session: newSession, tv: tv, sequence: 0), now: now) == .accepted) } @Test func decodingDoesNotBypassAdmission() throws { let session = UUID(), tv = UUID() let invalid = envelope(session: session, tv: tv, version: 99) let decoded = try JSONDecoder().decode(CommandEnvelope.self, from: JSONEncoder().encode(invalid)) #expect(decoded == invalid) var gate = CommandAdmission(sessionID: session, televisionID: tv) #expect(gate.admit(decoded, now: now) == .rejected(.unsupportedSchema)) }