Files
browser/platform/service/game_player_state_patch_test.go
T

110 lines
5.8 KiB
Go

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: "修正受审核的角色跑步技能"}
}