210 lines
11 KiB
Go
210 lines
11 KiB
Go
package dto
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"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: "{}", Encoding: "base64", 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.AssetFiles[0].Encoding != "base64" || 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/player-lookup.sql", MaxRows: 50, TimeoutSeconds: 10, PollIntervalSeconds: 3,
|
|
Projections: []GameClientBridgeQueryProjectionDeclarationBody{{Collection: "users", RowPath: "rows", MatchField: "kind", MatchValue: "player", UpsertKeys: []string{"steamId"}, FieldMappings: map[string]string{"steamId": "steamId"}, FixedValues: map[string]string{"source": "sqlite"}, ObservedAtField: "sampledAt", MergeExisting: true}},
|
|
}},
|
|
LifecycleProjections: []GameClientBridgeLifecycleProjectionDeclarationBody{{Key: "server.stop", Capabilities: []string{"process.stop"}, Target: GameClientBridgeBulkProjectionTargetBody{Collection: "users", MatchField: "online", MatchValue: "true", FixedValues: map[string]string{"online": "false"}, ObservedAtField: "lastLogoutAt", ActivityTarget: &GameClientBridgeBulkActivityTargetBody{Collection: "activity", UpsertKeys: []string{"steamId", "observedAt"}, RowMappings: map[string]string{"steamId": "steamId"}, FixedValues: map[string]string{"eventType": "logout"}, ObservedAtField: "observedAt"}}}},
|
|
DataPacks: []GameClientBridgeDataPackDeclarationBody{{Key: "db-v1", DatabaseUserVersion: 1, ConfigMapRefs: []string{"data/config.json"}, DataRefs: []string{"data/items.json"}}},
|
|
CommandRetentionSeconds: 86400,
|
|
MaxCommands: 1000,
|
|
Pages: []GameClientBridgePageContractBody{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}}},
|
|
}
|
|
|
|
domainManifest := body.ToDomain()
|
|
if len(domainManifest.QueryTemplates) != 1 || domainManifest.QueryTemplates[0].SQLRef != "sql/player-lookup.sql" || domainManifest.QueryTemplates[0].PollIntervalSeconds != 3 || len(domainManifest.QueryTemplates[0].Projections) != 1 || domainManifest.QueryTemplates[0].Projections[0].MatchValue != "player" || !domainManifest.QueryTemplates[0].Projections[0].MergeExisting || len(domainManifest.LifecycleProjections) != 1 || domainManifest.LifecycleProjections[0].Target.ActivityTarget.FixedValues["eventType"] != "logout" || len(domainManifest.DataPacks) != 1 || domainManifest.DataPacks[0].DataRefs[0] != "data/items.json" || domainManifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" {
|
|
t.Fatalf("query template conversion lost declaration fields: %#v", domainManifest)
|
|
}
|
|
domainManifest.QueryTemplates[0].Projections[0].FieldMappings["steamId"] = "mutated"
|
|
if body.QueryTemplates[0].Projections[0].FieldMappings["steamId"] != "steamId" {
|
|
t.Fatal("query projection target aliases request DTO data")
|
|
}
|
|
domainManifest.QueryTemplates[0].Projections[0].FieldMappings["steamId"] = "steamId"
|
|
domainManifest.LifecycleProjections[0].Target.ActivityTarget.RowMappings["steamId"] = "mutated"
|
|
if body.LifecycleProjections[0].Target.ActivityTarget.RowMappings["steamId"] != "steamId" {
|
|
t.Fatal("lifecycle projection target aliases request DTO data")
|
|
}
|
|
domainManifest.LifecycleProjections[0].Target.ActivityTarget.RowMappings["steamId"] = "steamId"
|
|
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"
|
|
domainManifest.DataPacks[0].DataRefs[0] = "mutated"
|
|
if body.DataPacks[0].DataRefs[0] != "data/items.json" {
|
|
t.Fatal("data pack data refs alias request DTO data")
|
|
}
|
|
domainManifest.DataPacks[0].DataRefs[0] = "data/items.json"
|
|
|
|
response := gameClientBridgeManifestFromDomain(domainManifest)
|
|
if !response.QueryTemplates[0].Projections[0].MergeExisting {
|
|
t.Fatal("query projection mergeExisting was not preserved")
|
|
}
|
|
response.QueryTemplates[0].Projections[0].FixedValues["source"] = "mutated"
|
|
if domainManifest.QueryTemplates[0].Projections[0].FixedValues["source"] != "sqlite" {
|
|
t.Fatal("query projection target aliases domain data")
|
|
}
|
|
response.QueryTemplates[0].Projections[0].FixedValues["source"] = "sqlite"
|
|
response.LifecycleProjections[0].Target.FixedValues["online"] = "mutated"
|
|
if domainManifest.LifecycleProjections[0].Target.FixedValues["online"] != "false" {
|
|
t.Fatal("lifecycle projection target aliases domain data")
|
|
}
|
|
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", "maxRows", "timeoutSeconds", "pollIntervalSeconds", "projections"}
|
|
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)
|
|
}
|
|
}
|
|
}
|