165 lines
9.8 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|