445 lines
21 KiB
Go
445 lines
21 KiB
Go
package dto
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
func TestAIProviderResponseExposesOnlySecretPresence(t *testing.T) {
|
|
responseType := reflect.TypeOf(AIProviderResponse{})
|
|
if _, ok := responseType.FieldByName("BaseURL"); ok {
|
|
t.Fatal("AI provider response must not expose the provider base URL")
|
|
}
|
|
if _, ok := responseType.FieldByName("APIKey"); ok {
|
|
t.Fatal("AI provider response must not expose raw API key")
|
|
}
|
|
if _, ok := responseType.FieldByName("RawAPIKey"); ok {
|
|
t.Fatal("AI provider response must not expose raw API key")
|
|
}
|
|
if _, ok := responseType.FieldByName("APIKeyRef"); ok {
|
|
t.Fatal("AI provider response must not expose internal API key reference")
|
|
}
|
|
if _, ok := responseType.FieldByName("APIKeyConfigured"); !ok {
|
|
t.Fatal("AI provider response must expose API key presence")
|
|
}
|
|
if _, ok := responseType.FieldByName("BaseURLConfigured"); !ok {
|
|
t.Fatal("AI provider response must expose base URL presence")
|
|
}
|
|
}
|
|
|
|
func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) {
|
|
payload := `{
|
|
"runEndpointId":"run-local",
|
|
"sessionToken":"run-session",
|
|
"jobId":"job-probe",
|
|
"leaseToken":"lease-probe",
|
|
"attempt":1,
|
|
"state":"succeeded",
|
|
"progress":{"percent":100,"message":"done"},
|
|
"executionResult":{
|
|
"kind":"sqlite.schema-probe",
|
|
"sqliteSchemaProbe":{
|
|
"requestId":"job-probe",
|
|
"jobId":"job-probe",
|
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
|
"status":"compatible",
|
|
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
|
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
|
"observedAt":"2026-08-12T00:00:00Z",
|
|
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"limits":{"maxObjects":256,"maxColumnsPerObject":128,"maxIndexesPerObject":64,"maxForeignKeys":64,"maxCardinalityReads":64,"maxSampleRows":3,"timeoutMs":5000,"maxResultBytes":524288}
|
|
}
|
|
}
|
|
}`
|
|
var request RunJobResultRequest
|
|
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
|
t.Fatalf("unmarshal Run job result: %v", err)
|
|
}
|
|
domainRequest := request.ToDomain()
|
|
probe := domainRequest.ExecutionResult.SQLiteSchemaProbe
|
|
if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "scum-database" || probe.SourceFingerprint != "sha256:"+strings.Repeat("c", 64) || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) {
|
|
t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe)
|
|
}
|
|
}
|
|
|
|
func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) {
|
|
payload := `{
|
|
"runEndpointId":"run-local",
|
|
"sessionToken":"run-session",
|
|
"jobId":"job-query",
|
|
"leaseToken":"lease-query",
|
|
"attempt":1,
|
|
"state":"succeeded",
|
|
"progress":{"percent":100,"message":"done"},
|
|
"executionResult":{
|
|
"kind":"sqlite.template-query",
|
|
"sqliteTemplate":{
|
|
"requestId":"request-query",
|
|
"jobId":"job-query",
|
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
|
"status":"succeeded",
|
|
"capability":"players.read",
|
|
"targetKey":"scum-database",
|
|
"templateKey":"players.active.v1",
|
|
"adapterVersion":"scum-live-data-v0",
|
|
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
|
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
|
"parameterDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
|
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
|
"observedAt":"2026-08-13T00:00:00Z",
|
|
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"rowCount":1,
|
|
"rows":[{"externalPlayerId":"player-redacted","fame":12.5,"online":true,"squadId":null}],
|
|
"limits":{"maxParameters":64,"maxRows":500,"timeoutMs":5000,"busyTimeoutMs":250,"maxResultBytes":1048576}
|
|
}
|
|
}
|
|
}`
|
|
var request RunJobResultRequest
|
|
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
|
t.Fatalf("unmarshal Run job result: %v", err)
|
|
}
|
|
domainRequest := request.ToDomain()
|
|
result := domainRequest.ExecutionResult.SQLiteTemplate
|
|
if result == nil || result.TemplateKey != "players.active.v1" || result.AssetDigest != "sha256:"+strings.Repeat("d", 64) || result.RowCount != 1 || result.Rows[0]["fame"].(float64) != 12.5 {
|
|
t.Fatalf("sqliteTemplate envelope did not parse: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) {
|
|
payload := `{
|
|
"runEndpointId":"run-local",
|
|
"sessionToken":"run-session",
|
|
"jobId":"job-rcon",
|
|
"leaseToken":"lease-rcon",
|
|
"attempt":1,
|
|
"state":"succeeded",
|
|
"progress":{"percent":100,"message":"done"},
|
|
"executionResult":{
|
|
"kind":"rcon.template-command",
|
|
"rconTemplate":{
|
|
"requestId":"request-rcon",
|
|
"jobId":"job-rcon",
|
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
|
"status":"succeeded",
|
|
"capability":"economy-command.write",
|
|
"transportKey":"scum-rcon",
|
|
"targetKey":"scum-rcon",
|
|
"templateKey":"economy.fame.set.v1",
|
|
"adapterVersion":"scum-live-data-v0",
|
|
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
|
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
|
"payloadDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
|
"confirmationDigest":"sha256:` + strings.Repeat("f", 64) + `",
|
|
"targetIdentityDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
|
"observedAt":"2026-08-13T00:00:00Z",
|
|
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"responseDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
|
"confirmationStatus":"confirmed",
|
|
"confirmationDigestId":"sha256:` + strings.Repeat("2", 64) + `",
|
|
"safeSummary":"confirmed by declared readback",
|
|
"limits":{"maxPayloadBytes":2048,"timeoutMs":5000,"maxResponseBytes":16384,"maxConfirmRecords":16}
|
|
}
|
|
}
|
|
}`
|
|
var request RunJobResultRequest
|
|
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
|
t.Fatalf("unmarshal Run job result: %v", err)
|
|
}
|
|
domainRequest := request.ToDomain()
|
|
result := domainRequest.ExecutionResult.RCONTemplate
|
|
if result == nil || result.TemplateKey != "economy.fame.set.v1" || result.PayloadDigest != "sha256:"+strings.Repeat("e", 64) || result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed {
|
|
t.Fatalf("rconTemplate envelope did not parse: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestRunJobResultRequestParsesGuardedMutationEnvelope(t *testing.T) {
|
|
payload := `{
|
|
"runEndpointId":"run-local",
|
|
"sessionToken":"run-session",
|
|
"jobId":"job-mutation",
|
|
"leaseToken":"lease-mutation",
|
|
"attempt":1,
|
|
"state":"succeeded",
|
|
"progress":{"percent":100,"message":"done"},
|
|
"executionResult":{
|
|
"kind":"sqlite.guarded-mutation",
|
|
"guardedMutation":{
|
|
"requestId":"request-mutation",
|
|
"jobId":"job-mutation",
|
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
|
"status":"succeeded",
|
|
"capability":"profile-xml.write",
|
|
"targetKey":"scum-mutation-db",
|
|
"templateKey":"profile.attributes.patch.v1",
|
|
"adapterVersion":"scum-live-data-v0",
|
|
"schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `",
|
|
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
|
"sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `",
|
|
"targetIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `",
|
|
"expectedRowDigest":"sha256:` + strings.Repeat("2", 64) + `",
|
|
"expectedValueDigest":"sha256:` + strings.Repeat("3", 64) + `",
|
|
"expectedXmlDigest":"sha256:` + strings.Repeat("4", 64) + `",
|
|
"patchDigest":"sha256:` + strings.Repeat("5", 64) + `",
|
|
"backupEvidenceDigest":"sha256:` + strings.Repeat("6", 64) + `",
|
|
"offlineEvidenceDigest":"sha256:` + strings.Repeat("7", 64) + `",
|
|
"dangerConfirmationDigest":"sha256:` + strings.Repeat("8", 64) + `",
|
|
"readbackExpectationDigest":"sha256:` + strings.Repeat("9", 64) + `",
|
|
"observedAt":"2026-08-13T00:00:00Z",
|
|
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"beforeDigest":"sha256:` + strings.Repeat("a", 64) + `",
|
|
"afterDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"readbackDigest":"sha256:` + strings.Repeat("c", 64) + `",
|
|
"affectedRows":1,
|
|
"readbackStatus":"confirmed",
|
|
"safeSummary":"confirmed by declared readback",
|
|
"limits":{"maxPayloadBytes":4096,"timeoutMs":5000,"busyTimeoutMs":250,"maxReadbackBytes":16384,"maxAffectedRows":1}
|
|
}
|
|
}
|
|
}`
|
|
var request RunJobResultRequest
|
|
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
|
t.Fatalf("unmarshal Run job result: %v", err)
|
|
}
|
|
domainRequest := request.ToDomain()
|
|
result := domainRequest.ExecutionResult.GuardedMutation
|
|
if result == nil || result.TemplateKey != "profile.attributes.patch.v1" || result.PatchDigest != "sha256:"+strings.Repeat("5", 64) || result.AffectedRows != 1 || result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed {
|
|
t.Fatalf("guardedMutation envelope did not parse: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestRunJobResultRequestParsesParsedLogBatchEnvelope(t *testing.T) {
|
|
payload := `{
|
|
"runEndpointId":"run-local",
|
|
"sessionToken":"run-session",
|
|
"jobId":"job-log",
|
|
"leaseToken":"lease-log",
|
|
"attempt":1,
|
|
"state":"succeeded",
|
|
"progress":{"percent":100,"message":"done"},
|
|
"executionResult":{
|
|
"kind":"log.parsed-events",
|
|
"parsedLogBatch":{
|
|
"requestId":"request-log",
|
|
"jobId":"job-log",
|
|
"binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"},
|
|
"status":"succeeded",
|
|
"sourceKey":"scum-login-events",
|
|
"streamKey":"scum.login",
|
|
"parserKey":"scum-login-log-login-parser",
|
|
"parserVersion":"scum-login-log-v1",
|
|
"adapterVersion":"scum-live-data-v0",
|
|
"assetDigest":"sha256:` + strings.Repeat("d", 64) + `",
|
|
"parserDigest":"sha256:` + strings.Repeat("e", 64) + `",
|
|
"observedAt":"2026-08-13T00:00:00Z",
|
|
"resultDigest":"sha256:` + strings.Repeat("b", 64) + `",
|
|
"firstCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
|
"lastCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},
|
|
"tailState":"rotated",
|
|
"replay":true,
|
|
"eventCount":1,
|
|
"events":[{"eventType":"scum.login","occurredAt":"2026-08-13T00:00:00Z","cursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},"logicalEventDigest":"sha256:` + strings.Repeat("3", 64) + `","eventDigest":"sha256:` + strings.Repeat("4", 64) + `","payloadDigest":"sha256:` + strings.Repeat("5", 64) + `","payload":{"externalPlayerId":"player-redacted","displayName":"Known Player"}}],
|
|
"safeSummary":"one sanitized login event parsed from declared source",
|
|
"limits":{"maxEvents":256,"maxPayloadBytes":16384,"maxLineBytes":4096,"maxResultBytes":262144}
|
|
}
|
|
}
|
|
}`
|
|
var request RunJobResultRequest
|
|
if err := json.Unmarshal([]byte(payload), &request); err != nil {
|
|
t.Fatalf("unmarshal Run parsed log result: %v", err)
|
|
}
|
|
domainRequest := request.ToDomain()
|
|
result := domainRequest.ExecutionResult.ParsedLogBatch
|
|
if result == nil || result.ParserKey != "scum-login-log-login-parser" || result.ParserDigest != "sha256:"+strings.Repeat("e", 64) || result.EventCount != 1 || result.Events[0].LogicalEventDigest != "sha256:"+strings.Repeat("3", 64) {
|
|
t.Fatalf("parsedLogBatch envelope did not parse: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestAIProviderFromDomainCopiesModels(t *testing.T) {
|
|
provider := domain.AIProvider{
|
|
ID: "ai.openai",
|
|
Name: "OpenAI",
|
|
Kind: domain.AIProviderKindOpenAI,
|
|
BaseURL: "https://api.openai.com/v1",
|
|
APIKeyRef: "secret://providers/openai",
|
|
Models: []string{"gpt-4.1"},
|
|
DefaultModel: "gpt-4.1",
|
|
RelayMode: domain.AIRelayModeDirect,
|
|
TimeoutMS: 30000,
|
|
Status: domain.AIProviderStatusActive,
|
|
RedactionPolicy: "default",
|
|
}
|
|
|
|
response := AIProviderFromDomain(provider)
|
|
response.Models[0] = "mutated"
|
|
|
|
if provider.Models[0] != "gpt-4.1" {
|
|
t.Fatalf("expected response models to be copied, got source models %+v", provider.Models)
|
|
}
|
|
if !response.APIKeyConfigured {
|
|
t.Fatal("expected configured API key presence")
|
|
}
|
|
if !response.BaseURLConfigured {
|
|
t.Fatal("expected configured base URL presence")
|
|
}
|
|
}
|
|
|
|
func TestGamePluginManifestRegistrationToDomainCopiesSlices(t *testing.T) {
|
|
request := GamePluginManifestRegistrationRequest{
|
|
ManifestRef: "artifact://manifests/game.example/0.1.0",
|
|
Manifest: GamePluginManifestBody{
|
|
ID: "game.example",
|
|
Name: "Example Server",
|
|
Version: "0.1.0",
|
|
Kind: "game-plugin",
|
|
Tags: []string{"example"},
|
|
Capabilities: []string{"process.start"},
|
|
Permissions: []string{"server.lifecycle"},
|
|
Server: GamePluginManifestServerBody{
|
|
Type: "example",
|
|
DisplayName: "Example Server",
|
|
SupportedOS: []string{"linux"},
|
|
CreateFormSchema: "schemas/create-form.schema.json",
|
|
},
|
|
Actions: PluginLifecycleActionsBody{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json"},
|
|
AssetFiles: []PluginAssetFileBody{
|
|
{Path: "actions/install.json", Mode: 0o600},
|
|
},
|
|
Pages: []GamePluginPageBody{
|
|
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
|
},
|
|
AI: GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}},
|
|
},
|
|
AssetFiles: []PluginAssetFileBody{
|
|
{Path: "actions/install.json", Content: "{}", Mode: 0o600},
|
|
},
|
|
}
|
|
|
|
domainRegistration := request.ToDomain()
|
|
domainRegistration.Manifest.Tags[0] = "mutated"
|
|
domainRegistration.Manifest.Server.SupportedOS[0] = "darwin"
|
|
domainRegistration.Manifest.AssetFiles[0].Path = "actions/mutated.json"
|
|
domainRegistration.AssetFiles[0].Content = "mutated"
|
|
domainRegistration.Manifest.Pages[0].Permissions[0] = "ai.invoke"
|
|
domainRegistration.Manifest.AI.Purposes[0] = "config.suggest"
|
|
|
|
if request.Manifest.Tags[0] != "example" || request.Manifest.Server.SupportedOS[0] != "linux" || request.Manifest.AssetFiles[0].Path != "actions/install.json" || request.AssetFiles[0].Content != "{}" || request.Manifest.Pages[0].Permissions[0] != "server.logs.read" || request.Manifest.AI.Purposes[0] != "logs.diagnose" {
|
|
t.Fatalf("expected manifest request slices to be copied, got %+v", request)
|
|
}
|
|
}
|
|
|
|
func TestGamePluginFromDomainCopiesRegistryMetadata(t *testing.T) {
|
|
plugin := domain.GamePlugin{
|
|
ID: "game.example",
|
|
Name: "Example Server",
|
|
Version: "0.1.0",
|
|
ServerType: "example",
|
|
RequiredRunCapabilities: []string{"process.start"},
|
|
DeclaredPermissions: []string{"server.lifecycle"},
|
|
SupportedOS: []string{"linux"},
|
|
Pages: []domain.GamePluginPage{
|
|
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
|
},
|
|
Tags: []string{"example"},
|
|
AIPurposes: []string{"logs.diagnose"},
|
|
}
|
|
|
|
response := GamePluginFromDomain(plugin)
|
|
response.RequiredRunCapabilities[0] = "files.read"
|
|
response.DeclaredPermissions[0] = "ai.invoke"
|
|
response.SupportedOS[0] = "darwin"
|
|
response.Pages[0].Permissions[0] = "ai.invoke"
|
|
response.Tags[0] = "mutated"
|
|
response.AIPurposes[0] = "config.suggest"
|
|
|
|
if plugin.RequiredRunCapabilities[0] != "process.start" || plugin.DeclaredPermissions[0] != "server.lifecycle" || plugin.SupportedOS[0] != "linux" || plugin.Pages[0].Permissions[0] != "server.logs.read" || plugin.Tags[0] != "example" || plugin.AIPurposes[0] != "logs.diagnose" {
|
|
t.Fatalf("expected plugin response registry metadata to be copied, got %+v", plugin)
|
|
}
|
|
}
|
|
|
|
func TestGameClientBridgeQueryTemplateDeclarationRoundTripIsSafe(t *testing.T) {
|
|
body := GameClientBridgeManifestBody{
|
|
QueryTemplates: []GameClientBridgeQueryTemplateDeclarationBody{{
|
|
Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite",
|
|
ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10,
|
|
}},
|
|
CommandRetentionSeconds: 86400,
|
|
MaxCommands: 1000,
|
|
Pages: []GameClientBridgePageContractBody{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}}},
|
|
}
|
|
|
|
domainManifest := body.ToDomain()
|
|
if len(domainManifest.QueryTemplates) != 1 || domainManifest.QueryTemplates[0].TransportKey != "sqlite-db" || domainManifest.QueryTemplates[0].MaxRows != 50 || domainManifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
|
t.Fatalf("query template conversion lost declaration fields: %#v", domainManifest)
|
|
}
|
|
domainManifest.Pages[0].QueryTemplateKeys[0] = "mutated"
|
|
if body.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
|
t.Fatal("query template page keys alias request DTO data")
|
|
}
|
|
domainManifest.Pages[0].QueryTemplateKeys[0] = "player.lookup"
|
|
|
|
response := gameClientBridgeManifestFromDomain(domainManifest)
|
|
response.Pages[0].QueryTemplateKeys[0] = "mutated"
|
|
if domainManifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
|
t.Fatal("query template page keys alias domain data")
|
|
}
|
|
|
|
encoded, err := json.Marshal(response.QueryTemplates[0])
|
|
if err != nil {
|
|
t.Fatalf("marshal safe query template projection: %v", err)
|
|
}
|
|
var projection map[string]any
|
|
if err := json.Unmarshal(encoded, &projection); err != nil {
|
|
t.Fatalf("decode safe query template projection: %v", err)
|
|
}
|
|
expectedFields := []string{"key", "title", "permission", "engine", "transportKey", "targetKey", "parameterSchemaRef", "resultSchemaRef", "maxRows", "timeoutSeconds"}
|
|
if len(projection) != len(expectedFields) {
|
|
t.Fatalf("query template projection contains unexpected fields: %s", encoded)
|
|
}
|
|
for _, field := range expectedFields {
|
|
if _, exists := projection[field]; !exists {
|
|
t.Fatalf("query template projection is missing %q: %s", field, encoded)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGameClientBridgeOperationTemplateDeclarationRoundTripIsSafe(t *testing.T) {
|
|
body := GameClientBridgeManifestBody{
|
|
OperationTemplates: []GameClientBridgeOperationTemplateDeclarationBody{{
|
|
Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: "platform-admin", Kind: "sqlite-mutation", TransportKey: "scum-mutation-db", TargetKey: "scum-mutation-db",
|
|
PayloadSchemaRef: "schemas/bridge/operations/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/operations/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/operations/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1,
|
|
Safety: GameClientBridgeOperationSafetyBody{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true},
|
|
}},
|
|
CommandRetentionSeconds: 86400,
|
|
MaxCommands: 1000,
|
|
Pages: []GameClientBridgePageContractBody{{PageKey: "players", OperationKeys: []string{"player.attribute.855.set"}}},
|
|
}
|
|
|
|
domainManifest := body.ToDomain()
|
|
if len(domainManifest.OperationTemplates) != 1 || domainManifest.OperationTemplates[0].Kind != "sqlite-mutation" || domainManifest.OperationTemplates[0].MaxRowsAffected != 1 || !domainManifest.OperationTemplates[0].Safety.RequiresBeforeValue || domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
|
t.Fatalf("operation template conversion lost declaration fields: %#v", domainManifest)
|
|
}
|
|
domainManifest.Pages[0].OperationKeys[0] = "mutated"
|
|
if body.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
|
t.Fatal("operation template page keys alias request DTO data")
|
|
}
|
|
domainManifest.Pages[0].OperationKeys[0] = "player.attribute.855.set"
|
|
|
|
response := gameClientBridgeManifestFromDomain(domainManifest)
|
|
response.Pages[0].OperationKeys[0] = "mutated"
|
|
if domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" {
|
|
t.Fatal("operation template page keys alias domain data")
|
|
}
|
|
|
|
encoded, err := json.Marshal(response.OperationTemplates[0])
|
|
if err != nil {
|
|
t.Fatalf("marshal safe operation template projection: %v", err)
|
|
}
|
|
if strings.Contains(strings.ToLower(string(encoded)), "sqltext") || strings.Contains(strings.ToLower(string(encoded)), "dsn") || strings.Contains(strings.ToLower(string(encoded)), "hostpath") || strings.Contains(strings.ToLower(string(encoded)), "socket") || strings.Contains(strings.ToLower(string(encoded)), "credential") {
|
|
t.Fatalf("operation template projection leaked unsafe material: %s", encoded)
|
|
}
|
|
}
|