package service import ( "testing" "browser.local/platform/domain" ) func TestGamePlayerStatePatchRejectsUnknownVersionRangeAndUnsafeWindow(t *testing.T) { svc, _, player, request := gamePlayerStatePatchFixture(t) state, err := svc.currentGamePlayerState(player) if err != nil { t.Fatal(err) } unknown := request unknown.GameVersion = "0.0.0" if err := validateGamePlayerStatePatch(state, unknown); err == nil { t.Fatal("unknown version was accepted") } outOfRange := request outOfRange.Changes[0].After = 11 if err := validateGamePlayerStatePatch(state, outOfRange); err == nil { t.Fatal("out-of-range field was accepted") } unsafe := state unsafe.PlayerOnline = true if err := validateGamePlayerStatePatch(unsafe, request); err == nil { t.Fatal("online player patch was accepted") } } func TestGamePlayerStatePatchApprovalAndFailedExecutionRemainAuditable(t *testing.T) { svc, session, player, request := gamePlayerStatePatchFixture(t) patch, err := svc.RequestGamePlayerStatePatchForSession(session, player.ID, request) if err != nil || patch.Status != domain.GamePlayerStatePatchPendingApproval || patch.Changes[0].Before != 4 || patch.Changes[0].After != 6 { t.Fatalf("request=%+v err=%v", patch, err) } approved, err := svc.ApproveGamePlayerStatePatchForSession(session, patch.ID) if err != nil || approved.Status != domain.GamePlayerStatePatchQueued || approved.ApproverID == "" { t.Fatalf("approved=%+v err=%v", approved, err) } claimed, err := svc.claimGameClientBridgeCommands(bridgeComponent(), 1) if err != nil || len(claimed) != 1 { t.Fatalf("claimed=%+v err=%v", claimed, err) } if _, err = svc.completeGameClientBridgeCommand(bridgeComponent(), domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: claimed[0].ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultFailed, Summary: "maintenance check changed"}); err != nil { t.Fatal(err) } patches, err := svc.ListGamePlayerStatePatchesForSession(session, player.ID) if err != nil || len(patches) != 1 || patches[0].Status != domain.GamePlayerStatePatchExecutionFailed || patches[0].ExecutionSummary == "" { t.Fatalf("patches=%+v err=%v", patches, err) } audits, err := svc.store.AuditEvents().List(domain.AuditEventFilter{ResourceID: patch.ID}) if err != nil || len(audits) < 3 { t.Fatalf("audits=%+v err=%v", audits, err) } } func TestGamePlayerStatePatchRequiresReadAfterWriteConfirmation(t *testing.T) { svc, session, player, request := gamePlayerStatePatchFixture(t) patch, err := svc.RequestGamePlayerStatePatchForSession(session, player.ID, request) if err != nil { t.Fatal(err) } if _, err = svc.ApproveGamePlayerStatePatchForSession(session, patch.ID); err != nil { t.Fatal(err) } claimed, err := svc.claimGameClientBridgeCommands(bridgeComponent(), 1) if err != nil || len(claimed) != 1 { t.Fatalf("claimed=%+v err=%v", claimed, err) } if _, err = svc.completeGameClientBridgeCommand(bridgeComponent(), domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: claimed[0].ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded, Summary: "confirmed after read", Payload: map[string]any{"confirmedStateVersion": "state-v2", "confirmedFields": map[string]any{"skills.running": float64(6)}}}); err != nil { t.Fatal(err) } patches, err := svc.ListGamePlayerStatePatchesForSession(session, player.ID) if err != nil || len(patches) != 1 || patches[0].Status != domain.GamePlayerStatePatchConfirmed || patches[0].ConfirmedStateVersion != "state-v2" { t.Fatalf("patches=%+v err=%v", patches, err) } } func gamePlayerStatePatchFixture(t *testing.T) (*CoreService, string, domain.GamePlayer, domain.GamePlayerStatePatchRequest) { t.Helper() svc, clock := newGameClientBridgeService(t) plugin, _ := svc.store.GamePlugins().Get("game.scum") plugin.GameClientBridge.Commands = append(plugin.GameClientBridge.Commands, domain.GameClientBridgeCommandDeclaration{Type: domain.SCUMPlayerStatePatchCommandType, ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, TimeoutSeconds: 120, MaxPayloadBytes: 4096}) if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatal(err) } user := domain.User{ID: "state-admin", DisplayName: "State Admin", Email: "state-admin@example.test", Roles: []string{"platform-admin"}, Status: domain.UserStatusActive, PasswordHash: "secret-password", CreatedAt: *clock, UpdatedAt: *clock} if err := svc.store.Users().Create(user); err != nil { t.Fatal(err) } auth, err := svc.issueAuthSession(user, "test") if err != nil { t.Fatal(err) } if err = svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-1", PluginID: "game.scum", OwnerUserID: user.ID}); err != nil { t.Fatal(err) } player := domain.GamePlayer{ID: "player-record-1", ServerInstanceID: "server-1", GamePlayerID: "steam-1", DisplayName: "Moon"} if err = svc.store.GamePlayers().Create(player); err != nil { t.Fatal(err) } snapshot := domain.GameClientBridgeSnapshot{ID: "state-snapshot-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: domain.SCUMPlayerStateSnapshotType, ObservedAt: *clock, Payload: map[string]any{"playerId": "steam-1", "gameVersion": "0.9.700.90357", "stateVersion": "state-v1", "safetyWindow": "maintenance-1", "maintenanceVerified": true, "playerOnline": false, "fields": map[string]any{"skills.running": float64(4), "attributes.strength": float64(5)}}} if err = svc.store.GameClientBridgeSnapshots().Create(snapshot); err != nil { t.Fatal(err) } return svc, auth.SessionID, player, domain.GamePlayerStatePatchRequest{GameVersion: "0.9.700.90357", ExpectedStateVersion: "state-v1", SafetyWindow: "maintenance-1", Changes: []domain.GamePlayerStatePatchChange{{FieldKey: "skills.running", Before: 4, After: 6}}, Reason: "修正受审核的角色跑步技能"} }