218 lines
10 KiB
Go
218 lines
10 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 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", SQLRef: "sql/scum-db-v57/users.sql", TargetTable: "scum_users", UpsertKeys: []string{"userProfileId"}, ColumnMappings: map[string]string{"userProfileId": "userProfileId"}, 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].SQLRef != "sql/scum-db-v57/users.sql" || domainManifest.QueryTemplates[0].RowTarget == nil || domainManifest.QueryTemplates[0].RowTarget.TargetTable != "scum_users" || 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", "sqlRef", "targetTable", "upsertKeys", "columnMappings", "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)
|
|
}
|
|
}
|