Files
Piik/internal/server/protocol/contract_test.go
T
AAA da47189fc3 refactor: unify Go runtime and audited frontend
Squash integration of the reviewed Go core, frontend audit fixes, release tooling, and deployment hygiene. CI passed all Server/Client jobs.
2026-09-06 23:26:01 +08:00

660 lines
27 KiB
Go

package protocol
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
)
// member is one JSON object key with its raw encoded value, so the tests can
// reproduce the exact wire shapes from tests/protocol.test.ts (including key
// presence, which a Go map cannot express).
type member struct {
key string
raw string
}
func object(members []member) []byte {
var builder strings.Builder
builder.WriteByte('{')
for index, entry := range members {
if index > 0 {
builder.WriteByte(',')
}
fmt.Fprintf(&builder, "%q:%s", entry.key, entry.raw)
}
builder.WriteByte('}')
return []byte(builder.String())
}
func with(base []member, changes ...member) []member {
out := append([]member(nil), base...)
for _, change := range changes {
replaced := false
for index := range out {
if out[index].key == change.key {
out[index] = change
replaced = true
break
}
}
if !replaced {
out = append(out, change)
}
}
return out
}
func drop(base []member, keys ...string) []member {
out := make([]member, 0, len(base))
for _, entry := range base {
remove := false
for _, key := range keys {
if entry.key == key {
remove = true
}
}
if !remove {
out = append(out, entry)
}
}
return out
}
const (
testAssignmentNone = `{"upstream":{"kind":"none"},"childPeerIds":[],` +
`"sfuPublicationGeneration":null}`
testQualitySettings = `{"resolution":"1080p","maxFramerate":60,"maxBitrate":8000000,` +
`"degradationPreference":"maintain-resolution"}`
testRoutePolicy = `{"peerOnly":false,"topologyOptimization":true,"natPrediction":false}`
testIceConfig = `{"iceServers":[],"natPredictionStunUrls":[]}`
)
func authenticatedHost() []member {
return []member{
{"type", `"authenticated"`},
{"protocol", `"screener-v21"`},
{"peerId", `"host_12345678"`},
{"roomExpiresAt", `"2026-08-18T18:00:00.000Z"`},
{"maxViewers", `8`},
{"endpointMediaCopyCapacity", `2`},
{"hostOnline", `true`},
{"connectionId", `null`},
{"iceConfig", testIceConfig},
{"routePolicy", testRoutePolicy},
{"codeEntryPolicy", `"open"`},
{"viewerAuthorizationGeneration", `"viewer_generation_12345678"`},
{"mediaMode", `"peer-assisted"`},
{"shareGeneration", `"share_generation_12345678"`},
{"routeRevision", `0`},
{"routeAssignment", testAssignmentNone},
{"qualitySettings", testQualitySettings},
{"role", `"host"`},
{"viewerPasswordEnabled", `false`},
}
}
func assertServerMessage(t *testing.T, name string, data []byte, want bool) {
t.Helper()
t.Run(name, func(t *testing.T) {
_, err := DecodeServerMessage(data)
if want && err != nil {
t.Fatalf("expected %s to decode, got %v", data, err)
}
if !want && err == nil {
t.Fatalf("expected %s to be rejected", data)
}
})
}
func assertClientMessage(t *testing.T, name string, data []byte, want bool) {
t.Helper()
t.Run(name, func(t *testing.T) {
_, err := DecodeClientMessage(data)
if want && err != nil {
t.Fatalf("expected %s to decode, got %v", data, err)
}
if !want && err == nil {
t.Fatalf("expected %s to be rejected", data)
}
})
}
// "accepts dynamic viewer limits within the protocol boundary" and
// "rejects viewer limit %s outside the protocol boundary".
func TestAuthenticatedViewerLimits(t *testing.T) {
for _, sample := range []struct {
raw string
want bool
}{{`1`, true}, {`8`, true}, {`20`, true}, {`0`, false}, {`1.5`, false}, {`21`, false}} {
assertServerMessage(t, "maxViewers "+sample.raw,
object(with(authenticatedHost(), member{"maxViewers", sample.raw})), sample.want)
}
}
// "requires the authenticated endpoint capacity to be 1, 2, or 3".
func TestAuthenticatedEndpointCapacity(t *testing.T) {
for _, sample := range []struct {
raw string
want bool
}{{`1`, true}, {`2`, true}, {`3`, true}, {`0`, false}, {`4`, false}, {`1.5`, false}} {
assertServerMessage(t, "capacity "+sample.raw,
object(with(authenticatedHost(),
member{"endpointMediaCopyCapacity", sample.raw})), sample.want)
}
assertServerMessage(t, "capacity absent",
object(drop(authenticatedHost(), "endpointMediaCopyCapacity")), false)
}
// "scopes password configuration state to authenticated Hosts".
func TestAuthenticatedPasswordStateIsHostScoped(t *testing.T) {
assertServerMessage(t, "host carries the flag", object(authenticatedHost()), true)
assertServerMessage(t, "host without the flag",
object(drop(authenticatedHost(), "viewerPasswordEnabled")), false)
viewer := drop(with(authenticatedHost(),
member{"role", `"viewer"`}, member{"peerId", `"viewer_12345678"`}),
"viewerPasswordEnabled")
assertServerMessage(t, "viewer without the flag", object(viewer), true)
assertServerMessage(t, "viewer with the flag",
object(with(viewer, member{"viewerPasswordEnabled", `true`})), false)
assertServerMessage(t, "viewer with hostPaused",
object(with(viewer, member{"hostPaused", `true`})), true)
}
// "represents persistent rooms without a room expiry" and the revision bound.
func TestAuthenticatedRoomLifetimeAndRevision(t *testing.T) {
assertServerMessage(t, "no expiry",
object(with(authenticatedHost(), member{"roomExpiresAt", `null`})), true)
assertServerMessage(t, "revision at the ceiling",
object(with(authenticatedHost(), member{"routeRevision", `9007199254740991`})), true)
assertServerMessage(t, "revision past the ceiling",
object(with(authenticatedHost(), member{"routeRevision", `9007199254740992`})), false)
assertServerMessage(t, "absent route assignment",
object(drop(authenticatedHost(), "routeAssignment")), false)
assertServerMessage(t, "absent media mode",
object(drop(authenticatedHost(), "mediaMode")), false)
assertServerMessage(t, "sfu standby url",
object(with(authenticatedHost(),
member{"sfuStandbyUrl", `"wss://sfu.example.test"`})), true)
assertServerMessage(t, "sfu standby url over https",
object(with(authenticatedHost(),
member{"sfuStandbyUrl", `"https://sfu.example.test"`})), false)
assertServerMessage(t, "quality settings past the bitrate ceiling",
object(with(authenticatedHost(), member{"qualitySettings",
`{"resolution":"1080p","maxFramerate":60,"maxBitrate":20000000,` +
`"degradationPreference":"maintain-resolution"}`})), false)
}
// "accepts only STUN URLs in the authenticated peer ICE config" and
// "requires the v20 NAT observation configuration".
func TestAuthenticatedIceConfig(t *testing.T) {
for _, sample := range []struct {
name string
raw string
want bool
}{
{"empty", `{"iceServers":[],"natPredictionStunUrls":[]}`, true},
{"one stun url", `{"iceServers":[{"urls":"stun:stun.example.test:3478"}],` +
`"natPredictionStunUrls":[]}`, true},
{"a list of stun urls", `{"iceServers":[{"urls":["stun:a.example.test:3478",` +
`"stun:b.example.test:3478"]}],"natPredictionStunUrls":[]}`, true},
{"missing nat prediction urls", `{"iceServers":[]}`, false},
{"turn url", `{"iceServers":[{"urls":"turn:relay.example.test:3478?transport=udp"}],` +
`"natPredictionStunUrls":[]}`, false},
{"turn credentials", `{"iceServers":[{"urls":"turn:relay.example.test:3478",` +
`"username":"1787076000","credential":"credential"}],` +
`"natPredictionStunUrls":[]}`, false},
{"stuns url", `{"iceServers":[{"urls":"stuns:stun.example.test:5349"}],` +
`"natPredictionStunUrls":[]}`, false},
{"too many nat prediction urls", `{"iceServers":[],"natPredictionStunUrls":` +
`["stun:a.example.test:3478","stun:b.example.test:3478",` +
`"stun:c.example.test:3478"]}`, false},
} {
assertServerMessage(t, "iceConfig "+sample.name,
object(with(authenticatedHost(), member{"iceConfig", sample.raw})), sample.want)
}
}
// "keeps Viewer route status as a strict crossed union".
func TestRouteStatusCrossedUnion(t *testing.T) {
for _, sample := range []struct {
name string
raw string
want bool
}{
{"waiting on admission",
`{"type":"route-status","revision":3,"state":"waiting","reason":"sfu-admission"}`, true},
{"failed on exhaustion",
`{"type":"route-status","revision":4,"state":"failed","reason":"route-exhausted"}`, true},
{"waiting on exhaustion",
`{"type":"route-status","revision":3,"state":"waiting","reason":"route-exhausted"}`, false},
{"failed on admission",
`{"type":"route-status","revision":3,"state":"failed","reason":"sfu-admission"}`, false},
{"with a retry hint",
`{"type":"route-status","revision":3,"state":"waiting","reason":"sfu-admission",` +
`"retryAfterMs":1000}`, false},
{"with a peer id",
`{"type":"route-status","revision":3,"state":"waiting","reason":"sfu-admission",` +
`"peerId":"private-peer-id"}`, false},
} {
assertServerMessage(t, sample.name, []byte(sample.raw), sample.want)
}
}
// "accepts a strict, unique and bounded Viewer presence snapshot".
func TestViewerPresence(t *testing.T) {
const viewer = `{"role":"viewer","peerId":"viewer_12345678","displayName":"小明",` +
`"upstream":{"kind":"peer","peerId":"viewer_parent_12345678"}}`
for _, sample := range []struct {
name string
raw string
want bool
}{
{"sfu upstream with mediaReady",
`{"type":"viewer-presence","viewers":[{"role":"viewer","peerId":"viewer_12345678",` +
`"displayName":"小明","upstream":{"kind":"sfu"},"mediaReady":true}]}`, true},
{"mediaReady false", `{"type":"viewer-presence","viewers":[{"role":"viewer",` +
`"peerId":"viewer_12345678","displayName":"小明",` +
`"upstream":{"kind":"peer","peerId":"viewer_parent_12345678"},` +
`"mediaReady":false}]}`, false},
{"duplicate peer ids",
`{"type":"viewer-presence","viewers":[` + viewer + `,` + viewer + `]}`, false},
{"leaked topology", `{"type":"viewer-presence","viewers":[{"role":"viewer",` +
`"peerId":"viewer_12345678","displayName":"小明",` +
`"upstream":{"kind":"none"},"mediaTopology":"peer-relay"}]}`, false},
{"leaked address", `{"type":"viewer-presence","viewers":[{"role":"viewer",` +
`"peerId":"viewer_12345678","displayName":"小明",` +
`"upstream":{"kind":"none"},"ip":"203.0.113.1"}]}`, false},
{"host entry", `{"type":"viewer-presence","viewers":[` + viewer + `,` +
`{"role":"host","peerId":"host_12345678","displayName":"分享者",` +
`"upstream":{"kind":"none"}}]}`, true},
{"host entry with an upstream", `{"type":"viewer-presence","viewers":[` +
`{"role":"host","peerId":"host_12345678","displayName":"分享者",` +
`"upstream":{"kind":"sfu"}}]}`, false},
{"host entry with mediaReady", `{"type":"viewer-presence","viewers":[` +
`{"role":"host","peerId":"host_12345678","displayName":"分享者",` +
`"upstream":{"kind":"none"},"mediaReady":true}]}`, false},
} {
assertServerMessage(t, sample.name, []byte(sample.raw), sample.want)
}
}
func TestViewerPresenceConstructors(t *testing.T) {
host := HostPresenceEntry("host_12345678", "分享者")
if host.Upstream != NoUpstream() || host.MediaReady {
t.Fatalf("host presence entry must carry {kind:\"none\"} and no mediaReady: %+v", host)
}
encoded, err := marshalJSON(NewViewerPresenceEntry(
"viewer_12345678", "小明", SfuUpstream(), false))
if err != nil {
t.Fatal(err)
}
if bytes.Contains(encoded, []byte("mediaReady")) {
t.Fatalf("mediaReady must be omitted when false: %s", encoded)
}
encoded, err = marshalJSON(NewViewerPresenceEntry(
"viewer_12345678", "小明", SfuUpstream(), true))
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(encoded, []byte(`"mediaReady":true`)) {
t.Fatalf("mediaReady must be emitted when true: %s", encoded)
}
}
// "accepts only the privacy-safe route snapshot payload" plus the superRefine.
func TestRouteDiagnosticSnapshot(t *testing.T) {
const childOne = `{"ordinal":1,"parent":{"kind":"host"},"effectiveCapacity":2,` +
`"childCount":1,"demandAgeMs":100,"queueWaitMs":10,"candidateStartMs":20,` +
`"firstDecodedFrameMs":80,"finalMs":80,"finalRoute":"direct","rejectionBucket":"none",` +
`"quality":{"eligibleWindows":3,"eligibleDurationMs":6000,"freezeWindows":1,` +
`"freezeCount":1,"freezeDurationMs":250,"pauseCount":0,"pauseDurationMs":0}}`
childTwo := func(parent string) string {
return `{"ordinal":2,"parent":` + parent + `,"effectiveCapacity":0,"childCount":0,` +
`"demandAgeMs":90,"queueWaitMs":null,"candidateStartMs":null,` +
`"firstDecodedFrameMs":null,"finalMs":null,"finalRoute":"waiting",` +
`"rejectionBucket":"sfu-admission","quality":null}`
}
snapshot := func(children, operation string) []byte {
return []byte(`{"type":"route-diagnostic-snapshot","snapshot":{"children":` +
children + `,"operation":` + operation + `}}`)
}
const operation = `{"childOrdinal":2,"reason":"join","stage":"admission",` +
`"cursor":1,"candidateCount":2}`
assertServerMessage(t, "full snapshot",
snapshot(`[`+childOne+`,`+childTwo(`{"kind":"viewer","ordinal":1}`)+`]`, operation), true)
assertServerMessage(t, "empty snapshot", snapshot(`[]`, `null`), true)
assertServerMessage(t, "leaked peer id",
[]byte(`{"type":"route-diagnostic-snapshot","snapshot":{"children":[],`+
`"operation":null,"peerId":"viewer_private_12345678"}}`), false)
assertServerMessage(t, "leaked connection id",
snapshot(`[{"ordinal":1,"parent":{"kind":"host"},"effectiveCapacity":2,"childCount":1,`+
`"demandAgeMs":100,"queueWaitMs":10,"candidateStartMs":20,`+
`"firstDecodedFrameMs":80,"finalMs":80,"finalRoute":"direct",`+
`"rejectionBucket":"none","quality":null,`+
`"connectionId":"connection_private_12345678"}]`, `null`), false)
assertServerMessage(t, "negative freeze count",
snapshot(`[{"ordinal":1,"parent":{"kind":"host"},"effectiveCapacity":2,"childCount":1,`+
`"demandAgeMs":100,"queueWaitMs":10,"candidateStartMs":20,`+
`"firstDecodedFrameMs":80,"finalMs":80,"finalRoute":"direct",`+
`"rejectionBucket":"none","quality":{"eligibleWindows":3,`+
`"eligibleDurationMs":6000,"freezeWindows":1,"freezeCount":-1,`+
`"freezeDurationMs":250,"pauseCount":0,"pauseDurationMs":0}}]`, `null`), false)
assertServerMessage(t, "self parent",
snapshot(`[`+childOne+`,`+childTwo(`{"kind":"viewer","ordinal":2}`)+`]`, `null`), false)
assertServerMessage(t, "unknown parent ordinal",
snapshot(`[`+childOne+`,`+childTwo(`{"kind":"viewer","ordinal":7}`)+`]`, `null`), false)
assertServerMessage(t, "duplicate ordinals",
snapshot(`[`+childOne+`,`+childOne+`]`, `null`), false)
assertServerMessage(t, "operation names an absent child",
snapshot(`[`+childOne+`]`, operation), false)
assertServerMessage(t, "cursor past the candidate count",
snapshot(`[`+childOne+`,`+childTwo(`{"kind":"viewer","ordinal":1}`)+`]`,
`{"childOrdinal":2,"reason":"join","stage":"admission","cursor":2,`+
`"candidateCount":2}`), false)
assertServerMessage(t, "raw rejection bucket on the operation",
snapshot(`[`+childOne+`,`+childTwo(`{"kind":"viewer","ordinal":1}`)+`]`,
`{"childOrdinal":2,"reason":"join","stage":"admission","cursor":1,`+
`"candidateCount":2,"rejectionBucket":"raw-error"}`), false)
assertServerMessage(t, "parent ordinal on a host parent",
snapshot(`[{"ordinal":1,"parent":{"kind":"host","ordinal":1},"effectiveCapacity":2,`+
`"childCount":1,"demandAgeMs":null,"queueWaitMs":null,"candidateStartMs":null,`+
`"firstDecodedFrameMs":null,"finalMs":null,"finalRoute":"direct",`+
`"rejectionBucket":"none","quality":null}]`, `null`), false)
}
// "keeps route messages strict alongside the single authenticated mode" and
// "accepts every bounded hybrid upstream shape".
func TestParticipantRouteAssignment(t *testing.T) {
routeUpdate := func(assignment string) []byte {
return []byte(`{"type":"route-update","revision":9,"phase":"active","assignment":` +
assignment + `}`)
}
for _, sample := range []struct {
name string
assignment string
want bool
}{
{"no upstream", testAssignmentNone, true},
{"peer upstream", `{"upstream":{"kind":"peer","peerId":"parent_12345678"},` +
`"childPeerIds":["child_12345678","child_87654321"],` +
`"sfuPublicationGeneration":null}`, true},
{"sfu upstream", `{"upstream":{"kind":"sfu"},"childPeerIds":[],` +
`"sfuPublicationGeneration":"generation_12345678"}`, true},
{"sfu upstream without a generation", `{"upstream":{"kind":"sfu"},` +
`"childPeerIds":[],"sfuPublicationGeneration":null}`, false},
{"peer upstream owning a generation",
`{"upstream":{"kind":"peer","peerId":"parent_12345678"},"childPeerIds":[],` +
`"sfuPublicationGeneration":"generation_12345678"}`, false},
{"too many children", `{"upstream":{"kind":"none"},"childPeerIds":` +
`["child_12345678","child_87654321","child_third_1234","child_overflow_1"],` +
`"sfuPublicationGeneration":null}`, false},
{"duplicate children", `{"upstream":{"kind":"none"},` +
`"childPeerIds":["child_12345678","child_12345678"],` +
`"sfuPublicationGeneration":null}`, false},
{"absent generation key", `{"upstream":{"kind":"none"},"childPeerIds":[]}`, false},
{"peer id on a none upstream", `{"upstream":{"kind":"none",` +
`"peerId":"parent_12345678"},"childPeerIds":[],` +
`"sfuPublicationGeneration":null}`, false},
} {
assertServerMessage(t, "assignment "+sample.name,
routeUpdate(sample.assignment), sample.want)
}
assertServerMessage(t, "prepare without a candidate",
[]byte(`{"type":"route-update","revision":9,"phase":"prepare","assignment":`+
testAssignmentNone+`}`), false)
assertServerMessage(t, "active with a candidate",
[]byte(`{"type":"route-update","revision":9,"phase":"active","assignment":`+
testAssignmentNone+`,"candidate":{"childPeerId":"child_12345678",`+
`"connectionId":"connection_12345678","transport":"direct","qualityProbe":false}}`),
false)
assertServerMessage(t, "prepare with a room-wide field",
[]byte(`{"type":"route-update","revision":9,"phase":"prepare","assignment":`+
testAssignmentNone+`,"roomAssignments":[]}`), false)
}
// "accepts only bounded connection-attempt progress on a prepared route".
func TestPreparedRouteCandidate(t *testing.T) {
prepared := func(candidate string) []byte {
return []byte(`{"type":"route-update","revision":9,"phase":"prepare","assignment":` +
testAssignmentNone + `,"candidate":` + candidate + `}`)
}
const base = `"childPeerId":"child_12345678","connectionId":"connection_12345678",` +
`"transport":"direct","qualityProbe":false`
for _, sample := range []struct {
attempt string
want bool
}{
{`{"current":1,"total":3}`, true},
{`{"current":2,"total":3}`, true},
{`{"current":3,"total":3}`, true},
{`{"current":0,"total":3}`, false},
{`{"current":4,"total":3}`, false},
{`{"current":1,"total":4}`, false},
{`{"current":1.5,"total":3}`, false},
} {
assertServerMessage(t, "connectionAttempt "+sample.attempt,
prepared(`{`+base+`,"connectionAttempt":`+sample.attempt+`}`), sample.want)
}
assertServerMessage(t, "candidate without an attempt", prepared(`{`+base+`}`), true)
assertServerMessage(t, "candidate with an unknown key",
prepared(`{`+base+`,"unexpected":null}`), false)
assertServerMessage(t, "candidate with a null attempt",
prepared(`{`+base+`,"connectionAttempt":null}`), false)
}
// "accepts only canonical server-derived viewer evidence envelopes" and
// "keeps route messages strict" for sfu-config.
func TestForwardedEvidenceAndSfuConfig(t *testing.T) {
const metrics = `{"natTraversalPath":"ordinary","width":null,"height":null,` +
`"framesPerSecond":null,"bitrateKbps":null,"packetsReceivedDelta":null,` +
`"packetsLostDelta":null,"rttMs":null,"jitterMs":null,"framesDecodedDelta":null,` +
`"framesDroppedDelta":null,"decodeMsPerFrame":null,"freezeCountDelta":null,` +
`"freezeDurationMsDelta":null,"pauseCountDelta":null,"pauseDurationMsDelta":null,` +
`"codec":null,"codecProfile":null,"codecParameters":null,"audioBitrateKbps":null,` +
`"audioPacketLossPercent":null,"audioJitterMs":null,"audioVideoPlayoutDeltaMs":null,` +
`"videoJitterBufferDelayMs":null,"audioJitterBufferDelayMs":null,` +
`"audioConcealedSamplesPercent":null,"audioConcealmentEventsDelta":null,` +
`"audioCodec":null}`
forwarded := func(extra string, upstream string) []byte {
return []byte(`{"type":"viewer-quality-evidence","viewerPeerId":"viewer_12345678",` +
`"upstream":` + upstream + `,"guard":{"connectionId":"connection_12345678",` +
`"routeRevision":0,"presentationEpoch":0},"sequence":0,"windowMs":2000,` +
`"metrics":` + metrics + extra + `}`)
}
assertServerMessage(t, "peer upstream",
forwarded("", `{"kind":"peer","peerId":"host_12345678"}`), true)
assertServerMessage(t, "sfu upstream", forwarded("", `{"kind":"sfu"}`), true)
assertServerMessage(t, "no upstream", forwarded("", `{"kind":"none"}`), false)
assertServerMessage(t, "leaked room id",
forwarded(`,"roomId":"1234"`, `{"kind":"sfu"}`), false)
assertServerMessage(t, "missing upstream",
[]byte(`{"type":"viewer-quality-evidence","viewerPeerId":"viewer_12345678",`+
`"guard":{"connectionId":"connection_12345678","routeRevision":0,`+
`"presentationEpoch":0},"sequence":0,"windowMs":2000,"metrics":`+metrics+`}`), false)
assertServerMessage(t, "sfu config",
[]byte(`{"type":"sfu-config","revision":9,"url":"wss://sfu.example.test",`+
`"token":"header.payload.signature"}`), true)
assertServerMessage(t, "sfu config with an oversized token",
[]byte(`{"type":"sfu-config","revision":9,"url":"wss://sfu.example.test","token":"`+
repeat("x", MaxSfuTokenLength+1)+`"}`), false)
}
// zod's z.number().int() accepts an integral JSON number in any spelling.
func TestIntAcceptsIntegralNumberForms(t *testing.T) {
for _, sample := range []struct {
raw string
want bool
}{
{`7`, true}, {`7.0`, true}, {`7e0`, true}, {`1e3`, true},
{`7.5`, false}, {`"7"`, false}, {`true`, false},
} {
assertClientMessage(t, "revision "+sample.raw,
[]byte(`{"type":"route-media-unavailable","revision":`+sample.raw+`}`), sample.want)
}
}
// JSON.parse keeps the last occurrence of a duplicate key.
func TestDuplicateKeysKeepTheLastOccurrence(t *testing.T) {
message, err := DecodeClientMessage(
[]byte(`{"type":"relay-capacity","downstreamEdges":9,"downstreamEdges":2}`))
if err != nil {
t.Fatalf("the last occurrence must win: %v", err)
}
if capacity, ok := message.(RelayCapacityMessage); !ok || capacity.DownstreamEdges != 2 {
t.Fatalf("decoded %#v, want downstreamEdges 2", message)
}
if _, err := DecodeClientMessage(
[]byte(`{"type":"relay-capacity","downstreamEdges":2,"downstreamEdges":9}`)); err == nil {
t.Fatal("the last occurrence must be the one validated")
}
// A duplicated object is replaced, not merged.
if _, err := DecodeClientMessage(
[]byte(`{"type":"set-quality-settings","qualitySettings":{"resolution":"480p"},` +
`"qualitySettings":` + testQualitySettings + `}`)); err != nil {
t.Fatalf("a duplicated object must be replaced wholesale: %v", err)
}
}
// JSON.stringify does not escape "<", ">" or "&"; display names may hold them.
func TestEncodeServerMessageDoesNotEscapeHTML(t *testing.T) {
encoded, err := EncodeServerMessage(ViewerPresenceMessage{
Type: "viewer-presence",
Viewers: []ParticipantPresenceEntry{
HostPresenceEntry("host_12345678", "a<b>&c"),
},
})
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(encoded, []byte(`"displayName":"a<b>&c"`)) {
t.Fatalf("display name must survive unescaped: %s", encoded)
}
if bytes.HasSuffix(encoded, []byte("\n")) {
t.Fatalf("the encoder newline must be dropped: %q", encoded)
}
}
// Empty collections encode as [] the way the TypeScript literals do.
func TestEncodeServerMessageEmitsEmptyArrays(t *testing.T) {
encoded, err := EncodeServerMessage(ViewerPresenceMessage{Type: "viewer-presence"})
if err != nil {
t.Fatal(err)
}
if string(encoded) != `{"type":"viewer-presence","viewers":[]}` {
t.Fatalf("got %s", encoded)
}
encoded, err = EncodeServerMessage(RouteDiagnosticSnapshotMessage{
Type: "route-diagnostic-snapshot"})
if err != nil {
t.Fatal(err)
}
if string(encoded) !=
`{"type":"route-diagnostic-snapshot","snapshot":{"children":[],"operation":null}}` {
t.Fatalf("got %s", encoded)
}
}
// The authenticated key order follows the object literal in signaling.ts.
func TestAuthenticatedKeyOrder(t *testing.T) {
standby := "wss://sfu.example.test"
paused := true
encoded, err := EncodeServerMessage(AuthenticatedViewerMessage{
Type: "authenticated", Protocol: SignalingProtocol, PeerID: "viewer_12345678",
MaxViewers: 8, EndpointMediaCopyCapacity: 2, HostOnline: true, HostPaused: &paused,
IceConfig: IceConfig{}, RoutePolicy: DefaultRoutePolicy, CodeEntryPolicy: CodeEntryOpen,
ViewerAuthorizationGeneration: "viewer_generation_12345678",
MediaMode: "peer-assisted", RouteRevision: 0,
QualitySettings: DefaultQualitySettings, SfuStandbyURL: &standby, Role: RoleViewer,
})
if err != nil {
t.Fatal(err)
}
var order []string
decoder := json.NewDecoder(bytes.NewReader(encoded))
if _, err := decoder.Token(); err != nil {
t.Fatal(err)
}
for decoder.More() {
key, err := decoder.Token()
if err != nil {
t.Fatal(err)
}
order = append(order, key.(string))
var skip json.RawMessage
if err := decoder.Decode(&skip); err != nil {
t.Fatal(err)
}
}
want := []string{
"type", "protocol", "peerId", "roomExpiresAt", "maxViewers",
"endpointMediaCopyCapacity", "hostOnline", "hostPaused", "connectionId",
"iceConfig", "routePolicy", "codeEntryPolicy", "viewerAuthorizationGeneration",
"mediaMode", "shareGeneration", "routeRevision", "routeAssignment",
"qualitySettings", "sfuStandbyUrl", "role",
}
if strings.Join(order, ",") != strings.Join(want, ",") {
t.Fatalf("key order\n got: %v\nwant: %v", order, want)
}
}
// Optional keys reject an explicit null; nullable keys reject absence.
func TestOptionalAndNullableAreExact(t *testing.T) {
for _, sample := range []struct {
name string
raw string
want bool
}{
{"null optional shareGeneration",
`{"type":"stop-sharing","shareGeneration":null}`, false},
{"absent optional shareGeneration", `{"type":"stop-sharing"}`, true},
{"null optional targetPeerId",
`{"type":"restart-request","targetPeerId":null,` +
`"connectionId":"connection_12345678","rebuild":true}`, false},
{"null optional qualityApproved",
`{"type":"route-ready","revision":1,"phase":"prepare","qualityApproved":null}`, false},
{"null nullable connectionId",
`{"type":"route-failed","revision":1,"phase":"active","connectionId":null}`, true},
{"absent nullable connectionId",
`{"type":"route-failed","revision":1,"phase":"active"}`, false},
} {
assertClientMessage(t, sample.name, []byte(sample.raw), sample.want)
}
}
// A host authenticate without routePolicy takes DEFAULT_ROUTE_POLICY.
func TestAuthenticateAppliesTheRoutePolicyDefault(t *testing.T) {
message, err := DecodeClientMessage([]byte(
`{"type":"authenticate","protocol":"screener-v21","roomId":"1234","role":"host",` +
`"token":"` + repeat("a", 43) + `","clientId":"client_12345678"}`))
if err != nil {
t.Fatal(err)
}
host, ok := message.(AuthenticateHostMessage)
if !ok {
t.Fatalf("decoded %#v, want AuthenticateHostMessage", message)
}
if host.RoutePolicy != DefaultRoutePolicy {
t.Fatalf("routePolicy = %+v, want %+v", host.RoutePolicy, DefaultRoutePolicy)
}
}
// The sealed unions accept nothing outside their own message structs.
func TestDecodeRejectsUnknownEnvelopes(t *testing.T) {
for _, raw := range []string{`{}`, `[]`, `null`, `"authenticate"`, `{"type":42}`,
`{"type":"unknown"}`, `{"kind":"signal"}`} {
if _, err := DecodeClientMessage([]byte(raw)); err == nil {
t.Errorf("client decode accepted %s", raw)
}
if _, err := DecodeServerMessage([]byte(raw)); err == nil {
t.Errorf("server decode accepted %s", raw)
}
}
}