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