Files
browser/platform/service/remote_adapters_test.go
T

212 lines
14 KiB
Go

package service
import (
"encoding/json"
"strings"
"testing"
"browser.local/platform/domain"
"browser.local/platform/dto"
)
func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
capability := domain.JobCapabilityRemoteRunDBSQLiteQuery
plugin.Permissions.RemoteAccess = true
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability)
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{capability}, DatabaseEngines: []string{"sqlite"}}
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "player-lookup", Kind: "sqlite", TargetKey: "scum-db.player-lookup", Capabilities: []string{capability}})
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatal(err)
}
endpoint.Capabilities = append(endpoint.Capabilities, capability)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
session := createServiceUserAndLogin(t, svc, domain.User{ID: "user-remote-owner", DisplayName: "Remote Owner", Email: "remote-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-remote-input", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Remote Input"})
if err != nil {
t.Fatal(err)
}
inputs := map[string]string{"playerId": "steam-123", "limit": "25"}
result, err := svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "player-lookup", TargetKey: "scum-db.player-lookup", Capability: capability, IdempotencyKey: "lookup-1", InputRef: "input://scum-db/player-lookup/lookup-1", Inputs: inputs})
if err != nil {
t.Fatalf("request remote adapter: %v", err)
}
inputs["playerId"] = "mutated"
job, err := svc.store.Jobs().Get(result.RequestID)
if err != nil {
t.Fatal(err)
}
if job.InputRef != "input://scum-db/player-lookup/lookup-1" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["limit"] != "25" {
t.Fatalf("typed inputs were not propagated: %#v", job)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, capability)
helloRequest.CapabilityReport.Fingerprint = "cap-remote-inputs"
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register Run hello: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{capability}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil {
t.Fatalf("claim remote adapter job: %v", err)
}
if !claim.HasJob || claim.Job == nil || claim.Job.JobID != job.ID || claim.Job.ExecutionInput.Inputs["playerId"] != "steam-123" || claim.Job.ExecutionInput.Inputs["limit"] != "25" {
t.Fatalf("typed inputs were not propagated to real Run claim: %#v", claim.Job)
}
response := dto.RunJobAssignmentFromDomain(*claim.Job)
payload, err := json.Marshal(response)
if err != nil {
t.Fatalf("marshal Run assignment response: %v", err)
}
var wire struct {
ExecutionInput struct {
Inputs map[string]string `json:"inputs"`
} `json:"executionInput"`
}
if err := json.Unmarshal(payload, &wire); err != nil {
t.Fatalf("unmarshal Run assignment response: %v", err)
}
if wire.ExecutionInput.Inputs["playerId"] != "steam-123" || wire.ExecutionInput.Inputs["limit"] != "25" {
t.Fatalf("typed inputs were not preserved in Run assignment JSON: %s", payload)
}
wire.ExecutionInput.Inputs["playerId"] = "wire-mutated"
if claim.Job.ExecutionInput.Inputs["playerId"] != "steam-123" {
t.Fatal("Run assignment DTO aliases domain remote inputs")
}
claim.Job.ExecutionInput.Inputs["playerId"] = "assignment-mutated"
stored, err := svc.store.Jobs().Get(result.RequestID)
if err != nil {
t.Fatalf("get claimed remote adapter job: %v", err)
}
if stored.ExecutionInput.Inputs["playerId"] != "steam-123" {
t.Fatal("real Run claim aliases persisted remote inputs")
}
}
func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.Permissions.RemoteAccess = true
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
)
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets, domain.RuntimeDataTarget{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}})
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
plugin.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatal(err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
session := createServiceUserAndLogin(t, svc, domain.User{ID: "user-scum-probe-owner", DisplayName: "SCUM Probe Owner", Email: "scum-probe-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-scum-probe", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Probe"})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
_, err = svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "scum-database", TargetKey: "scum-database", Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, IdempotencyKey: "direct-probe-denied", InputRef: "input://scum-schema-probe/direct-probe-denied"})
if err == nil || !strings.Contains(err.Error(), "scheduled by Platform") {
t.Fatalf("expected public probe request denial, got %v", err)
}
endpoint.Capabilities = withoutCapability(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
if _, _, err = svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-missing-run-capability"); err == nil || !strings.Contains(err.Error(), "does not expose") {
t.Fatalf("expected missing active Run capability, got %v", err)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil || len(jobs) != 0 {
t.Fatalf("missing probe executor must not create jobs: len=%d err=%v", len(jobs), err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatal(err)
}
probeRequest, queued, err := svc.RequestSCUMSchemaProbeForSession(session, instance.ID, "probe-current-schema")
if err != nil {
t.Fatalf("queue SCUM schema probe: %v", err)
}
job, err := svc.store.Jobs().Get(queued.RequestID)
if err != nil {
t.Fatalf("get probe job: %v", err)
}
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || job.TargetKey != "databases/scum-database" || job.InputRef != "" || job.ExecutionInput.RemoteAdapterKey != "" || job.ExecutionInput.RemoteAdapterKind != "" || len(job.ExecutionInput.Inputs) != 0 {
t.Fatalf("unexpected probe job envelope: %+v", job)
}
if job.ExecutionInput.SQLiteSchemaProbe == nil || job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || job.ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity != "scum-database" || job.ExecutionInput.SQLiteSchemaProbe.Bounds.MaxResultBytes != 524288 {
t.Fatalf("probe job did not include typed SQLite schema probe request: %+v", job.ExecutionInput.SQLiteSchemaProbe)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
helloRequest.CapabilityReport.Fingerprint = "cap-scum-probe"
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register Run hello: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil {
t.Fatalf("claim probe job: %v", err)
}
if !claim.HasJob || claim.Job == nil || claim.Job.TargetKey != "databases/scum-database" || claim.Job.InputRef != "" || claim.Job.FencingToken == 0 || claim.Job.MaxAttempts != 1 || claim.Job.ExecutionInput.RemoteAdapterKey != "" || len(claim.Job.ExecutionInput.Inputs) != 0 {
t.Fatalf("claimed probe job lost fenced typed envelope: %+v", claim.Job)
}
if claim.Job.ExecutionInput.SQLiteSchemaProbe == nil || claim.Job.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || claim.Job.ExecutionInput.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
t.Fatalf("claimed probe job lost typed schema probe request: %+v", claim.Job.ExecutionInput.SQLiteSchemaProbe)
}
assignmentBody := dto.RunJobAssignmentFromDomain(*claim.Job)
payload, err := json.Marshal(assignmentBody)
if err != nil {
t.Fatalf("marshal probe Run assignment: %v", err)
}
var runWire struct {
ExecutionInput struct {
SQLiteSchemaProbe struct {
RequestID string `json:"requestId"`
JobID string `json:"jobId"`
Bounds *dto.SCUMSchemaProbeBoundsDTO `json:"bounds"`
Limits dto.SCUMSchemaProbeBoundsDTO `json:"limits"`
} `json:"sqliteSchemaProbe"`
} `json:"executionInput"`
}
if err := json.Unmarshal(payload, &runWire); err != nil {
t.Fatalf("unmarshal probe Run assignment: %v", err)
}
if runWire.ExecutionInput.SQLiteSchemaProbe.RequestID != probeRequest.RequestID || runWire.ExecutionInput.SQLiteSchemaProbe.JobID != "" || runWire.ExecutionInput.SQLiteSchemaProbe.Bounds != nil || runWire.ExecutionInput.SQLiteSchemaProbe.Limits.MaxResultBytes != probeRequest.Bounds.MaxResultBytes {
t.Fatalf("probe Run assignment JSON does not match Run contract: %s", payload)
}
badBinding := probeRequest.Binding
badBinding.RunBindingID = "runtime-binding-other"
badProbe := domain.SCUMSchemaProbeResult{RequestID: probeRequest.RequestID, JobID: probeRequest.JobID, Binding: badBinding, Status: domain.SCUMCapabilityEvidenceCompatible, SchemaFingerprint: "sha256:" + strings.Repeat("a", 64), ObservedAt: fixedTime, ResultDigest: "sha256:" + strings.Repeat("b", 64), Limits: probeRequest.Bounds}
_, err = svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &badProbe, AuditSummary: "redacted schema probe"}})
if err == nil || !strings.Contains(err.Error(), "binding identity") {
t.Fatalf("expected binding mismatch rejection, got %v", err)
}
goodProbe := badProbe
goodProbe.Binding = probeRequest.Binding
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "probe complete"}, Message: "probe complete", ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &goodProbe, AuditSummary: "redacted schema probe"}}); err != nil {
t.Fatalf("complete fenced probe job: %v", err)
}
stored, err := svc.store.Jobs().Get(job.ID)
if err != nil {
t.Fatalf("get completed probe job: %v", err)
}
if stored.ExecutionResult.SQLiteSchemaProbe == nil || stored.ExecutionResult.SQLiteSchemaProbe.ResultDigest != goodProbe.ResultDigest || stored.ExecutionResult.SQLiteSchemaProbe.Binding.RunBindingID != probeRequest.Binding.RunBindingID {
t.Fatalf("typed probe result was not persisted safely: %+v", stored.ExecutionResult.SQLiteSchemaProbe)
}
}