261 lines
13 KiB
Go
261 lines
13 KiB
Go
package protocol_test
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
|
)
|
|
|
|
func TestRC6GeneratedContractsHaveFrozenExactShapes(t *testing.T) {
|
|
tests := []struct {
|
|
value any
|
|
fields []string
|
|
}{
|
|
{protocol.VideoProfile{}, []string{"Codec", "BitDepth", "ChromaSubsampling", "ColorSpace", "TransferFunction"}},
|
|
{protocol.AudioProfile{}, []string{"Codec", "SampleRateHz", "Channels", "ChannelLayout", "PacketDurationMs"}},
|
|
{protocol.BitratePreference{}, []string{"Mode", "TargetKbps"}},
|
|
{protocol.SessionQualityLimits{}, []string{"PolicyVersionID", "PolicyDisplayLimit", "SelectableDisplayMaximum", "DisplayLimitOverride", "BitrateMinimumKbps", "BitrateTargetKbps", "BitrateMaximumKbps"}},
|
|
{protocol.SessionAdjustment{}, []string{"DisplayReason", "BitrateReason"}},
|
|
{protocol.SelectedSessionDescriptor{}, []string{"VideoProfile", "AudioProfile", "DisplayMode", "BitrateTargetKbps", "BitrateMaximumKbps", "Adjustment", "MediaTimestampBasis"}},
|
|
}
|
|
for _, test := range tests {
|
|
typeOf := reflect.TypeOf(test.value)
|
|
if typeOf.NumField() != len(test.fields) {
|
|
t.Fatalf("%s field count = %d, want %d", typeOf.Name(), typeOf.NumField(), len(test.fields))
|
|
}
|
|
for index, field := range test.fields {
|
|
if typeOf.Field(index).Name != field {
|
|
t.Fatalf("%s field %d = %s, want %s", typeOf.Name(), index, typeOf.Field(index).Name, field)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRC6ProfileValidationAndOrderedIntersection(t *testing.T) {
|
|
h264 := protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}
|
|
hevc := h264
|
|
hevc.Codec = "hevc"
|
|
audio := protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5}
|
|
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded", SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{h264, hevc}, AudioProfiles: []protocol.AudioProfile{audio}}
|
|
second := first
|
|
second.VideoProfiles = []protocol.VideoProfile{hevc, h264}
|
|
selected, err := protocol.IntersectCapabilityProfiles(first, second)
|
|
if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{h264, hevc}) {
|
|
t.Fatalf("ordered intersection = %+v, %v", selected.VideoProfiles, err)
|
|
}
|
|
duplicate := first
|
|
duplicate.VideoProfiles = []protocol.VideoProfile{h264, h264}
|
|
if duplicate.Validate() == nil {
|
|
t.Fatal("CapabilityProfile accepted duplicate video profiles")
|
|
}
|
|
invalid := h264
|
|
invalid.BitDepth = 10
|
|
if invalid.Validate() == nil {
|
|
t.Fatal("VideoProfile accepted a non-RC6 bit depth")
|
|
}
|
|
}
|
|
|
|
func TestRC6StrictDecodeRejectsLegacyAndMalformedBodies(t *testing.T) {
|
|
for name, test := range map[string]struct {
|
|
payload []byte
|
|
decode func([]byte) error
|
|
}{
|
|
"opaque capability": {[]byte(`{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}`), func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err }},
|
|
"old session request": {[]byte(`{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}`), func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err }},
|
|
"old provider policy": {[]byte(`{"resolution_width":1920,"resolution_height":1080,"fps":60,"codec":"H264","bitrate_kbps":20000,"audio_enabled":true}`), func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err }},
|
|
"unknown field": {[]byte(`{"mode":"explicit","target_kbps":1000,"unknown":true}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
|
"duplicate key": {[]byte(`{"mode":"explicit","mode":"auto","target_kbps":1000}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
|
"trailing JSON": {[]byte(`{"mode":"explicit","target_kbps":1000} {}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
|
} {
|
|
if err := test.decode(test.payload); err == nil {
|
|
t.Fatalf("RC6 accepted %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRC6BitratePreferenceTaggedBounds(t *testing.T) {
|
|
for _, valid := range []protocol.BitratePreference{{Mode: "auto"}, {Mode: "explicit", TargetKbps: int64Pointer(100)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_000)}} {
|
|
if err := valid.Validate(); err != nil {
|
|
t.Fatalf("valid preference %+v rejected: %v", valid, err)
|
|
}
|
|
}
|
|
for _, invalid := range []protocol.BitratePreference{{Mode: "auto", TargetKbps: int64Pointer(100)}, {Mode: "explicit"}, {Mode: "explicit", TargetKbps: int64Pointer(99)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_001)}} {
|
|
if invalid.Validate() == nil {
|
|
t.Fatalf("invalid preference %+v accepted", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRC6GatewayQualityDiscoveryAndLeaseFencing(t *testing.T) {
|
|
operationID := "12345678-1234-1234-1234-123456789abc"
|
|
revision := int64(7)
|
|
leaseGeneration := int64(3)
|
|
currentRevision := int64(6)
|
|
|
|
poll := protocol.GatewayQualityWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
|
|
if err := poll.Validate(); err != nil {
|
|
t.Fatalf("session-bound lost-prompt poll rejected: %v", err)
|
|
}
|
|
prompt := poll
|
|
prompt.Acquisition = "prompt"
|
|
prompt.OperationID = operationID
|
|
prompt.Revision = &revision
|
|
if err := prompt.Validate(); err != nil {
|
|
t.Fatalf("prompt-bound acquisition rejected: %v", err)
|
|
}
|
|
observation := prompt
|
|
observation.Acquisition = "observation"
|
|
observation.LeaseGeneration = &leaseGeneration
|
|
observation.CurrentAppliedRevision = ¤tRevision
|
|
if err := observation.Validate(); err != nil {
|
|
t.Fatalf("lease-bound applied-revision observation rejected: %v", err)
|
|
}
|
|
|
|
for name, invalid := range map[string]protocol.GatewayQualityWorkRequest{
|
|
"poll with unknown coordinates": prompt,
|
|
"prompt missing revision": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "prompt", OperationID: operationID},
|
|
"observation missing lease": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "observation", OperationID: operationID, Revision: &revision, CurrentAppliedRevision: ¤tRevision},
|
|
} {
|
|
if name == "poll with unknown coordinates" {
|
|
invalid.Acquisition = "poll"
|
|
}
|
|
if invalid.Validate() == nil {
|
|
t.Fatalf("accepted invalid %s: %+v", name, invalid)
|
|
}
|
|
}
|
|
|
|
stopPoll := protocol.GatewayStopWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
|
|
if err := stopPoll.Validate(); err != nil {
|
|
t.Fatalf("session-bound Stop poll rejected: %v", err)
|
|
}
|
|
stopPrompt := stopPoll
|
|
stopPrompt.Acquisition = "prompt"
|
|
stopPrompt.OperationID = operationID
|
|
if err := stopPrompt.Validate(); err != nil {
|
|
t.Fatalf("prompt-bound Stop acquisition rejected: %v", err)
|
|
}
|
|
stopPoll.OperationID = operationID
|
|
if stopPoll.Validate() == nil {
|
|
t.Fatal("Stop poll accepted unknown operation coordinates")
|
|
}
|
|
}
|
|
|
|
func TestRC6GatewayQualityAckHasUnambiguousObservation(t *testing.T) {
|
|
revision := int64(7)
|
|
prior := int64(6)
|
|
base := protocol.GatewayQualityAck{
|
|
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2,
|
|
OperationID: "12345678-1234-1234-1234-123456789abc", Revision: revision, LeaseGeneration: 3,
|
|
}
|
|
for _, valid := range []protocol.GatewayQualityAck{
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "applied"
|
|
value.CurrentAppliedRevision = &revision
|
|
return value
|
|
}(),
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "proven_prior"
|
|
value.CurrentAppliedRevision = &prior
|
|
return value
|
|
}(),
|
|
func() protocol.GatewayQualityAck { value := base; value.Outcome = "unknown"; return value }(),
|
|
} {
|
|
if err := valid.Validate(); err != nil {
|
|
t.Fatalf("valid quality acknowledgement rejected: %+v: %v", valid, err)
|
|
}
|
|
}
|
|
for _, invalid := range []protocol.GatewayQualityAck{
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "applied"
|
|
value.CurrentAppliedRevision = &prior
|
|
return value
|
|
}(),
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "proven_prior"
|
|
value.CurrentAppliedRevision = &revision
|
|
return value
|
|
}(),
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "unknown"
|
|
value.CurrentAppliedRevision = &prior
|
|
return value
|
|
}(),
|
|
func() protocol.GatewayQualityAck {
|
|
value := base
|
|
value.Outcome = "unknown"
|
|
value.LeaseGeneration = 0
|
|
return value
|
|
}(),
|
|
} {
|
|
if invalid.Validate() == nil {
|
|
t.Fatalf("contradictory or unfenced quality acknowledgement accepted: %+v", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRC6JSONFixturesAndCanonicalOperationFields(t *testing.T) {
|
|
validFixtures := map[string]func([]byte) error{
|
|
"../../fixtures/valid/session-request.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
|
|
"../../fixtures/valid/selected-session-descriptor.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
|
|
"../../fixtures/valid/session-quality-limits.json": func(raw []byte) error { _, err := protocol.DecodeSessionQualityLimits(raw); return err },
|
|
"../../fixtures/valid/gateway-quality-poll.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
|
|
"../../fixtures/valid/gateway-quality-ack-applied.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
|
}
|
|
for path, decode := range validFixtures {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read valid fixture %s: %v", path, err)
|
|
}
|
|
if err := decode(raw); err != nil {
|
|
t.Fatalf("valid fixture %s rejected: %v", path, err)
|
|
}
|
|
}
|
|
invalidFixtures := map[string]func([]byte) error{
|
|
"../../fixtures/invalid/capability-rc5-opaque.json": func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err },
|
|
"../../fixtures/invalid/session-request-rc5.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
|
|
"../../fixtures/invalid/provider-stream-policy-rc5.json": func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err },
|
|
"../../fixtures/invalid/video-profile-unknown.json": func(raw []byte) error { _, err := protocol.DecodeVideoProfile(raw); return err },
|
|
"../../fixtures/invalid/bitrate-preference-auto-target.json": func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err },
|
|
"../../fixtures/invalid/selected-session-descriptor-provider-field.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
|
|
"../../fixtures/invalid/gateway-quality-poll-coordinates.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
|
|
"../../fixtures/invalid/gateway-quality-ack-contradictory.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
|
"../../fixtures/invalid/gateway-quality-ack-uppercase-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
|
"../../fixtures/invalid/gateway-quality-ack-zero-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
|
"../../fixtures/invalid/quality-operation-offset-time.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
|
|
"../../fixtures/invalid/quality-operation-noncanonical-fraction.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
|
|
}
|
|
for path, decode := range invalidFixtures {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decode(raw) == nil {
|
|
t.Fatalf("invalid fixture %s accepted", path)
|
|
}
|
|
}
|
|
validOperation := `{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":1,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":1,"created_at":"2099-01-01T00:00:00Z","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}`
|
|
if _, err := protocol.DecodeQualityChangeOperation([]byte(validOperation)); err != nil {
|
|
t.Fatalf("valid quality operation rejected: %v", err)
|
|
}
|
|
for _, invalid := range []string{
|
|
strings.Replace(validOperation, "123456789abc", "123456789ABC", 1),
|
|
strings.Replace(validOperation, "12345678-1234-1234-1234-123456789abc", "00000000-0000-0000-0000-000000000000", 1),
|
|
strings.Replace(validOperation, "2099-01-01T00:00:00Z", "2099-01-01T00:00:00+00:00", 1),
|
|
strings.Replace(validOperation, `,"revision":1`, "", 1),
|
|
} {
|
|
if _, err := protocol.DecodeQualityChangeOperation([]byte(invalid)); err == nil {
|
|
t.Fatalf("invalid operation accepted: %s", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func int64Pointer(value int64) *int64 { return &value }
|