package service import ( "errors" "strings" "testing" "time" "browser.local/platform/domain" "browser.local/platform/repo" ) 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) } 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) != 1 { t.Fatalf("list log streams: len=%d err=%v", len(streams), err) } audit, err := svc.CreateAuditEvent(domain.AuditEvent{ ID: "audit-1", ActorID: user.ID, Action: "server.create", ResourceKind: "server-instance", ResourceID: instance.ID, Result: domain.AuditResultSuccess, Summary: "created server instance", }) if err != nil { t.Fatalf("create audit event: %v", err) } if !audit.CreatedAt.Equal(fixedTime) { t.Fatalf("expected audit timestamp default, got %+v", audit) } if _, err := svc.GetAuditEvent(audit.ID); err != nil { t.Fatalf("get audit event: %v", err) } auditEvents, err := svc.ListAuditEvents(domain.AuditEventFilter{ResourceID: instance.ID}) if err != nil || len(auditEvents) != 1 { t.Fatalf("list audit events: len=%d err=%v", len(auditEvents), err) } } 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) } 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 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 { t.Fatalf("unexpected owner metrics: %+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 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) } 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 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 != "metadata" { 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() plugin, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()) 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) } 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 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 TestCoreServiceRejectsDuplicateGamePluginManifest(t *testing.T) { svc := newTestCoreService() registration := validPluginManifestRegistration() if _, err := svc.RegisterGamePluginManifest(registration); err != nil { t.Fatalf("register first manifest: %v", err) } _, err := svc.RegisterGamePluginManifest(registration) if !errors.Is(err, repo.ErrDuplicate) { t.Fatalf("expected duplicate plugin registration, got %v", 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 { return newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime }) } 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, }, }) 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}, }) if err != nil { t.Fatalf("create run endpoint fixture: %v", err) } return plugin, endpoint } 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"}}, }, } } 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", } }