Run pushes typed SCUM facts through POST /api/v1/run/scum/facts, but the production router rejected that path before the signature middleware because runServiceRequest listed individual Run channel prefixes, and MySQL kept SCUM rows inside the whole metadata snapshot instead of platform tables. - /api/v1/run/ is now the signed machine channel space while /api/v1/run/endpoints keeps normal bearer/admin authorization. - MySQL gets real scum_user, scum_user_trajectory, scum_vehicle, scum_vehicle_trajectory and scum_vehicle_lock tables with parameterized per-row repositories instead of full snapshot rewrites. Snapshot-shaped tables from the unreleased interim build are replaced, and SCUM rows still inside a metadata snapshot are migrated once. - Facts ingest verifies the target server plugin type and converges stale online users to offline after SCUMUserOfflineAfter. - The plugin page and browser read one bounded /scum/surface response instead of five list calls per refresh. Call-count budget for one server: per 5s facts batch, one SELECT plus one INSERT/UPDATE per reported user and vehicle, one INSERT per moved trajectory sample or new lock row, one bounded stale-user SELECT, and a trajectory retention DELETE at most once per hour. One browser refresh issues one surface request every 15s instead of five list requests.
167 lines
8.5 KiB
Go
167 lines
8.5 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
func TestSCUMFactIngestMaintainsPlatformTablesAndWelcomeQueue(t *testing.T) {
|
|
svc, session, runSession, instance := newSourceRCONFixture(t)
|
|
squadID := int64(42)
|
|
bank := int64(1200)
|
|
gold := int64(3)
|
|
exists := true
|
|
locked := true
|
|
observedAt := fixedTime.Add(-1 * time.Minute)
|
|
|
|
result, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
|
RunEndpointID: instance.RunEndpointID,
|
|
SessionToken: runSession,
|
|
ServerInstanceID: instance.ID,
|
|
Users: []domain.SCUMUserFact{{
|
|
SteamID: "76561198000000001", DisplayName: "Mira", SquadID: &squadID, LoginIP: "203.0.113.7", Online: true, Login: true,
|
|
ObservedAt: observedAt, LoginObservedAt: observedAt, Position: &domain.SCUMPosition{X: 10, Y: 20, Z: 3}, BankBalance: &bank, GoldBars: &gold, RiddenGameVehicleID: "game-veh-1",
|
|
}},
|
|
Vehicles: []domain.SCUMVehicleFact{{
|
|
GameVehicleID: "game-veh-1", VehicleClass: "BPC_Laika_C", DisplayName: "Laika", Exists: &exists, Locked: &locked, LockedBySteamID: "76561198000000001", LockedAt: observedAt, ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 400, Y: 200, Z: 0},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ingest SCUM facts: %v", err)
|
|
}
|
|
if !result.Accepted || result.AcceptedUserCount != 1 || result.AcceptedVehicleCount != 1 || result.WelcomeQueuedCount != 1 {
|
|
t.Fatalf("unexpected ingest result: %+v", result)
|
|
}
|
|
|
|
users, err := svc.ListSCUMUsersForSession(session, domain.SCUMUserFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(users) != 1 {
|
|
t.Fatalf("list SCUM users: users=%+v err=%v", users, err)
|
|
}
|
|
user := users[0]
|
|
if user.SteamID != "76561198000000001" || user.SquadID != squadID || user.LastLoginIP != "203.0.113.7" || !user.Online || user.BankBalance == nil || *user.BankBalance != bank || user.GoldBars == nil || *user.GoldBars != gold || user.RiddenVehicleID == "" || user.GameVehicleID != "game-veh-1" {
|
|
t.Fatalf("unexpected SCUM user: %+v", user)
|
|
}
|
|
if user.LastLoginAge != time.Minute {
|
|
t.Fatalf("expected one-minute login age, got %s", user.LastLoginAge)
|
|
}
|
|
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 1 {
|
|
t.Fatalf("expected one queued welcome job, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
if jobs[0].Capability != domain.JobCapabilityRemoteRunRCONCommand || !jobs[0].QueueEligibleAt.Equal(fixedTime.Add(domain.SCUMWelcomeMessageDelay)) {
|
|
t.Fatalf("unexpected welcome job: %+v", jobs[0])
|
|
}
|
|
|
|
userTracks, err := svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(userTracks) != 1 || userTracks[0].RiddenVehicleID != user.RiddenVehicleID {
|
|
t.Fatalf("unexpected user trajectory rows: rows=%+v err=%v", userTracks, err)
|
|
}
|
|
vehicles, err := svc.ListSCUMVehiclesForSession(session, domain.SCUMVehicleFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(vehicles) != 1 || vehicles[0].GameVehicleID != "game-veh-1" || !vehicles[0].Exists || !vehicles[0].Locked {
|
|
t.Fatalf("unexpected SCUM vehicles: vehicles=%+v err=%v", vehicles, err)
|
|
}
|
|
vehicleTracks, err := svc.ListSCUMVehicleTrajectoriesForSession(session, domain.SCUMVehicleTrajectoryFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(vehicleTracks) != 1 || vehicleTracks[0].SCUMVehicleID != vehicles[0].ID {
|
|
t.Fatalf("unexpected vehicle trajectory rows: rows=%+v err=%v", vehicleTracks, err)
|
|
}
|
|
locks, err := svc.ListSCUMVehicleLocksForSession(session, domain.SCUMVehicleLockFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(locks) != 1 || locks[0].SCUMUserID != user.ID || locks[0].SCUMVehicleID != vehicles[0].ID {
|
|
t.Fatalf("unexpected vehicle lock history: locks=%+v err=%v", locks, err)
|
|
}
|
|
|
|
_, err = svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
|
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID,
|
|
Users: []domain.SCUMUserFact{{SteamID: "76561198000000001", Online: true, ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 10, Y: 20, Z: 3}}},
|
|
Vehicles: []domain.SCUMVehicleFact{{GameVehicleID: "game-veh-1", ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 400, Y: 200, Z: 0}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("re-ingest unchanged SCUM facts: %v", err)
|
|
}
|
|
userTracks, _ = svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
|
vehicleTracks, _ = svc.ListSCUMVehicleTrajectoriesForSession(session, domain.SCUMVehicleTrajectoryFilter{ServerInstanceID: instance.ID})
|
|
if len(userTracks) != 1 || len(vehicleTracks) != 1 {
|
|
t.Fatalf("unchanged positions must not create trajectory rows, users=%+v vehicles=%+v", userTracks, vehicleTracks)
|
|
}
|
|
|
|
_, err = svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
|
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID,
|
|
Users: []domain.SCUMUserFact{{SteamID: "76561198000000001", Online: false, ObservedAt: fixedTime, Position: &domain.SCUMPosition{X: 11, Y: 21, Z: 4}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ingest offline SCUM user fact: %v", err)
|
|
}
|
|
userTracks, _ = svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
|
if len(userTracks) != 1 {
|
|
t.Fatalf("offline users must not create trajectory rows, rows=%+v", userTracks)
|
|
}
|
|
}
|
|
|
|
func TestSCUMFactIngestQueuesFreshWelcomeAfterActivityGap(t *testing.T) {
|
|
svc, session, runSession, instance := newSourceRCONFixture(t)
|
|
firstLogin := fixedTime.Add(-20 * time.Minute)
|
|
secondLogin := fixedTime.Add(-4 * time.Minute)
|
|
|
|
for _, loginAt := range []time.Time{firstLogin, secondLogin} {
|
|
result, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID, Users: []domain.SCUMUserFact{{SteamID: "76561198000000002", DisplayName: "Noah", Online: true, Login: true, ObservedAt: loginAt, LoginObservedAt: loginAt}}})
|
|
if err != nil {
|
|
t.Fatalf("ingest login at %s: %v", loginAt, err)
|
|
}
|
|
if result.WelcomeQueuedCount != 1 {
|
|
t.Fatalf("expected fresh welcome for login at %s, got %+v", loginAt, result)
|
|
}
|
|
}
|
|
|
|
users, err := svc.ListSCUMUsersForSession(session, domain.SCUMUserFilter{ServerInstanceID: instance.ID, SteamID: "76561198000000002"})
|
|
if err != nil || len(users) != 1 {
|
|
t.Fatalf("list gap user: users=%+v err=%v", users, err)
|
|
}
|
|
if users[0].LastLoginAge != 4*time.Minute {
|
|
t.Fatalf("expected last login age to show the second login, got %s", users[0].LastLoginAge)
|
|
}
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 2 {
|
|
t.Fatalf("expected two welcome jobs after activity gap, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
}
|
|
|
|
func TestSCUMStaleOnlineUserConvergesToOffline(t *testing.T) {
|
|
svc, session, runSession, instance := newSourceRCONFixture(t)
|
|
staleAt := fixedTime.Add(-16 * time.Minute)
|
|
if _, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
|
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID,
|
|
Users: []domain.SCUMUserFact{{SteamID: "76561198000000003", DisplayName: "Stale", Online: true, Login: true, ObservedAt: staleAt, LoginObservedAt: staleAt}},
|
|
}); err != nil {
|
|
t.Fatalf("ingest stale SCUM user fact: %v", err)
|
|
}
|
|
|
|
users, err := svc.ListSCUMUsersForSession(session, domain.SCUMUserFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(users) != 1 {
|
|
t.Fatalf("list stale SCUM users: users=%+v err=%v", users, err)
|
|
}
|
|
if users[0].Online {
|
|
t.Fatalf("expected a user without activity for %s to converge offline: %+v", domain.SCUMUserOfflineAfter, users[0])
|
|
}
|
|
}
|
|
|
|
func TestSCUMFactsRejectedForNonSCUMServerInstance(t *testing.T) {
|
|
svc, _, runSession, instance := newSourceRCONFixture(t)
|
|
if err := svc.store.GamePlugins().Create(domain.GamePlugin{ID: "server.other", Name: "Other", Version: "1.0.0", ServerType: "other"}); err != nil {
|
|
t.Fatalf("create non-SCUM plugin: %v", err)
|
|
}
|
|
if err := svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-other", PluginID: "server.other", RunEndpointID: instance.RunEndpointID, Name: "Other Server", State: domain.ServerInstanceStateRunning, ConfigVersion: 1}); err != nil {
|
|
t.Fatalf("create non-SCUM server: %v", err)
|
|
}
|
|
|
|
_, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
|
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: "server-other",
|
|
Users: []domain.SCUMUserFact{{SteamID: "76561198000000004", Online: true}},
|
|
})
|
|
if !errors.Is(err, ErrForbidden) {
|
|
t.Fatalf("expected SCUM facts for a non-SCUM server to be rejected, got %v", err)
|
|
}
|
|
}
|