88 lines
5.6 KiB
Go
88 lines
5.6 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/dto"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/service"
|
|
)
|
|
|
|
func TestGameClientBridgeOperatorRoutes(t *testing.T) {
|
|
store := repo.NewMemoryStore()
|
|
coreService := service.NewCoreService(store)
|
|
if err := coreService.SeedLocalPlatformAdmin(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plugin := validGamePluginRequest().ToDomain()
|
|
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command", "server.game-client.read")
|
|
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery)
|
|
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "sqlite-db", Kind: "sqlite", TargetKey: "db/sqlite", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery}}}
|
|
plugin.GameClientBridge = domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", Title: "Diagnostic ping", Permission: "server.game-client.command", PayloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", ResultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", TimeoutSeconds: 3600, MaxPayloadBytes: 4096}}, QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{{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}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}
|
|
if _, err := coreService.CreateGamePlugin(plugin); err != nil {
|
|
t.Fatalf("create bridge plugin: %v", err)
|
|
}
|
|
endpoint := validRunEndpointRequest().ToDomain()
|
|
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery)
|
|
if _, err := coreService.CreateRunEndpoint(endpoint); err != nil {
|
|
t.Fatalf("create run endpoint: %v", err)
|
|
}
|
|
router := NewTestRouterWithCore(coreService)
|
|
adminSession := createAdminSession(t, router)
|
|
if _, err := coreService.CreateServerInstanceForSession(adminSession, domain.ServerInstance{ID: "server-bridge", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Bridge Server"}); err != nil {
|
|
t.Fatalf("create bridge server: %v", err)
|
|
}
|
|
|
|
status := getJSONWithAuth[dto.GameClientBridgeStatusResponse](t, router, "/api/v1/server-instances/server-bridge/game-client-bridge", adminSession)
|
|
if status.ServerInstanceID != "server-bridge" || status.PluginID != "server.scum" || !status.Available || status.Profiles == nil {
|
|
t.Fatalf("unexpected bridge status: %#v", status)
|
|
}
|
|
if len(status.Profiles) != 1 || status.Profiles[0].ProfileKey != "plugin-owned" || len(status.Profiles[0].QueryTemplateKeys) != 1 || status.Profiles[0].QueryTemplateKeys[0] != "player.lookup" {
|
|
t.Fatalf("bridge status did not safely expose query template availability: %#v", status)
|
|
}
|
|
|
|
queue := dto.GameClientBridgeQueueRequest{ProfileKey: "plugin-owned", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "hello"}, IdempotencyKey: "diag-1", ExpiresAt: time.Now().UTC().Add(time.Hour)}
|
|
queuedRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-bridge/game-client-bridge/commands", queue, adminSession)
|
|
assertStatus(t, queuedRecorder, http.StatusAccepted)
|
|
queued := decodeBody[dto.GameClientBridgeCommandResponse](t, queuedRecorder)
|
|
if queued.ID == "" || queued.CommandType != queue.CommandType || queued.State != "pending" {
|
|
t.Fatalf("unexpected queued command: %#v", queued)
|
|
}
|
|
undeclared := queue
|
|
undeclared.CommandType = "undeclared.command"
|
|
undeclared.IdempotencyKey = "undeclared-1"
|
|
undeclaredRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-bridge/game-client-bridge/commands", undeclared, adminSession)
|
|
assertErrorResponse(t, undeclaredRecorder, http.StatusBadRequest, errorCodeValidation)
|
|
|
|
commands := getJSONWithAuth[dto.GameClientBridgeCommandListResponse](t, router, "/api/v1/server-instances/server-bridge/game-client-bridge/commands?state=pending", adminSession)
|
|
if commands.Count != 1 || commands.Items[0].ID != queued.ID {
|
|
t.Fatalf("unexpected command list: %#v", commands)
|
|
}
|
|
detail := getJSONWithAuth[dto.GameClientBridgeCommandResponse](t, router, "/api/v1/server-instances/server-bridge/game-client-bridge/commands/"+queued.ID, adminSession)
|
|
if detail.ID != queued.ID {
|
|
t.Fatalf("unexpected command detail: %#v", detail)
|
|
}
|
|
|
|
cancelRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/server-bridge/game-client-bridge/commands/"+queued.ID+"/cancel", dto.GameClientBridgeCancelRequest{Reason: "operator requested"}, adminSession)
|
|
assertStatus(t, cancelRecorder, http.StatusOK)
|
|
cancelled := decodeBody[dto.GameClientBridgeCancelResponse](t, cancelRecorder)
|
|
if cancelled.State != "cancelled" || cancelled.CommandID != queued.ID {
|
|
t.Fatalf("unexpected cancellation: %#v", cancelled)
|
|
}
|
|
|
|
snapshots := getJSONWithAuth[dto.GameClientBridgeSnapshotListResponse](t, router, "/api/v1/server-instances/server-bridge/game-client-bridge/snapshots?limit=20", adminSession)
|
|
if snapshots.Count != 0 || snapshots.Items == nil {
|
|
t.Fatalf("unexpected snapshot list: %#v", snapshots)
|
|
}
|
|
|
|
for _, serialized := range []string{queued.ResultSummary, status.Reason} {
|
|
if strings.Contains(strings.ToLower(serialized), "token") || strings.Contains(strings.ToLower(serialized), "secret") {
|
|
t.Fatalf("safe operator response leaked sensitive text: %q", serialized)
|
|
}
|
|
}
|
|
}
|