Files
browser/platform/validator/game_client_bridge_test.go

165 lines
9.8 KiB
Go

package validator
import (
"encoding/json"
"fmt"
"math"
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
func validBridgeQueueRequest() domain.GameClientBridgeQueueRequest {
return domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "hello"}, IdempotencyKey: "diag-1", ExpiresAt: time.Now().UTC().Add(time.Minute)}
}
func TestValidateGameClientBridgeRequests(t *testing.T) {
now := time.Now().UTC()
checks := map[string]error{
"queue": ValidateGameClientBridgeQueueRequest(validBridgeQueueRequest()),
"claim": ValidateGameClientBridgeClaimRequest(domain.GameClientBridgeClaimRequest{SessionToken: "session", Limit: 50}),
"ack": ValidateGameClientBridgeAckRequest(domain.GameClientBridgeAckRequest{SessionToken: "session", CommandID: "command-1", FencingToken: 1}),
"result": ValidateGameClientBridgeResultRequest(domain.GameClientBridgeResultRequest{SessionToken: "session", CommandID: "command-1", FencingToken: 1, Status: domain.GameClientBridgeResultSucceeded, Summary: "completed"}),
"cancel": ValidateGameClientBridgeCancelRequest(domain.GameClientBridgeCancelRequest{CommandID: "command-1", Reason: "operator request"}),
"snapshot": ValidateGameClientBridgeSnapshotIngestRequest(domain.GameClientBridgeSnapshotIngestRequest{SessionToken: "session", Type: "players", SchemaVersion: "1.2.0", StreamKey: "current", Sequence: 1, ObservedAt: now, Payload: map[string]any{"players": []any{}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600}}),
"query": ValidateGameClientBridgeSnapshotQuery(domain.GameClientBridgeSnapshotQuery{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current", Limit: 200}),
}
for name, err := range checks {
if err != nil {
t.Fatalf("validate %s request: %v", name, err)
}
}
}
func TestValidateGameClientBridgeRequestFieldBounds(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{name: "queue identifier", err: func() error {
request := validBridgeQueueRequest()
request.CommandType = "bad/type"
return ValidateGameClientBridgeQueueRequest(request)
}(), want: "commandType"},
{name: "queue priority", err: func() error {
request := validBridgeQueueRequest()
request.Priority = 101
return ValidateGameClientBridgeQueueRequest(request)
}(), want: "priority"},
{name: "claim token", err: ValidateGameClientBridgeClaimRequest(domain.GameClientBridgeClaimRequest{SessionToken: " session"}), want: "sessionToken"},
{name: "claim limit", err: ValidateGameClientBridgeClaimRequest(domain.GameClientBridgeClaimRequest{SessionToken: "session", Limit: 51}), want: "limit"},
{name: "ack fence", err: ValidateGameClientBridgeAckRequest(domain.GameClientBridgeAckRequest{SessionToken: "session", CommandID: "command-1"}), want: "fencingToken"},
{name: "result state", err: ValidateGameClientBridgeResultRequest(domain.GameClientBridgeResultRequest{SessionToken: "session", CommandID: "command-1", FencingToken: 1, Status: "unexpected"}), want: "status"},
{name: "snapshot payload", err: ValidateGameClientBridgeSnapshotIngestRequest(domain.GameClientBridgeSnapshotIngestRequest{SessionToken: "session", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: time.Now(), Retention: domain.GameClientBridgeRetention{KeepForSeconds: 1}}), want: "payload"},
{name: "snapshot sequence", err: ValidateGameClientBridgeSnapshotIngestRequest(domain.GameClientBridgeSnapshotIngestRequest{SessionToken: "session", Type: "players", SchemaVersion: "1", StreamKey: "current", ObservedAt: time.Now(), Payload: map[string]any{}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 1}}), want: "sequence"},
{name: "snapshot retention", err: ValidateGameClientBridgeSnapshotIngestRequest(domain.GameClientBridgeSnapshotIngestRequest{SessionToken: "session", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: time.Now(), Payload: map[string]any{}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 31*24*60*60 + 1}}), want: "keepForSeconds"},
{name: "query field", err: ValidateGameClientBridgeSnapshotQuery(domain.GameClientBridgeSnapshotQuery{ServerInstanceID: "server-1", PluginID: "game.scum", StreamKey: "tcp://host"}), want: "streamKey"},
{name: "query limit", err: ValidateGameClientBridgeSnapshotQuery(domain.GameClientBridgeSnapshotQuery{ServerInstanceID: "server-1", PluginID: "game.scum", Limit: 201}), want: "limit"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.err == nil || !strings.Contains(test.err.Error(), test.want) {
t.Fatalf("expected %q violation, got %v", test.want, test.err)
}
})
}
}
func TestValidateGameClientBridgePayloadAcceptsJSONValuesWithoutKeyFalsePositives(t *testing.T) {
request := validBridgeQueueRequest()
request.Payload = map[string]any{
"nullValue": nil, "enabled": true, "count": int(2), "ratio": float64(1.5), "sequence": json.Number("9007199254740993"), "nested": []any{map[string]any{"value": "safe"}},
"secretaryName": "Mina", "socketCount": 2, "apiKeyEnabled": false, "sqlQueryTemplateKey": "players.by-id", "shellStatus": "unavailable", "logicalPath": "snapshots.current", "monkey": "ordinary", "chatMessage": "Select your reward from the list",
}
if err := ValidateGameClientBridgeQueueRequest(request); err != nil {
t.Fatalf("valid JSON payload with benign keys rejected: %v", err)
}
}
func TestValidateGameClientBridgePayloadPreservesPluginOwnedKeyNames(t *testing.T) {
keys := []string{"sessionToken", "authToken", "accessKey", "privateKey", "component-key", "databasePassword", "clientSecret", "api_key", "databaseDSN", "storageCredential", "hostPath", "absolute_path", "directSocket", "socketAddress", "runEndpoint", "runUrl", "rawSQL", "rawQuery", "sqlText", "sql_statement", "shellCommand", "shell_script", "scriptBody", "terminalCommand", "commandLine"}
for _, key := range keys {
t.Run(key, func(t *testing.T) {
request := validBridgeQueueRequest()
request.Payload = map[string]any{key: "value"}
if err := ValidateGameClientBridgeQueueRequest(request); err != nil {
t.Fatalf("plugin-owned key %q should pass structural validation: %v", key, err)
}
})
}
}
func TestValidateGameClientBridgePayloadPreservesOpaqueStringMaterial(t *testing.T) {
values := []string{"/etc/passwd", "prefix path=/var/run/run.sock", `C:\\Users\\operator\\secret.txt`, "tcp://127.0.0.1:9000", "unix:///var/run/run.sock", "http://localhost:9000", "mysql://user:password@host/db", "secret://component/key", "vault://runtime/token", "Bearer abc123", "password=leak"}
for index, value := range values {
request := validBridgeQueueRequest()
request.IdempotencyKey = fmt.Sprintf("case-%d", index)
request.Payload = map[string]any{"value": value}
if err := ValidateGameClientBridgeQueueRequest(request); err != nil {
t.Fatalf("opaque plugin value %q should pass structural validation: %v", value, err)
}
}
}
func TestValidateGameClientBridgeResultAndCancelTextPreserveOpaqueContent(t *testing.T) {
if err := ValidateGameClientBridgeResultRequest(domain.GameClientBridgeResultRequest{SessionToken: "session", CommandID: "command-1", FencingToken: 1, Status: domain.GameClientBridgeResultFailed, Summary: "read /etc/passwd password=opaque"}); err != nil {
t.Fatalf("opaque result summary should pass structural validation: %v", err)
}
if err := ValidateGameClientBridgeCancelRequest(domain.GameClientBridgeCancelRequest{CommandID: "command-1", Reason: "Bearer private game token text"}); err != nil {
t.Fatalf("opaque cancel reason should pass structural validation: %v", err)
}
}
func TestValidateGameClientBridgePayloadRejectsNonJSONValuesAndInvalidNumbers(t *testing.T) {
tests := map[string]any{"typed map": map[string]string{"key": "value"}, "typed slice": []string{"value"}, "time": time.Now(), "channel": make(chan int), "not a number": math.NaN(), "infinity": math.Inf(1), "invalid number": json.Number("01")}
for name, value := range tests {
t.Run(name, func(t *testing.T) {
request := validBridgeQueueRequest()
request.Payload = map[string]any{"value": value}
if err := ValidateGameClientBridgeQueueRequest(request); err == nil {
t.Fatalf("expected %s rejection", name)
}
})
}
}
func TestValidateGameClientBridgePayloadStructuralBudgets(t *testing.T) {
tests := map[string]map[string]any{}
tooManyKeys := map[string]any{}
for index := 0; index < maxGameClientBridgeObjectKeys+1; index++ {
tooManyKeys[fmt.Sprintf("key%d", index)] = index
}
tests["object keys"] = tooManyKeys
deep := map[string]any{"value": true}
for index := 0; index < maxGameClientBridgePayloadDepth+2; index++ {
deep = map[string]any{"nested": deep}
}
tests["depth"] = deep
tests["array items"] = map[string]any{"items": make([]any, maxGameClientBridgeArrayItems+1)}
tests["string"] = map[string]any{"message": strings.Repeat("x", maxGameClientBridgePayloadString+1)}
tests["encoded size"] = map[string]any{"one": strings.Repeat("x", 15000), "two": strings.Repeat("x", 15000), "three": strings.Repeat("x", 15000), "four": strings.Repeat("x", 15000), "five": strings.Repeat("x", 15000)}
tests["invalid key"] = map[string]any{"bad key": true}
tests["invalid utf8"] = map[string]any{"value": string([]byte{0xff})}
wide := map[string]any{}
for index := 0; index < 64; index++ {
wide[fmt.Sprintf("items%d", index)] = make([]any, 128)
}
tests["node budget"] = wide
cycle := map[string]any{}
cycle["self"] = cycle
tests["cycle"] = cycle
for name, payload := range tests {
t.Run(name, func(t *testing.T) {
request := validBridgeQueueRequest()
request.Payload = payload
if err := ValidateGameClientBridgeQueueRequest(request); err == nil {
t.Fatalf("expected %s rejection", name)
}
})
}
}