package api import ( "encoding/json" "net/http" "strings" "testing" "time" "browser.local/platform/domain" "browser.local/platform/dto" "browser.local/platform/repo" "browser.local/platform/service" ) func TestSCUMResourceAPIsExposeLocalRowsAndRemovedLegacyEndpoints(t *testing.T) { store := repo.NewMemoryStore() core := service.NewCoreService(store) if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil { t.Fatalf("create owner: %v", err) } plugin := validGamePluginRequest().ToDomain() plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.read") plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000} if _, err := core.CreateGamePlugin(plugin); err != nil { t.Fatalf("create plugin: %v", err) } endpoint := validRunEndpointRequest().ToDomain() endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedRCON) endpoint.LastHeartbeatAt = time.Now().UTC() if _, err := core.CreateRunEndpoint(endpoint); err != nil { t.Fatalf("create endpoint: %v", err) } if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-api", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM API", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil { t.Fatalf("create server: %v", err) } if _, err := core.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-scum-api", PluginID: plugin.ID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:api-profile", ObservedAt: time.Now().UTC(), Rows: []map[string]any{{"gamePlayerId": "steam-api", "displayName": "API Player", "normalBalance": 25, "x": 1, "y": 2, "z": 3}}}); err != nil { t.Fatalf("seed projection: %v", err) } auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-owner@example.test", Password: "secret-password"}) if err != nil { t.Fatalf("login: %v", err) } router := NewAuthorizedRouterWithCore(core) players := getJSONWithAuth[dto.SCUMPlayerLiveStateListResponse](t, router, "/api/v1/server-instances/server-scum-api/scum/players", auth.SessionID) if players.Count != 1 || players.Items[0].GamePlayerID != "steam-api" || players.Items[0].Position.X != 1 { t.Fatalf("unexpected SCUM players response: %+v", players) } body, err := json.Marshal(players) if err != nil { t.Fatalf("marshal responses: %v", err) } for _, forbidden := range []string{"#SetFamePoints", "requestText", "SELECT ", "UPDATE ", "SCUM.db", "password", "run token", "hostPath"} { if strings.Contains(strings.ToUpper(string(body)), strings.ToUpper(forbidden)) { t.Fatalf("SCUM safe API leaked %q: %s", forbidden, body) } } for _, legacy := range []struct{ method, path string }{ {http.MethodPost, "/api/v1/server-instances/server-scum-api/rcon/commands"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/logs/live"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/logs/backfill"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/files/read-snapshot?key=scum-server-log"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/config"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/config/diff"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/config/approve"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/operations/op-1/approve"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodPost, "/api/v1/server-instances/server-scum-api/scum/workflows"}, {http.MethodGet, "/api/v1/server-instances/server-scum-api/scum/workflow-steps?workflowId=workflow-1"}, } { recorder := requestWithAuth(t, router, legacy.method, legacy.path, `{}`, auth.SessionID) assertStatus(t, recorder, http.StatusNotFound) } jobs, err := core.ListJobsForSession(auth.SessionID, domain.JobFilter{ServerInstanceID: "server-scum-api"}) if err != nil || len(jobs) != 0 { t.Fatalf("removed SCUM endpoints must not dispatch jobs, got jobs=%+v err=%v", jobs, err) } } func TestSCUMAPIsEnforceServerAuthorization(t *testing.T) { store := repo.NewMemoryStore() core := service.NewCoreService(store) if _, err := core.CreateUser(domain.User{ID: "scum-api-owner", DisplayName: "SCUM API Owner", Email: "scum-api-owner-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil { t.Fatal(err) } if _, err := core.CreateUser(domain.User{ID: "scum-api-other", DisplayName: "SCUM API Other", Email: "scum-api-other-authz@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil { t.Fatal(err) } if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil { t.Fatal(err) } if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil { t.Fatal(err) } if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-scum-authz", PluginID: "server.scum", RunEndpointID: "run-local", Name: "SCUM Authz", OwnerUserID: "scum-api-owner", State: domain.ServerInstanceStateRunning}); err != nil { t.Fatal(err) } auth, err := core.LoginUser(domain.UserLogin{Account: "scum-api-other-authz@example.test", Password: "secret-password"}) if err != nil { t.Fatal(err) } router := NewAuthorizedRouterWithCore(core) assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/server-scum-authz/scum/players", "", auth.SessionID), http.StatusForbidden, errorCodeForbidden) }