Files
browser/platform/service/resources_test.go
T

2067 lines
87 KiB
Go

package service
import (
"encoding/base64"
"errors"
"strings"
"testing"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
"browser.local/platform/validator"
)
var fixedTime = time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)
func TestCoreServiceCreateListGetWorkflows(t *testing.T) {
svc := newTestCoreService()
user, err := svc.CreateUser(domain.User{
ID: "user-1",
DisplayName: "Operator",
Roles: []string{"admin"},
})
if err != nil {
t.Fatalf("create user: %v", err)
}
if user.Status != domain.UserStatusActive || !user.CreatedAt.Equal(fixedTime) {
t.Fatalf("expected user defaults, got %+v", user)
}
if _, err := svc.GetUser(user.ID); err != nil {
t.Fatalf("get user: %v", err)
}
users, err := svc.ListUsers(domain.UserFilter{Status: domain.UserStatusActive})
if err != nil || len(users) != 1 {
t.Fatalf("list users: len=%d err=%v", len(users), err)
}
generatedUser, err := svc.CreateUser(domain.User{
DisplayName: "Generated User",
Email: "generated@example.test",
Roles: []string{"server-admin"},
})
if err != nil {
t.Fatalf("create generated user: %v", err)
}
if generatedUser.ID != "user-generated-example-test" {
t.Fatalf("expected generated user id from email, got %q", generatedUser.ID)
}
provider, err := svc.CreateAIProvider(validProvider())
if err != nil {
t.Fatalf("create provider: %v", err)
}
if provider.APIKeyRef != "secret://providers/openai" {
t.Fatalf("expected provider key reference only, got %+v", provider)
}
if _, err := svc.GetAIProvider(provider.ID); err != nil {
t.Fatalf("get provider: %v", err)
}
providers, err := svc.ListAIProviders(domain.AIProviderFilter{Status: domain.AIProviderStatusActive})
if err != nil || len(providers) != 1 {
t.Fatalf("list providers: len=%d err=%v", len(providers), err)
}
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
if _, err := svc.GetGamePlugin(plugin.ID); err != nil {
t.Fatalf("get plugin: %v", err)
}
plugins, err := svc.ListGamePlugins(domain.GamePluginFilter{Status: domain.GamePluginStatusInstalled})
if err != nil || len(plugins) != 1 {
t.Fatalf("list plugins: len=%d err=%v", len(plugins), err)
}
if _, err := svc.GetRunEndpoint(endpoint.ID); err != nil {
t.Fatalf("get endpoint: %v", err)
}
endpoints, err := svc.ListRunEndpoints(domain.RunEndpointFilter{Status: domain.RunEndpointStatusOnline})
if err != nil || len(endpoints) != 1 {
t.Fatalf("list endpoints: len=%d err=%v", len(endpoints), err)
}
instance, err := svc.CreateServerInstance(domain.ServerInstance{
ID: "server-1",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "SCUM #1",
})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
if instance.PluginVersion != plugin.Version || instance.ConfigVersion != 1 || instance.State != domain.ServerInstanceStateDraft {
t.Fatalf("expected server defaults, got %+v", instance)
}
if _, err := svc.GetServerInstance(instance.ID); err != nil {
t.Fatalf("get server instance: %v", err)
}
instances, err := svc.ListServerInstances(domain.ServerInstanceFilter{PluginID: plugin.ID})
if err != nil || len(instances) != 1 {
t.Fatalf("list server instances: len=%d err=%v", len(instances), err)
}
job, err := svc.CreateJob(domain.Job{
ID: "job-1",
ServerInstanceID: instance.ID,
RunEndpointID: endpoint.ID,
Capability: "process.start",
IdempotencyKey: "idem-start",
})
if err != nil {
t.Fatalf("create job: %v", err)
}
if job.State != domain.JobStateQueued || !job.CreatedAt.Equal(fixedTime) {
t.Fatalf("expected job defaults, got %+v", job)
}
if _, err := svc.GetJob(job.ID); err != nil {
t.Fatalf("get job: %v", err)
}
streams, err := svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
if err != nil || len(streams) != 2 {
t.Fatalf("expected default job stdout/stderr streams, len=%d err=%v streams=%+v", len(streams), err, streams)
}
jobs, err := svc.ListJobs(domain.JobFilter{RunEndpointID: endpoint.ID})
if err != nil || len(jobs) != 1 {
t.Fatalf("list jobs: len=%d err=%v", len(jobs), err)
}
artifact, err := svc.CreateArtifact(domain.Artifact{
ID: "artifact-1",
OwnerKind: domain.ArtifactOwnerKindJob,
OwnerID: job.ID,
SizeBytes: 128,
Checksum: "sha256:abc",
})
if err != nil {
t.Fatalf("create artifact: %v", err)
}
if artifact.State != domain.ArtifactStateUploading {
t.Fatalf("expected artifact default state, got %+v", artifact)
}
if _, err := svc.GetArtifact(artifact.ID); err != nil {
t.Fatalf("get artifact: %v", err)
}
artifacts, err := svc.ListArtifacts(domain.ArtifactFilter{OwnerID: job.ID})
if err != nil || len(artifacts) != 1 {
t.Fatalf("list artifacts: len=%d err=%v", len(artifacts), err)
}
stream, err := svc.CreateLogStream(domain.LogStream{
ID: "log-1",
ServerInstanceID: instance.ID,
Source: domain.LogStreamSourceProcess,
StreamKey: "stdout",
StorageBackend: domain.LogStorageBackendLocalSegments,
RetentionPolicy: "default",
})
if err != nil {
t.Fatalf("create log stream: %v", err)
}
if _, err := svc.GetLogStream(stream.ID); err != nil {
t.Fatalf("get log stream: %v", err)
}
streams, err = svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
if err != nil || len(streams) != 3 {
t.Fatalf("list log streams: len=%d err=%v", len(streams), err)
}
}
func TestCoreServiceRejectsServerCreationOnStaleRunEndpoint(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter + time.Second) }
_, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-stale-run", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Stale Run Server"})
if err == nil || !strings.Contains(err.Error(), "get run endpoint dependency") || !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected stale Run endpoint to be rejected before binding, got %v", err)
}
}
func TestCoreServiceCreateRemoteProgramJobCreatesManagementLogStreams(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProgram)
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin capabilities: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProgram)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update endpoint capabilities: %v", err)
}
instance, err := svc.CreateServerInstance(domain.ServerInstance{
ID: "server-terminal",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "SCUM Terminal",
})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
job, err := svc.CreateJob(domain.Job{
ID: "job-terminal",
ServerInstanceID: instance.ID,
RunEndpointID: endpoint.ID,
Capability: domain.JobCapabilityRemoteRunProgram,
TargetKey: "protected-program",
InputRef: "input://protected-program/job-terminal",
IdempotencyKey: "idem-terminal",
})
if err != nil {
t.Fatalf("create remote program job: %v", err)
}
streams, err := svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID})
if err != nil {
t.Fatalf("list log streams: %v", err)
}
if len(streams) != 4 {
t.Fatalf("expected stdout/stderr plus management program streams, got %+v", streams)
}
want := map[string]domain.LogStreamSource{
"stdout": domain.LogStreamSourceProcess,
"stderr": domain.LogStreamSourceProcess,
"management-program.stdout": domain.LogStreamSourceManagementProgram,
"management-program.stderr": domain.LogStreamSourceManagementProgram,
}
for _, stream := range streams {
source, ok := want[stream.StreamKey]
if !ok {
t.Fatalf("unexpected stream key: %+v", stream)
}
if stream.Source != source || stream.ID != jobLogStreamID(job.ID, stream.StreamKey) {
t.Fatalf("unexpected stream metadata: %+v", stream)
}
delete(want, stream.StreamKey)
}
if len(want) != 0 {
t.Fatalf("missing streams: %+v", want)
}
}
func TestCoreServiceStartupRecoversLegacyJobLogStreams(t *testing.T) {
store := repo.NewMemoryStore()
seed := newCoreService(store, func() time.Time { return fixedTime })
plugin, endpoint := createPluginAndRunEndpoint(t, seed)
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProgram)
if err := seed.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin capabilities: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProgram)
if err := seed.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update endpoint capabilities: %v", err)
}
if _, err := seed.CreateServerInstance(domain.ServerInstance{ID: "legacy-terminal-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Legacy Terminal"}); err != nil {
t.Fatalf("create server instance: %v", err)
}
if err := store.Jobs().Create(domain.Job{
ID: "legacy-terminal-job",
ServerInstanceID: "legacy-terminal-server",
RunEndpointID: endpoint.ID,
Capability: domain.JobCapabilityRemoteRunProgram,
TargetKey: "protected-program",
InputRef: "input://protected-program/legacy-terminal-job",
IdempotencyKey: "legacy-terminal",
State: domain.JobStateQueued,
CreatedAt: fixedTime,
UpdatedAt: fixedTime,
}); err != nil {
t.Fatalf("seed legacy job: %v", err)
}
before, err := seed.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: "legacy-terminal-server"})
if err != nil || len(before) != 0 {
t.Fatalf("expected no seeded streams before recovery, len=%d err=%v streams=%+v", len(before), err, before)
}
recovered, err := NewCoreServiceWithDurableStores(store, NewMemoryLogBodyStore(), NewMemoryArtifactBodyStore())
if err != nil {
t.Fatalf("recover durable service: %v", err)
}
after, err := recovered.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: "legacy-terminal-server"})
if err != nil || len(after) != 4 {
t.Fatalf("expected recovered job log streams, len=%d err=%v streams=%+v", len(after), err, after)
}
}
func TestCoreServiceRejectsInvalidServerDependencies(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
disabledPlugin := plugin
disabledPlugin.ID = "server.disabled"
disabledPlugin.Status = domain.GamePluginStatusDisabled
if _, err := svc.CreateGamePlugin(disabledPlugin); err != nil {
t.Fatalf("create disabled plugin fixture: %v", err)
}
_, err := svc.CreateServerInstance(domain.ServerInstance{
ID: "server-disabled",
PluginID: disabledPlugin.ID,
RunEndpointID: endpoint.ID,
Name: "Disabled Plugin Server",
})
if err == nil || !strings.Contains(err.Error(), "plugin must be installed") {
t.Fatalf("expected disabled plugin rejection, got %v", err)
}
weakEndpoint := endpoint
weakEndpoint.ID = "run-weak"
weakEndpoint.Capabilities = []string{"process.start"}
if _, err := svc.CreateRunEndpoint(weakEndpoint); err != nil {
t.Fatalf("create weak endpoint fixture: %v", err)
}
_, err = svc.CreateServerInstance(domain.ServerInstance{
ID: "server-weak",
PluginID: plugin.ID,
RunEndpointID: weakEndpoint.ID,
Name: "Weak Endpoint Server",
})
if err == nil || !strings.Contains(err.Error(), "logs.read") {
t.Fatalf("expected missing capability rejection, got %v", err)
}
}
func TestCoreServiceRejectsRawAIProviderSecret(t *testing.T) {
svc := newTestCoreService()
provider := validProvider()
provider.APIKeyRef = "sk-raw-secret"
_, err := svc.CreateAIProvider(provider)
if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") {
t.Fatalf("expected raw secret rejection, got %v", err)
}
}
func TestCoreServiceAuthenticatesActiveUsers(t *testing.T) {
svc := newTestCoreService()
created, err := svc.CreateUser(domain.User{
ID: "user-auth",
DisplayName: "Auth User",
Email: "auth@example.test",
Roles: []string{"platform-admin"},
PasswordHash: "secret-password",
})
if err != nil {
t.Fatalf("create auth user: %v", err)
}
if created.PasswordHash == "secret-password" || created.PasswordHash == "" {
t.Fatalf("expected password to be hashed, got %q", created.PasswordHash)
}
session, err := svc.LoginUser(domain.UserLogin{Account: "auth@example.test", Password: "secret-password"})
if err != nil {
t.Fatalf("login: %v", err)
}
if session.SessionID == "" || session.Status != "authenticated" || session.User.ID != created.ID {
t.Fatalf("unexpected auth session: %+v", session)
}
current, err := svc.GetCurrentUser(session.SessionID)
if err != nil {
t.Fatalf("current user: %v", err)
}
if current.ID != created.ID {
t.Fatalf("expected current user %q, got %+v", created.ID, current)
}
if err := svc.LogoutUser(session.SessionID); err != nil {
t.Fatalf("logout: %v", err)
}
if _, err := svc.GetCurrentUser(session.SessionID); !errors.Is(err, ErrUnauthorized) {
t.Fatalf("expected logged out session to be unauthorized, got %v", err)
}
}
func TestCoreServiceFirstRegistrationBootstrapsPlatformAdmin(t *testing.T) {
svc := newTestCoreService()
session, err := svc.RegisterUser(domain.UserRegistration{
DisplayName: "Bootstrap Admin",
Email: "bootstrap@example.test",
Password: "secret-password",
Profile: domain.UserProfile{Phone: "13800000000", QQ: "10001"},
})
if err != nil {
t.Fatalf("register: %v", err)
}
if session.Status != "authenticated" || session.SessionID == "" {
t.Fatalf("expected authenticated bootstrap registration, got %+v", session)
}
if session.User.Status != domain.UserStatusActive || len(session.User.Roles) != 1 || session.User.Roles[0] != "platform-admin" {
t.Fatalf("expected active platform admin user, got %+v", session.User)
}
}
func TestCoreServiceRegistersLaterUsersAsPendingLowPrivilege(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.CreateUser(domain.User{ID: "user-existing", DisplayName: "Existing Admin", Roles: []string{"platform-admin"}}); err != nil {
t.Fatalf("create existing user: %v", err)
}
session, err := svc.RegisterUser(domain.UserRegistration{
DisplayName: "Pending Player",
Email: "pending@example.test",
Password: "secret-password",
Profile: domain.UserProfile{Phone: "13800000000", QQ: "10001"},
})
if err != nil {
t.Fatalf("register: %v", err)
}
if session.Status != "pending" || session.SessionID != "" {
t.Fatalf("expected pending registration without session token, got %+v", session)
}
if session.User.Status != domain.UserStatusPending || len(session.User.Roles) != 1 || session.User.Roles[0] != "server-admin" {
t.Fatalf("expected low-privilege pending user, got %+v", session.User)
}
if _, err := svc.LoginUser(domain.UserLogin{Account: "pending@example.test", Password: "secret-password"}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected pending login to be forbidden, got %v", err)
}
}
func TestCoreServiceScopesServerAccessAndMembership(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-owner",
DisplayName: "Server Owner",
Email: "owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
helperSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-helper",
DisplayName: "Server Helper",
Email: "helper@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
})
adminSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-platform",
DisplayName: "Platform Admin",
Email: "platform@example.test",
Roles: []string{"platform-admin"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-owned",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Owned Server",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create owned server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
if instance.OwnerUserID != "user-owner" {
t.Fatalf("expected owner to be recorded, got %+v", instance)
}
ownerServers, err := svc.ListServerInstancesForSession(ownerSession, domain.ServerInstanceFilter{})
if err != nil || len(ownerServers) != 1 {
t.Fatalf("expected owner server visibility, len=%d err=%v", len(ownerServers), err)
}
helperServers, err := svc.ListServerInstancesForSession(helperSession, domain.ServerInstanceFilter{})
if err != nil || len(helperServers) != 0 {
t.Fatalf("expected helper to see no servers before invite, len=%d err=%v", len(helperServers), err)
}
adminServers, err := svc.ListServerInstancesForSession(adminSession, domain.ServerInstanceFilter{})
if err != nil || len(adminServers) != 1 {
t.Fatalf("expected platform admin to see all servers, len=%d err=%v", len(adminServers), err)
}
candidates, err := svc.ListServerAdministratorCandidates(ownerSession, instance.ID)
if err != nil {
t.Fatalf("list candidates: %v", err)
}
if len(candidates) != 1 || candidates[0].ID != "user-helper" {
t.Fatalf("expected only helper candidate, got %+v", candidates)
}
if _, err := svc.AddServerAdministrator(helperSession, instance.ID, "user-owner"); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected non-owner add to be forbidden, got %v", err)
}
if _, err := svc.AddServerAdministrator(ownerSession, instance.ID, "user-platform"); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected platform admin invite to be forbidden, got %v", err)
}
updated, err := svc.AddServerAdministrator(ownerSession, instance.ID, "user-helper")
if err != nil {
t.Fatalf("add helper admin: %v", err)
}
if len(updated.AdminUserIDs) != 1 || updated.AdminUserIDs[0] != "user-helper" {
t.Fatalf("expected helper membership, got %+v", updated)
}
helperServers, err = svc.ListServerInstancesForSession(helperSession, domain.ServerInstanceFilter{})
if err != nil || len(helperServers) != 1 {
t.Fatalf("expected helper to see invited server, len=%d err=%v", len(helperServers), err)
}
if _, err := svc.StartServerInstanceForSession(helperSession, domain.ServerLifecycleCommand{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion,
IdempotencyKey: "idem-helper-start",
}); err != nil {
t.Fatalf("expected helper lifecycle access: %v", err)
}
removed, err := svc.RemoveServerAdministrator(ownerSession, instance.ID, "user-helper")
if err != nil {
t.Fatalf("remove helper admin: %v", err)
}
if len(removed.AdminUserIDs) != 0 {
t.Fatalf("expected helper membership removed, got %+v", removed)
}
if _, err := svc.GetServerInstanceForSession(helperSession, instance.ID); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected helper access to be revoked, got %v", err)
}
}
func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-owner",
DisplayName: "Delete Owner",
Email: "delete-owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
adminSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-admin",
DisplayName: "Delete Admin",
Email: "delete-admin@example.test",
Roles: []string{"platform-admin"},
PasswordHash: "secret-password",
})
otherSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-other",
DisplayName: "Delete Other",
Email: "delete-other@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
})
ownerInstance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-owner",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Owner Server",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create owner instance: %v", err)
}
adminTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-admin",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Admin Target",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create admin target: %v", err)
}
runningTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-running",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Running Target",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create running target: %v", err)
}
runningTarget.State = domain.ServerInstanceStateRunning
if err := svc.store.ServerInstances().Update(runningTarget); err != nil {
t.Fatalf("set running target state: %v", err)
}
installingTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-installing",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Installing Target",
State: domain.ServerInstanceStateInstalling,
})
if err != nil {
t.Fatalf("create installing target: %v", err)
}
draftTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-draft",
PluginID: plugin.ID,
Name: "Delete Draft Target",
State: domain.ServerInstanceStateDraft,
})
if err != nil {
t.Fatalf("create draft target: %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(otherSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected non-owner delete to be forbidden, got %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{}); err == nil {
t.Fatalf("expected missing password to fail")
} else {
var validationErr validator.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected missing password to be validation error, got %v", err)
}
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "wrong-password"}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected wrong password to be forbidden, got %v", err)
}
deletedOwner, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"})
if err != nil {
t.Fatalf("delete owner instance: %v", err)
}
if deletedOwner.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted owner state, got %+v", deletedOwner)
}
deletedAdmin, err := svc.DeleteServerInstanceForSession(adminSession, adminTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"})
if err != nil {
t.Fatalf("delete admin target: %v", err)
}
if deletedAdmin.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted admin state, got %+v", deletedAdmin)
}
deletedDraft, err := svc.DeleteServerInstanceForSession(ownerSession, draftTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"})
if err != nil {
t.Fatalf("delete draft target without run endpoint: %v", err)
}
if deletedDraft.State != domain.ServerInstanceStateDeleted || deletedDraft.RunEndpointID != "" {
t.Fatalf("expected deleted draft target without run endpoint, got %+v", deletedDraft)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"}); err == nil {
t.Fatalf("expected running instance delete to fail")
} else {
var validationErr validator.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected running delete to be validation error, got %v", err)
}
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: "not enough"}); err == nil {
t.Fatalf("expected installing force delete without exact confirmation to fail")
}
forcedRunning, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation})
if err != nil {
t.Fatalf("force delete running target: %v", err)
}
if forcedRunning.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected forced running target deleted, got %+v", forcedRunning)
}
forcedInstalling, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation})
if err != nil {
t.Fatalf("force delete installing target: %v", err)
}
if forcedInstalling.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected forced installing target deleted, got %+v", forcedInstalling)
}
}
func TestCoreServiceMetricsAndConfigReadAreRoleScoped(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-owner-metrics",
DisplayName: "Metrics Owner",
Email: "owner-metrics@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
otherSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-other-metrics",
DisplayName: "Metrics Other",
Email: "other-metrics@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-metrics",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Metrics Server",
State: domain.ServerInstanceStateRunning,
})
if err != nil {
t.Fatalf("create server: %v", err)
}
if _, err := svc.CreateJob(domain.Job{ID: "job-metrics", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: "process.start", IdempotencyKey: "idem-metrics"}); err != nil {
t.Fatalf("create job: %v", err)
}
usage, err := svc.GetPlatformResourceUsage()
if err != nil {
t.Fatalf("get platform usage: %v", err)
}
if usage.Source != "platform-derived" || usage.CollectedAt.IsZero() || usage.CPUPercent < 0 || usage.CPUPercent > 100 {
t.Fatalf("unexpected platform usage: %+v", usage)
}
ownerMetrics, err := svc.ListServerMetricsForSession(ownerSession)
if err != nil {
t.Fatalf("list owner metrics: %v", err)
}
if len(ownerMetrics) != 1 || ownerMetrics[0].ServerInstanceID != instance.ID || ownerMetrics[0].Online || ownerMetrics[0].CPUPercent != nil || ownerMetrics[0].Source != "run-metrics-pending" {
t.Fatalf("expected pending metrics without fabricated resource values, got %+v", ownerMetrics)
}
runHello := validRunControlHello()
runHello.RunEndpointID = endpoint.ID
registered, err := svc.RegisterRunHello(runHello)
if err != nil {
t.Fatalf("register run: %v", err)
}
oldCPU := 28.0
latestCPU := 64.0
latestMemory := 52.0
latestDisk := 31.0
players := 11
maxPlayers := 40
tps := 19.7
latency := 48.0
if _, err := svc.IngestMetricBatch(domain.MetricBatchIngest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Samples: []domain.MetricSample{
{ServerInstanceID: instance.ID, Online: true, CPUPercent: &oldCPU, Source: "run", CollectedAt: fixedTime.Add(-time.Minute)},
{ServerInstanceID: instance.ID, Online: true, PlayerCount: &players, MaxPlayers: &maxPlayers, TPS: &tps, LatencyMS: &latency, CPUPercent: &latestCPU, MemoryPercent: &latestMemory, DiskPercent: &latestDisk, Source: "run", CollectedAt: fixedTime.Add(time.Minute)},
}}); err != nil {
t.Fatalf("ingest metrics: %v", err)
}
ownerMetrics, err = svc.ListServerMetricsForSession(ownerSession)
if err != nil {
t.Fatalf("list owner metrics after ingest: %v", err)
}
if len(ownerMetrics) != 1 || ownerMetrics[0].CPUPercent == nil || *ownerMetrics[0].CPUPercent != latestCPU || ownerMetrics[0].PlayerCount == nil || *ownerMetrics[0].PlayerCount != players || ownerMetrics[0].CollectedAt != fixedTime.Add(time.Minute) {
t.Fatalf("expected latest persisted sample metrics, got %+v", ownerMetrics)
}
otherMetrics, err := svc.ListServerMetricsForSession(otherSession)
if err != nil {
t.Fatalf("list other metrics: %v", err)
}
if len(otherMetrics) != 0 {
t.Fatalf("expected other user to see no metrics, got %+v", otherMetrics)
}
config, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
if config.ServerInstanceID != instance.ID || config.ConfigVersion != instance.ConfigVersion || !strings.Contains(config.Content, "server.name=Metrics Server") {
t.Fatalf("unexpected config: %+v", config)
}
for _, forbidden := range []string{"/Users/", "unix://", "Bearer ", "sk-", "password="} {
if strings.Contains(config.Content, forbidden) {
t.Fatalf("config content exposed forbidden fragment %q: %s", forbidden, config.Content)
}
}
if _, err := svc.GetServerConfigForSession(otherSession, instance.ID); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected other config access to be forbidden, got %v", err)
}
}
func TestCoreServiceMergesRecentPartialMetricSamples(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-partial-metrics", DisplayName: "Partial Metrics Owner", Email: "partial-metrics@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-partial-metrics", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Partial Metrics", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
runHello := validRunControlHello()
runHello.RunEndpointID = endpoint.ID
registered, err := svc.RegisterRunHello(runHello)
if err != nil {
t.Fatalf("register run: %v", err)
}
cpu := 37.0
memory := 49.0
players := 8
disk := 78.0
staleMemory := 12.0
if _, err := svc.IngestMetricBatch(domain.MetricBatchIngest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Samples: []domain.MetricSample{
{ServerInstanceID: instance.ID, Online: true, CPUPercent: &cpu, MemoryPercent: &memory, PlayerCount: &players, Source: "run", CollectedAt: fixedTime.Add(-15 * time.Second)},
{ServerInstanceID: instance.ID, Online: true, MemoryPercent: &staleMemory, Source: "run", CollectedAt: fixedTime.Add(-3 * time.Minute)},
{ServerInstanceID: instance.ID, Online: true, DiskPercent: &disk, Source: "run", CollectedAt: fixedTime},
}}); err != nil {
t.Fatalf("ingest partial metrics: %v", err)
}
metrics, err := svc.ListServerMetricsForSession(ownerSession)
if err != nil {
t.Fatalf("list merged metrics: %v", err)
}
if len(metrics) != 1 || metrics[0].CPUPercent == nil || *metrics[0].CPUPercent != cpu || metrics[0].MemoryPercent == nil || *metrics[0].MemoryPercent != memory || metrics[0].PlayerCount == nil || *metrics[0].PlayerCount != players || metrics[0].DiskPercent == nil || *metrics[0].DiskPercent != disk || metrics[0].CollectedAt != fixedTime {
t.Fatalf("expected recent partial metrics to merge, got %+v", metrics)
}
}
func TestCoreServiceConfigWriteAndFileDispatchAreScoped(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-owner-config",
DisplayName: "Config Owner",
Email: "owner-config@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
otherSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-other-config",
DisplayName: "Config Other",
Email: "other-config@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-config",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Config Server",
State: domain.ServerInstanceStateRunning,
})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
current, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
if err != nil {
t.Fatalf("get config: %v", err)
}
proposed := strings.Replace(current.Content, "state=running", "state=running\nmotd=Approved", 1)
preview, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion,
Key: current.Key,
ProposedContent: proposed,
})
if err != nil {
t.Fatalf("preview config write: %v", err)
}
if !preview.HasChanges || preview.Source != "platform-review" || preview.ProposedContent != proposed {
t.Fatalf("unexpected preview: %+v", preview)
}
jobs, err := svc.ListJobs(domain.JobFilter{})
if err != nil || len(jobs) != 0 {
t.Fatalf("preview must not create jobs, jobs=%+v err=%v", jobs, err)
}
dispatch, err := svc.ApproveServerConfigWriteForSession(ownerSession, domain.ServerConfigWriteApproval{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion,
Key: current.Key,
ProposedContent: proposed,
IdempotencyKey: "idem-config-approve",
})
if err != nil {
t.Fatalf("approve config write: %v", err)
}
if dispatch.Status != "queued" || dispatch.Job.Capability != domain.JobCapabilityConfigWrite || dispatch.Job.TargetKey != current.Key || !strings.HasPrefix(dispatch.Job.InputRef, "input://server-config/") {
t.Fatalf("unexpected config dispatch: %+v", dispatch)
}
if _, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion + 1,
Key: current.Key,
ProposedContent: proposed,
}); err == nil || !strings.Contains(err.Error(), "expectedConfigVersion") {
t.Fatalf("expected stale config version rejection, got %v", err)
}
if _, err := svc.ApproveServerConfigWriteForSession(otherSession, domain.ServerConfigWriteApproval{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion,
Key: current.Key,
ProposedContent: proposed,
IdempotencyKey: "idem-config-forbidden",
}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected unauthorized approval rejection, got %v", err)
}
if _, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{
ServerInstanceID: instance.ID,
ExpectedConfigVersion: instance.ConfigVersion,
Key: "/Users/tasia/secret.properties",
ProposedContent: proposed,
}); err == nil || !strings.Contains(err.Error(), "key") {
t.Fatalf("expected unsafe key rejection, got %v", err)
}
if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
Operation: domain.FileOperationRead,
Key: "../secrets.env",
IdempotencyKey: "idem-file-unsafe",
}); err == nil || !strings.Contains(err.Error(), "key") {
t.Fatalf("expected unsafe file key rejection, got %v", err)
}
fileDispatch, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
Operation: domain.FileOperationRead,
Key: "logs/latest.log",
IdempotencyKey: "idem-file-read",
})
if err != nil {
t.Fatalf("dispatch file read: %v", err)
}
if fileDispatch.Job.Capability != domain.JobCapabilityFilesRead || fileDispatch.Job.TargetKey != "logs/latest.log" {
t.Fatalf("unexpected file dispatch: %+v", fileDispatch)
}
jobs, err = svc.ListJobs(domain.JobFilter{})
if err != nil || len(jobs) != 2 {
t.Fatalf("expected only approved config and file jobs, jobs=%+v err=%v", jobs, err)
}
}
func TestPluginFileWorkspaceDoesNotConstrainServerFileDispatch(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.FileWorkspace = scumTestFileWorkspace()
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin workspace: %v", err)
}
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-owner-file-workspace",
DisplayName: "File Workspace Owner",
Email: "file-workspace-owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-file-workspace",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "File Workspace Server",
State: domain.ServerInstanceStateRunning,
})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
allowed, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
Operation: domain.FileOperationRead,
Key: "scum-server-settings",
IdempotencyKey: "idem-file-workspace-read",
})
if err != nil {
t.Fatalf("dispatch declared file read: %v", err)
}
if allowed.Job.TargetKey != "scum-server-settings" || allowed.Job.Capability != domain.JobCapabilityFilesRead {
t.Fatalf("unexpected declared file dispatch: %+v", allowed)
}
unknown, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
Operation: domain.FileOperationRead,
Key: "logs/latest.log",
IdempotencyKey: "idem-file-workspace-unknown",
})
if err != nil || unknown.Job.TargetKey != "logs/latest.log" || unknown.Job.Capability != domain.JobCapabilityFilesRead {
t.Fatalf("expected undeclared file read to queue, dispatch=%+v err=%v", unknown, err)
}
written, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
Operation: domain.FileOperationWrite,
Key: "scum-chat-log",
InputRef: "input://file-workspace/update",
Content: "line",
IdempotencyKey: "idem-file-workspace-log-write",
})
if err != nil || written.Job.TargetKey != "scum-chat-log" || written.Job.Capability != domain.JobCapabilityFilesWrite {
t.Fatalf("expected declared log write to queue, dispatch=%+v err=%v", written, err)
}
}
func TestDeclaredFileReadSnapshotProjectionStatesAndRedaction(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.FileWorkspace = scumTestFileWorkspace()
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin workspace: %v", err)
}
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-owner", DisplayName: "File Snapshot Owner", Email: "file-snapshot-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
otherSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-other", DisplayName: "File Snapshot Other", Email: "file-snapshot-other@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-snapshot", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Snapshot Server", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
snapshot, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.State != "not-read" {
t.Fatalf("expected not-read without jobs, snapshot=%+v err=%v", snapshot, err)
}
queued := createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-queued", domain.JobStateQueued, 1, "")
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.State != "pending" || snapshot.JobID != queued.ID {
t.Fatalf("expected pending queued job, snapshot=%+v err=%v", snapshot, err)
}
queued.State = domain.JobStateFailed
queued.UpdatedAt = fixedTime.Add(2 * time.Minute)
queued.TerminalAt = fixedTime.Add(2 * time.Minute)
if err := svc.store.Jobs().Update(queued); err != nil {
t.Fatalf("update failed read job: %v", err)
}
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-cancelled", domain.JobStateCancelled, 3, "")
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.State != "not-read" {
t.Fatalf("failed/cancelled reads must not mask not-read, snapshot=%+v err=%v", snapshot, err)
}
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-old", domain.JobStateSucceeded, 4, "ServerName=Old\nRconPassword=secret\n")
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-failed-newer", domain.JobStateFailed, 5, "")
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.State != "ready" || snapshot.JobID != "job-file-snapshot-success-old" || !strings.Contains(snapshot.Content, "RconPassword=<redacted>") {
t.Fatalf("expected older successful redacted result, snapshot=%+v err=%v", snapshot, err)
}
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-new", domain.JobStateSucceeded, 6, "ServerName=New\nApiToken=secret\n")
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.JobID != "job-file-snapshot-success-new" || !strings.Contains(snapshot.Content, "ServerName=New") || strings.Contains(snapshot.Content, "secret") {
t.Fatalf("expected newest successful redacted result, snapshot=%+v err=%v", snapshot, err)
}
unknownSnapshot, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "logs/latest.log")
if err != nil || unknownSnapshot.State != "not-read" {
t.Fatalf("expected unknown logical key to report not-read, snapshot=%+v err=%v", unknownSnapshot, err)
}
if _, err := svc.GetDeclaredFileReadSnapshotForSession(otherSession, instance.ID, "scum-server-settings"); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected unrelated session forbidden, got %v", err)
}
}
func scumTestFileWorkspace() domain.PluginFileWorkspace {
return domain.PluginFileWorkspace{
DefaultDirectoryKey: "scum-config",
Directories: []domain.PluginLogicalDirectory{
{Key: "scum-config", Label: "服务器配置", Scope: "config"},
{Key: "scum-logs", Label: "日志文件", Scope: "logs"},
},
Files: []domain.PluginLogicalFile{
{Key: "scum-server-settings", DirectoryKey: "scum-config", Label: "ServerSettings.ini", Kind: "config", Editable: true},
{Key: "scum-chat-log", DirectoryKey: "scum-logs", Label: "Chat.log", Kind: "log", StreamKey: "scum.chat"},
},
ConfigFields: []domain.PluginConfigField{
{Key: "max-players", FileKey: "scum-server-settings", ConfigKey: "MaxPlayers", Label: "最大玩家数", Description: "玩家上限", Control: "number", Minimum: 1, Maximum: 128, DefaultValue: "128", RestartImpact: "restart-required"},
},
}
}
func createDeclaredFileReadJob(t *testing.T, svc *CoreService, instance domain.ServerInstance, endpoint domain.RunEndpoint, id string, state domain.JobState, minuteOffset int, content string) domain.Job {
t.Helper()
job, err := svc.CreateJob(domain.Job{
ID: id,
ServerInstanceID: instance.ID,
RunEndpointID: endpoint.ID,
Capability: domain.JobCapabilityFilesRead,
TargetKey: "scum-server-settings",
IdempotencyKey: id,
})
if err != nil {
t.Fatalf("create declared file read job: %v", err)
}
stamp := fixedTime.Add(time.Duration(minuteOffset) * time.Minute)
job.State = state
job.UpdatedAt = stamp
if state == domain.JobStateSucceeded || state == domain.JobStateFailed || state == domain.JobStateCancelled {
job.TerminalAt = stamp
}
if state == domain.JobStateSucceeded {
job.ExecutionResult = domain.JobExecutionResult{Kind: "file.read", Version: minuteOffset, Checksum: validator.BytesChecksum([]byte(content)), SizeBytes: int64(len(content)), Content: content}
}
if err := svc.store.Jobs().Update(job); err != nil {
t.Fatalf("update declared file read job: %v", err)
}
return job
}
func TestConfigWriteTerminalResultAppliesDurableTypedProjection(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "typed-config-owner", DisplayName: "Typed Config Owner", Email: "typed-config-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "typed-config-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Typed Config", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create typed config server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
current, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
if err != nil {
t.Fatalf("read typed config: %v", err)
}
proposed := current.Content + "motd=typed\n"
dispatch, err := svc.ApproveServerConfigWriteForSession(ownerSession, domain.ServerConfigWriteApproval{ServerInstanceID: instance.ID, ExpectedConfigVersion: current.ConfigVersion, ExpectedChecksum: current.Checksum, Key: current.Key, ProposedContent: proposed, IdempotencyKey: "typed-config-write"})
if err != nil {
t.Fatalf("queue typed config write: %v", err)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityConfigWrite)
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register typed config Run: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityConfigWrite}, Capacity: domain.RunCapacity{MaxJobs: 2}})
if err != nil || !claim.HasJob {
t.Fatalf("claim typed config job: claim=%+v err=%v", claim, err)
}
checksum := validator.BytesChecksum([]byte(proposed))
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: "config write completed"}, Message: "config write completed", ExecutionResult: domain.JobExecutionResult{Kind: "file.write", Version: dispatch.Job.ExecutionInput.ExpectedVersion + 1, Checksum: checksum, SizeBytes: int64(len(proposed)), Summary: "atomic compare-and-swap file write"}}); err != nil {
t.Fatalf("complete typed config job: %v", err)
}
updated, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
if err != nil || updated.Content != proposed || updated.ConfigVersion != current.ConfigVersion+1 || updated.Checksum != checksum {
t.Fatalf("expected durable typed config projection, config=%+v err=%v", updated, err)
}
stored, err := svc.GetJobForSession(ownerSession, dispatch.Job.ID)
if err != nil {
t.Fatalf("read typed config job: %v", err)
}
if stored.ExecutionResult.Content != "" || stored.ExecutionResult.Checksum != checksum || stored.ExecutionResult.Version != current.ConfigVersion+1 {
t.Fatalf("unexpected safe/private job result projection: %+v", stored.ExecutionResult)
}
}
func TestCoreServiceUpdatesUsersProfileAndTheme(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.CreateUser(domain.User{
ID: "user-profile",
DisplayName: "Profile User",
Email: "profile@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
}); err != nil {
t.Fatalf("create user: %v", err)
}
session, err := svc.LoginUser(domain.UserLogin{Account: "profile@example.test", Password: "secret-password"})
if err != nil {
t.Fatalf("login: %v", err)
}
updated, err := svc.UpdateCurrentUserProfile(session.SessionID, domain.UserProfile{AvatarURL: "avatar://profile", Phone: "13900000000", ContactNote: "primary contact"})
if err != nil {
t.Fatalf("update profile: %v", err)
}
if updated.Profile.Phone != "13900000000" || updated.Profile.ContactNote != "primary contact" {
t.Fatalf("unexpected profile: %+v", updated.Profile)
}
theme, err := svc.UpdateCurrentUserTheme(session.SessionID, domain.UserThemePreference{PaletteID: "crystal-moonlight", BackgroundPresetID: "moon"})
if err != nil {
t.Fatalf("update theme: %v", err)
}
if theme.UserID != "user-profile" || theme.Persistence != "api" || !theme.UpdatedAt.Equal(fixedTime) {
t.Fatalf("unexpected theme preference: %+v", theme)
}
adminUpdate := updated
adminUpdate.Status = domain.UserStatusDisabled
adminUpdate.Roles = []string{"server-owner"}
adminUpdate.DisplayName = "Profile User Updated"
saved, err := svc.UpdateUser(updated.ID, adminUpdate)
if err != nil {
t.Fatalf("admin update user: %v", err)
}
if saved.Status != domain.UserStatusDisabled || saved.Roles[0] != "server-owner" || saved.DisplayName != "Profile User Updated" {
t.Fatalf("unexpected updated user: %+v", saved)
}
}
func createServiceUserAndLogin(t *testing.T, svc *CoreService, user domain.User) string {
t.Helper()
if _, err := svc.CreateUser(user); err != nil {
t.Fatalf("create %s: %v", user.ID, err)
}
session, err := svc.LoginUser(domain.UserLogin{Account: user.Email, Password: "secret-password"})
if err != nil {
t.Fatalf("login %s: %v", user.ID, err)
}
return session.SessionID
}
func TestCoreServiceManagesAIProviderMetadata(t *testing.T) {
svc := newTestCoreService()
created, err := svc.CreateAIProvider(validProvider())
if err != nil {
t.Fatalf("create provider: %v", err)
}
updated := created
updated.Name = "OpenAI Primary"
updated.BaseURL = "https://relay.example.test/v1"
updated.Models = []string{"gpt-4.1-mini"}
updated.DefaultModel = "gpt-4.1-mini"
updated.RelayMode = domain.AIRelayModeRelay
updated.APIKeyRef = "vault://providers/openai-primary"
got, err := svc.UpdateAIProvider(created.ID, updated)
if err != nil {
t.Fatalf("update provider: %v", err)
}
if got.Name != "OpenAI Primary" || got.Status != domain.AIProviderStatusActive || got.APIKeyRef != "vault://providers/openai-primary" {
t.Fatalf("unexpected updated provider: %+v", got)
}
disabled, err := svc.SetAIProviderStatus(created.ID, domain.AIProviderStatusDisabled)
if err != nil {
t.Fatalf("disable provider: %v", err)
}
if disabled.Status != domain.AIProviderStatusDisabled {
t.Fatalf("expected disabled provider, got %+v", disabled)
}
testResult, err := svc.TestAIProvider(created.ID)
if err != nil {
t.Fatalf("test provider: %v", err)
}
if testResult.Success || !strings.Contains(strings.Join(testResult.Violations, ","), "provider must be active") {
t.Fatalf("expected disabled provider test failure, got %+v", testResult)
}
enabled, err := svc.SetAIProviderStatus(created.ID, domain.AIProviderStatusActive)
if err != nil {
t.Fatalf("enable provider: %v", err)
}
if enabled.Status != domain.AIProviderStatusActive {
t.Fatalf("expected active provider, got %+v", enabled)
}
testResult, err = svc.TestAIProvider(created.ID)
if err != nil {
t.Fatalf("test enabled provider: %v", err)
}
if !testResult.Success || testResult.Mode != "provider" {
t.Fatalf("expected metadata test success, got %+v", testResult)
}
models, err := svc.ListAIProviderModels(created.ID)
if err != nil {
t.Fatalf("list provider models: %v", err)
}
if models.ProviderID != created.ID || models.DefaultModel != "gpt-4.1-mini" || len(models.Models) != 1 || models.Models[0] != "gpt-4.1-mini" {
t.Fatalf("unexpected provider models: %+v", models)
}
}
func TestCoreServiceRegistersGamePluginManifest(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
registration.Manifest.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Mode: 0o600}, {Path: "bin/install-server", Mode: 0o700}, {Path: "assets/map.bin", Mode: 0o600}}
registration.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Content: "{}", Mode: 0o600}, {Path: "bin/install-server", Content: "#!/usr/bin/env sh\n"}, {Path: "assets/map.bin", Content: base64.StdEncoding.EncodeToString([]byte{0xff, 0x00, 0x7f}), Encoding: "base64"}}
plugin, err := svc.RegisterGamePluginManifest(registration)
if err != nil {
t.Fatalf("register manifest: %v", err)
}
if plugin.ID != "game.example" || plugin.ServerType != "example" || plugin.ServerDisplayName != "Example Server" {
t.Fatalf("unexpected registered plugin metadata: %+v", plugin)
}
if plugin.Status != domain.GamePluginStatusInstalled {
t.Fatalf("expected installed status, got %+v", plugin)
}
if !plugin.Permissions.AI || !plugin.Permissions.Logs || !plugin.Permissions.Files || !plugin.Permissions.Artifacts || !plugin.Permissions.Jobs {
t.Fatalf("expected aggregate permissions from manifest, got %+v", plugin.Permissions)
}
if len(plugin.Pages) != 1 || plugin.Pages[0].Permissions[0] != "server.logs.read" {
t.Fatalf("expected page metadata, got %+v", plugin.Pages)
}
if len(plugin.AIPurposes) != 1 || plugin.AIPurposes[0] != "logs.diagnose" {
t.Fatalf("expected AI purposes, got %+v", plugin.AIPurposes)
}
if len(plugin.BridgeActions) != 4 || plugin.BridgeActions[0] != string(domain.PluginBridgeActionServerInstancesRead) {
t.Fatalf("expected bridge actions, got %+v", plugin.BridgeActions)
}
if len(plugin.LifecycleAssets) != 3 || plugin.LifecycleAssets[1].Path != "bin/install-server" || plugin.LifecycleAssets[1].Mode != 0o700 || plugin.LifecycleAssets[2].Encoding != "base64" {
t.Fatalf("expected declared lifecycle assets with manifest mode defaults, got %+v", plugin.LifecycleAssets)
}
listed, err := svc.ListGamePlugins(domain.GamePluginFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled})
if err != nil || len(listed) != 1 {
t.Fatalf("list registered plugins: len=%d err=%v", len(listed), err)
}
}
func TestCoreServiceMarketplacePluginsAreFilteredSafeAndStateful(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil {
t.Fatalf("register manifest: %v", err)
}
listed, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled, Capability: "logs.read", Keyword: "development"})
if err != nil {
t.Fatalf("list marketplace plugins: %v", err)
}
if len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Source != "platform-registry" {
t.Fatalf("unexpected marketplace list: %+v", listed)
}
if len(listed[0].Capabilities) == 0 || listed[0].Capabilities[0] != "process.install" || len(listed[0].Pages) != 1 || listed[0].AIPurposes[0] != "logs.diagnose" {
t.Fatalf("expected manifest-backed projection, got %+v", listed[0])
}
detail, err := svc.GetMarketplacePlugin("game.example")
if err != nil {
t.Fatalf("get marketplace plugin: %v", err)
}
if detail.ManifestRef != "artifact://manifests/game.example/0.1.0" || detail.CreateFormSchemaRef != "schemas/create-form.schema.json" {
t.Fatalf("unexpected marketplace detail refs: %+v", detail)
}
disabled, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateActionDisable)
if err != nil {
t.Fatalf("disable marketplace plugin: %v", err)
}
if disabled.Status != domain.GamePluginStatusDisabled {
t.Fatalf("expected disabled status, got %+v", disabled)
}
enabled, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateActionEnable)
if err != nil {
t.Fatalf("enable marketplace plugin: %v", err)
}
if enabled.Status != domain.GamePluginStatusInstalled {
t.Fatalf("expected installed status after enable, got %+v", enabled)
}
missing, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{Keyword: "missing"})
if err != nil || len(missing) != 0 {
t.Fatalf("expected empty keyword result, len=%d err=%v", len(missing), err)
}
if _, err := svc.GetMarketplacePlugin("missing"); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected missing plugin error, got %v", err)
}
if _, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateAction("download")); err == nil || !strings.Contains(err.Error(), "action is not supported") {
t.Fatalf("expected unsupported action validation, got %v", err)
}
if _, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{Keyword: "sk-raw-secret"}); err == nil || !strings.Contains(err.Error(), "raw credential") {
t.Fatalf("expected unsafe keyword validation, got %v", err)
}
}
func TestCoreServiceMarketplaceListSkipsHistoricalInvalidCapabilities(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil {
t.Fatalf("register current manifest: %v", err)
}
legacy := gamePluginFromManifestRegistration(validPluginManifestRegistration())
legacy.ID = "game.legacy"
legacy.Name = "Legacy Server"
legacy.RequiredRunCapabilities = append(legacy.RequiredRunCapabilities, "remote.run.protected.sql", "remote.run.protected.rcon")
legacy.RemoteAccess.RunCapabilities = append(legacy.RemoteAccess.RunCapabilities, "remote.run.protected.sql", "remote.run.protected.rcon")
if err := svc.store.GamePlugins().Create(legacy); err != nil {
t.Fatalf("seed historical plugin: %v", err)
}
listed, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{})
if err != nil {
t.Fatalf("list marketplace plugins with historical record: %v", err)
}
if len(listed) != 1 || listed[0].ID != "game.example" {
t.Fatalf("expected only current marketplace plugin, got %+v", listed)
}
}
func TestCoreServiceAuthorizesPluginBridgeActions(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil {
t.Fatalf("register manifest: %v", err)
}
allowed, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{
PluginID: "game.example",
RouteKey: "logs",
Action: domain.PluginBridgeActionLogsQuery,
})
if err != nil {
t.Fatalf("authorize logs query: %v", err)
}
if !allowed.Allowed || allowed.RequiredPermissions[0] != "server.logs.read" {
t.Fatalf("expected allowed logs bridge action, got %+v", allowed)
}
missingPermission, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{
PluginID: "game.example",
RouteKey: "logs",
Action: domain.PluginBridgeActionFilesRequest,
})
if err != nil {
t.Fatalf("authorize files request: %v", err)
}
if missingPermission.Allowed || !strings.Contains(missingPermission.Reason, "required permission") {
t.Fatalf("expected missing permission denial, got %+v", missingPermission)
}
aiAllowed, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{
PluginID: "game.example",
RouteKey: "logs",
Action: domain.PluginBridgeActionAIInvoke,
AIPurpose: "logs.diagnose",
})
if err != nil {
t.Fatalf("authorize AI request: %v", err)
}
if !aiAllowed.Allowed {
t.Fatalf("expected allowed AI bridge action, got %+v", aiAllowed)
}
aiDenied, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{
PluginID: "game.example",
RouteKey: "logs",
Action: domain.PluginBridgeActionAIInvoke,
AIPurpose: "config.suggest",
})
if err != nil {
t.Fatalf("authorize undeclared AI request: %v", err)
}
if aiDenied.Allowed || !strings.Contains(aiDenied.Reason, "ai purpose") {
t.Fatalf("expected undeclared AI purpose denial, got %+v", aiDenied)
}
_, err = svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{
PluginID: "game.example",
RouteKey: "logs",
Action: domain.PluginBridgeAction("direct.run.socket"),
})
if err == nil || !strings.Contains(err.Error(), "action is not supported") {
t.Fatalf("expected unsupported action validation error, got %v", err)
}
}
func TestCoreServiceRemoteAccessRequiresPluginDeclaration(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities,
domain.JobCapabilityRemoteRunDBSQLiteQuery,
domain.JobCapabilityRemoteRunLogsTransfer,
domain.JobCapabilityRemoteRunRCONCommand,
)
registration.Manifest.Permissions = append(registration.Manifest.Permissions, "server.remote.access")
registration.Manifest.Bridge.Actions = append(registration.Manifest.Bridge.Actions, string(domain.PluginBridgeActionRemoteAccessRequest))
registration.Manifest.Pages = append(registration.Manifest.Pages, domain.GamePluginPage{
Key: "remote",
Title: "Remote",
Path: "/remote",
Permissions: []string{"server.remote.access"},
BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)},
})
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{
Methods: []string{"run"},
RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand},
DatabaseEngines: []string{"sqlite"},
RCON: true,
LogTransfer: true,
}
plugin, err := svc.RegisterGamePluginManifest(registration)
if err != nil {
t.Fatalf("register remote manifest: %v", err)
}
if !plugin.Permissions.RemoteAccess || !plugin.RemoteAccess.RCON || plugin.RemoteAccess.DatabaseEngines[0] != "sqlite" {
t.Fatalf("expected remote access metadata from manifest, got %+v", plugin)
}
marketplace, err := svc.GetMarketplacePlugin(plugin.ID)
if err != nil || !marketplace.RemoteAccess.LogTransfer || marketplace.RemoteAccess.Methods[0] != "run" {
t.Fatalf("expected marketplace remote access projection, got %+v err=%v", marketplace, err)
}
endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{
ID: "run-remote",
DisplayName: "Remote Run",
Version: "0.1.0",
Capabilities: append([]string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"}, plugin.RemoteAccess.RunCapabilities...),
Capacity: domain.RunCapacity{MaxJobs: 2},
})
if err != nil {
t.Fatalf("create remote endpoint: %v", err)
}
ownerSession := 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(ownerSession, domain.ServerInstance{
ID: "server-remote",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Remote Server",
State: domain.ServerInstanceStateRunning,
})
if err != nil {
t.Fatalf("create remote server: %v", err)
}
queued, err := svc.ExecutePluginBridgeAction(ownerSession, domain.PluginBridgeExecuteRequest{
RequestID: "remote-rcon-1",
PluginID: plugin.ID,
RouteKey: "remote",
ServerInstanceID: instance.ID,
Action: domain.PluginBridgeActionRemoteAccessRequest,
Payload: map[string]string{
"capability": domain.JobCapabilityRemoteRunRCONCommand,
"targetKey": "rcon/command",
"inputRef": "input://server-remote/rcon/command/1",
"idempotencyKey": "idem-remote-rcon",
},
})
if err != nil {
t.Fatalf("execute remote bridge action: %v", err)
}
if queued.Status != "queued" || queued.Result["capability"] != domain.JobCapabilityRemoteRunRCONCommand {
t.Fatalf("expected queued remote bridge job, got %+v", queued)
}
plainPlugin, plainEndpoint := createPluginAndRunEndpoint(t, svc)
plainEndpoint.Capabilities = append(plainEndpoint.Capabilities, domain.JobCapabilityRemoteRunRCONCommand)
if err := svc.store.RunEndpoints().Update(plainEndpoint); err != nil {
t.Fatalf("extend plain endpoint: %v", err)
}
plainInstance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-plain", PluginID: plainPlugin.ID, RunEndpointID: plainEndpoint.ID, Name: "Plain Server"})
if err != nil {
t.Fatalf("create plain server: %v", err)
}
_, err = svc.CreateJob(domain.Job{ID: "job-remote-denied", ServerInstanceID: plainInstance.ID, RunEndpointID: plainEndpoint.ID, Capability: domain.JobCapabilityRemoteRunRCONCommand, TargetKey: "rcon/command", InputRef: "input://plain/rcon/command/1", IdempotencyKey: "idem-denied"})
if err == nil || !strings.Contains(err.Error(), "plugin missing required capability") {
t.Fatalf("expected undeclared remote job denial, got %v", err)
}
}
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["sqlRef"] != "sql/players.by-id.sql" || 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 TestCoreServiceUpsertsDuplicateGamePluginManifest(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
if _, err := svc.RegisterGamePluginManifest(registration); err != nil {
t.Fatalf("register first manifest: %v", err)
}
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, Name: "Manifest refresh server"}); err != nil {
t.Fatalf("create server for manifest refresh: %v", err)
}
if err := svc.store.RuntimeBindings().Create(domain.RuntimeBinding{ID: "runtime-binding-manifest-refresh-server", ServerInstanceID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
t.Fatalf("create runtime binding for manifest refresh: %v", err)
}
registration.Manifest.Version = "0.1.1"
registration.Manifest.Description = "Development plugin refreshed"
registration.ManifestRef = "artifact://manifests/game.example/0.1.1"
updated, err := svc.RegisterGamePluginManifest(registration)
if err != nil {
t.Fatalf("upsert manifest: %v", err)
}
if updated.ID != "game.example" || updated.Version != "0.1.1" || updated.ManifestRef != "artifact://manifests/game.example/0.1.1" {
t.Fatalf("expected existing plugin to update in place, got %+v", updated)
}
listed, err := svc.ListGamePlugins(domain.GamePluginFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled})
if err != nil || len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Version != "0.1.1" {
t.Fatalf("expected one refreshed plugin after upsert, listed=%+v err=%v", listed, err)
}
server, err := svc.GetServerInstance("manifest-refresh-server")
if err != nil || server.PluginVersion != "0.1.1" {
t.Fatalf("expected existing server to follow refreshed plugin version, server=%+v err=%v", server, err)
}
binding, err := svc.store.RuntimeBindings().Get("runtime-binding-manifest-refresh-server")
if err != nil || binding.PluginVersion != "0.1.1" {
t.Fatalf("expected runtime binding to follow refreshed plugin version, binding=%+v err=%v", binding, err)
}
}
func TestCoreServiceRejectsUnsafeGamePluginManifest(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
registration.Manifest.Description = "requires direct run socket and raw AI key"
_, err := svc.RegisterGamePluginManifest(registration)
if err == nil || !strings.Contains(err.Error(), "direct run access") || !strings.Contains(err.Error(), "raw credential") {
t.Fatalf("expected unsafe manifest rejection, got %v", err)
}
}
func TestCoreServiceRejectsInvalidAIProviderManagement(t *testing.T) {
svc := newTestCoreService()
provider := validProvider()
if _, err := svc.CreateAIProvider(provider); err != nil {
t.Fatalf("create provider: %v", err)
}
provider.APIKeyRef = "sk-raw-secret"
_, err := svc.UpdateAIProvider(provider.ID, provider)
if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") {
t.Fatalf("expected raw secret rejection, got %v", err)
}
_, err = svc.SetAIProviderStatus(provider.ID, domain.AIProviderStatusError)
if err == nil || !strings.Contains(err.Error(), "status must be active or disabled") {
t.Fatalf("expected invalid status rejection, got %v", err)
}
_, err = svc.UpdateAIProvider("missing", provider)
if !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected missing update target, got %v", err)
}
_, err = svc.TestAIProvider("missing")
if !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected missing test target, got %v", err)
}
_, err = svc.ListAIProviderModels("missing")
if !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected missing models target, got %v", err)
}
}
func TestCoreServiceReturnsExistingJobForDuplicateIdempotencyKey(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
instance, err := svc.CreateServerInstance(domain.ServerInstance{
ID: "server-1",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "SCUM #1",
})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
first, err := svc.CreateJob(domain.Job{
ID: "job-1",
ServerInstanceID: instance.ID,
RunEndpointID: endpoint.ID,
Capability: "process.start",
IdempotencyKey: "idem-start",
})
if err != nil {
t.Fatalf("create first job: %v", err)
}
second, err := svc.CreateJob(domain.Job{
ID: "job-2",
ServerInstanceID: instance.ID,
RunEndpointID: endpoint.ID,
Capability: "process.start",
IdempotencyKey: "idem-start",
})
if err != nil {
t.Fatalf("create second job: %v", err)
}
if second.ID != first.ID {
t.Fatalf("expected idempotent job %q, got %q", first.ID, second.ID)
}
jobs, err := svc.ListJobs(domain.JobFilter{RunEndpointID: endpoint.ID})
if err != nil {
t.Fatalf("list jobs: %v", err)
}
if len(jobs) != 1 {
t.Fatalf("expected one stored job, got %+v", jobs)
}
}
func TestCoreServiceRejectsJobTargetMismatch(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
otherEndpoint := endpoint
otherEndpoint.ID = "run-other"
if _, err := svc.CreateRunEndpoint(otherEndpoint); err != nil {
t.Fatalf("create other endpoint: %v", err)
}
instance, err := svc.CreateServerInstance(domain.ServerInstance{
ID: "server-1",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "SCUM #1",
})
if err != nil {
t.Fatalf("create server instance: %v", err)
}
_, err = svc.CreateJob(domain.Job{
ID: "job-1",
ServerInstanceID: instance.ID,
RunEndpointID: otherEndpoint.ID,
Capability: "process.start",
IdempotencyKey: "idem-start",
})
if err == nil || !strings.Contains(err.Error(), "job runEndpointId must match server instance") {
t.Fatalf("expected target mismatch rejection, got %v", err)
}
}
func TestCoreServicePropagatesDuplicateErrors(t *testing.T) {
svc := newTestCoreService()
user := domain.User{ID: "user-1", DisplayName: "Operator", Status: domain.UserStatusActive}
if _, err := svc.CreateUser(user); err != nil {
t.Fatalf("create user: %v", err)
}
_, err := svc.CreateUser(user)
if !errors.Is(err, repo.ErrDuplicate) {
t.Fatalf("expected duplicate error, got %v", err)
}
}
func newTestCoreService() *CoreService {
svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime })
svc.ConfigureDistributionBuilder(staticDistributionBuilder{payload: []byte("platform-built-distribution")})
return svc
}
type staticDistributionBuilder struct {
payload []byte
err error
}
func (builder staticDistributionBuilder) Readiness() (bool, string) {
if builder.err != nil {
return false, builder.err.Error()
}
return true, ""
}
func (builder staticDistributionBuilder) Build(domain.DistributionBuildInput) ([]byte, error) {
if builder.err != nil {
return nil, builder.err
}
return domain.CopyBytes(builder.payload), nil
}
func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlugin, domain.RunEndpoint) {
t.Helper()
plugin, err := svc.CreateGamePlugin(domain.GamePlugin{
ID: "server.scum",
Name: "SCUM",
Version: "1.0.0",
ServerType: "scum",
ManifestRef: "artifact://manifests/server.scum/1.0.0",
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
RequiredRunCapabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "config.write", "files.read", "files.write"},
DeclaredPermissions: []string{"server.files.read", "server.files.write"},
LifecycleActions: domain.PluginLifecycleActions{
Install: "actions/install.json",
Start: "actions/start.json",
Stop: "actions/stop.json",
},
Permissions: domain.PluginPermissions{
Logs: true,
Files: true,
Jobs: true,
},
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
})
if err != nil {
t.Fatalf("create plugin fixture: %v", err)
}
endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{
ID: "run-local",
DisplayName: "Local Run",
Version: "0.1.0",
Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "config.write", "files.read", "files.write"},
Capacity: domain.RunCapacity{MaxJobs: 4},
LastHeartbeatAt: svc.now(),
})
if err != nil {
t.Fatalf("create run endpoint fixture: %v", err)
}
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",
SQLRef: "sql/players.by-id.sql",
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)
if err != nil {
t.Fatalf("get plugin for runtime binding: %v", err)
}
profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, profileKey)
if !ok {
t.Fatalf("runtime profile %s missing", profileKey)
}
required, _ := runtimeBindingKeys(plugin.RuntimeProfiles, profile)
bindings := map[string]string{}
for _, key := range required {
if runtimeBindingTestKeyIsSensitive(key) {
bindings[key] = "secret://" + instance.ID + "/" + strings.ReplaceAll(key, "/", "-")
} else {
bindings[key] = "runtime." + strings.ReplaceAll(key, "/", ".")
}
}
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: profileKey, Bindings: bindings}, true)
if err != nil {
t.Fatalf("build runtime binding: %v", err)
}
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
t.Fatalf("create runtime binding: %v", err)
}
return binding
}
func runtimeBindingTestKeyIsSensitive(key string) bool {
normalized := strings.ToLower(key)
return strings.Contains(normalized, "password") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "secret") || strings.Contains(normalized, "token") || strings.Contains(normalized, "dsn")
}
func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
return domain.GamePluginManifestRegistration{
ManifestRef: "artifact://manifests/game.example/0.1.0",
Manifest: domain.GamePluginManifest{
ID: "game.example",
Name: "Example Server",
Description: "Development plugin",
Version: "0.1.0",
Kind: "game-plugin",
Tags: []string{"example", "development"},
Server: domain.GamePluginManifestServer{
Type: "example",
DisplayName: "Example Server",
SupportedOS: []string{"linux", "darwin"},
CreateFormSchema: "schemas/create-form.schema.json",
},
Bridge: domain.GamePluginBridge{
Actions: []string{
string(domain.PluginBridgeActionServerInstancesRead),
string(domain.PluginBridgeActionLogsQuery),
string(domain.PluginBridgeActionFilesRequest),
string(domain.PluginBridgeActionAIInvoke),
},
},
Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"},
Permissions: []string{"server.read", "server.lifecycle", "server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"},
Actions: domain.PluginLifecycleActions{
Install: "actions/install.json",
Start: "actions/start.json",
Stop: "actions/stop.json",
Restart: "actions/restart.json",
},
Pages: []domain.GamePluginPage{
{
Key: "logs",
Title: "Logs",
Path: "/logs",
Permissions: []string{"server.logs.read", "ai.invoke"},
BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)},
},
},
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"},
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
},
}
}
func validProvider() domain.AIProvider {
return 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", "gpt-4.1-mini"},
DefaultModel: "gpt-4.1",
RelayMode: domain.AIRelayModeDirect,
TimeoutMS: 30000,
RedactionPolicy: "default",
}
}