功能修改
This commit is contained in:
@@ -757,7 +757,7 @@ func TestCoreServiceManagesAIProviderMetadata(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("test enabled provider: %v", err)
|
||||
}
|
||||
if !testResult.Success || testResult.Mode != "metadata" {
|
||||
if !testResult.Success || testResult.Mode != "provider" {
|
||||
t.Fatalf("expected metadata test success, got %+v", testResult)
|
||||
}
|
||||
|
||||
@@ -1022,6 +1022,147 @@ func TestCoreServiceRemoteAccessRequiresPluginDeclaration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceDispatchesDeclaredSQLiteQueryTemplate(t *testing.T) {
|
||||
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
|
||||
|
||||
queued, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "query-template-dispatch-1",
|
||||
PluginID: plugin.ID,
|
||||
RouteKey: "remote",
|
||||
ServerInstanceID: instance.ID,
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
"declarationKey": "scum-db-read",
|
||||
"targetKey": "scum-db.player-lookup",
|
||||
"idempotencyKey": "query-template-dispatch-1",
|
||||
"input.templateKey": "players.by-id",
|
||||
"input.playerId": "steam-123",
|
||||
"input.maxRows": "100",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute declared sqlite query template: %v", err)
|
||||
}
|
||||
if queued.Status != "queued" || queued.Result["jobId"] == "" {
|
||||
t.Fatalf("expected queued query template job, got %+v", queued)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(queued.Result["jobId"])
|
||||
if err != nil {
|
||||
t.Fatalf("get query template job: %v", err)
|
||||
}
|
||||
if job.ExecutionInput.TimeoutSeconds != 20 {
|
||||
t.Fatalf("expected template timeout 20, got %+v", job.ExecutionInput)
|
||||
}
|
||||
if job.ExecutionInput.Inputs["templateKey"] != "players.by-id" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["maxRows"] != "25" {
|
||||
t.Fatalf("expected typed bounded query template inputs, got %#v", job.ExecutionInput.Inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceDeniesUndeclaredOrMismatchedSQLiteQueryTemplateBeforeJob(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
templateKey string
|
||||
declarationKey string
|
||||
targetKey string
|
||||
}{
|
||||
{name: "undeclared template", templateKey: "players.unknown", declarationKey: "scum-db-read", targetKey: "scum-db.player-lookup"},
|
||||
{name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "scum-db.player-lookup"},
|
||||
{name: "mismatched target", templateKey: "players.by-id", declarationKey: "scum-db-read", targetKey: "scum-db.other"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
|
||||
result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "query-template-denied-1",
|
||||
PluginID: plugin.ID,
|
||||
RouteKey: "remote",
|
||||
ServerInstanceID: instance.ID,
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
"declarationKey": test.declarationKey,
|
||||
"targetKey": test.targetKey,
|
||||
"idempotencyKey": "query-template-denied-1",
|
||||
"input.templateKey": test.templateKey,
|
||||
"input.playerId": "steam-123",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute denied sqlite query template: %v", err)
|
||||
}
|
||||
if result.Status != "denied" || result.Error == nil || result.Error.Code != "query_template_denied" {
|
||||
t.Fatalf("expected query template denial, got %+v", result)
|
||||
}
|
||||
jobs, err := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("list jobs after denial: %v", err)
|
||||
}
|
||||
if len(jobs) != 0 {
|
||||
t.Fatalf("query template denial created jobs: %+v", jobs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBridgeQueryTemplateRequiresPagePermissionAndRemoteAction(t *testing.T) {
|
||||
_, plugin, _, _, _ := createSQLiteQueryBridgeFixture(t)
|
||||
plugin.GameClientBridge.QueryTemplates[0].Permission = "server.game-client.read"
|
||||
|
||||
for index := range plugin.Pages {
|
||||
if plugin.Pages[index].Key == "remote" {
|
||||
plugin.Pages[index].Permissions = []string{"server.remote.access"}
|
||||
}
|
||||
}
|
||||
if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "permission") {
|
||||
t.Fatalf("expected query template permission denial, got %q", reason)
|
||||
}
|
||||
|
||||
for index := range plugin.Pages {
|
||||
if plugin.Pages[index].Key == "remote" {
|
||||
plugin.Pages[index].Permissions = []string{"server.remote.access"}
|
||||
plugin.Pages[index].BridgeActions = nil
|
||||
}
|
||||
}
|
||||
plugin.GameClientBridge.QueryTemplates[0].Permission = "server.remote.access"
|
||||
if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "remote access") {
|
||||
t.Fatalf("expected query template remote access denial, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsArbitrarySQLBridgeInputBeforeJob(t *testing.T) {
|
||||
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
|
||||
|
||||
result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "query-template-sql-rejected-1",
|
||||
PluginID: plugin.ID,
|
||||
RouteKey: "remote",
|
||||
ServerInstanceID: instance.ID,
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
"declarationKey": "scum-db-read",
|
||||
"targetKey": "scum-db.player-lookup",
|
||||
"idempotencyKey": "query-template-sql-rejected-1",
|
||||
"input.templateKey": "players.by-id",
|
||||
"input.sqlText": "SELECT * FROM users",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute arbitrary SQL bridge input: %v", err)
|
||||
}
|
||||
if result.Status != "error" || result.Error == nil || !strings.Contains(strings.ToLower(result.Error.Message), "unsafe") {
|
||||
t.Fatalf("expected arbitrary SQL input rejection, got %+v", result)
|
||||
}
|
||||
jobs, listErr := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if listErr != nil {
|
||||
t.Fatalf("list jobs after arbitrary SQL rejection: %v", listErr)
|
||||
}
|
||||
if len(jobs) != 0 {
|
||||
t.Fatalf("arbitrary SQL rejection created jobs: %+v", jobs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsDuplicateGamePluginManifest(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
registration := validPluginManifestRegistration()
|
||||
@@ -1212,6 +1353,80 @@ func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlug
|
||||
return plugin, endpoint
|
||||
}
|
||||
|
||||
func createSQLiteQueryBridgeFixture(t *testing.T) (*CoreService, domain.GamePlugin, domain.RunEndpoint, string, domain.ServerInstance) {
|
||||
t.Helper()
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
capability := domain.JobCapabilityRemoteRunDBSQLiteQuery
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability)
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
||||
plugin.Permissions.RemoteAccess = true
|
||||
plugin.BridgeActions = append(plugin.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest))
|
||||
plugin.Pages = append(plugin.Pages, domain.GamePluginPage{
|
||||
Key: "remote",
|
||||
Title: "Remote",
|
||||
Path: "/remote",
|
||||
Permissions: []string{"server.remote.access"},
|
||||
BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)},
|
||||
})
|
||||
plugin.RemoteAccess = domain.GamePluginRemoteAccess{
|
||||
Methods: []string{"run"},
|
||||
RunCapabilities: []string{capability},
|
||||
DatabaseEngines: []string{"sqlite"},
|
||||
}
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{
|
||||
Key: "scum-db-read",
|
||||
Kind: "sqlite",
|
||||
TargetKey: "scum-db.player-lookup",
|
||||
Capabilities: []string{capability},
|
||||
})
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{
|
||||
QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{
|
||||
{
|
||||
Key: "players.by-id",
|
||||
Title: "Player lookup",
|
||||
Permission: "server.remote.access",
|
||||
Engine: "sqlite",
|
||||
TransportKey: "scum-db-read",
|
||||
TargetKey: "scum-db.player-lookup",
|
||||
ParameterSchemaRef: "schemas/queries/players.by-id.parameters.schema.json",
|
||||
ResultSchemaRef: "schemas/queries/players.by-id.result.schema.json",
|
||||
MaxRows: 25,
|
||||
TimeoutSeconds: 20,
|
||||
},
|
||||
},
|
||||
Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100},
|
||||
Pages: []domain.GameClientBridgePageContract{
|
||||
{PageKey: "remote", QueryTemplateKeys: []string{"players.by-id"}},
|
||||
},
|
||||
}
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update sqlite query plugin fixture: %v", err)
|
||||
}
|
||||
endpoint.Capabilities = append(endpoint.Capabilities, capability)
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("update sqlite query endpoint fixture: %v", err)
|
||||
}
|
||||
session := createServiceUserAndLogin(t, svc, domain.User{
|
||||
ID: "user-query-owner",
|
||||
DisplayName: "Query Owner",
|
||||
Email: "query-owner@example.test",
|
||||
Roles: []string{"server-owner"},
|
||||
PasswordHash: "secret-password",
|
||||
})
|
||||
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
|
||||
ID: "server-query-template",
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Name: "Query Template Server",
|
||||
State: domain.ServerInstanceStateRunning,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlite query server fixture: %v", err)
|
||||
}
|
||||
return svc, plugin, endpoint, session, instance
|
||||
}
|
||||
|
||||
func createCompleteRuntimeBinding(t *testing.T, svc *CoreService, instance domain.ServerInstance, profileKey string) domain.RuntimeBinding {
|
||||
t.Helper()
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
@@ -1269,8 +1484,9 @@ func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
|
||||
BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)},
|
||||
},
|
||||
},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
||||
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user