Files
browser/platform/repo/game_client_bridge_test.go
T

133 lines
7.0 KiB
Go

package repo
import (
"errors"
"path/filepath"
"testing"
"time"
"browser.local/platform/domain"
)
func TestGameClientBridgeCommandRepositoryIdempotencyAndRetention(t *testing.T) {
store := NewMemoryStore()
now := time.Now().UTC()
command := domain.GameClientBridgeCommand{ID: "command-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "diagnostic.ping", IdempotencyKey: "same-key", RequesterID: "user-1", State: domain.GameClientBridgeCommandSucceeded, Payload: map[string]any{"nested": map[string]any{"value": "original"}}, ExpiresAt: now.Add(-time.Minute), CompletedAt: now.Add(-time.Minute)}
if err := store.GameClientBridgeCommands().Create(command); err != nil {
t.Fatalf("create command: %v", err)
}
command.Payload["nested"].(map[string]any)["value"] = "mutated"
loaded, err := store.GameClientBridgeCommands().GetByIdempotency("server-1", "user-1", "diagnostic.ping", "same-key")
if err != nil {
t.Fatalf("get by idempotency: %v", err)
}
if loaded.Payload["nested"].(map[string]any)["value"] != "original" {
t.Fatal("repository did not isolate nested payload")
}
for _, lookup := range [][4]string{{"server-2", "user-1", "diagnostic.ping", "same-key"}, {"server-1", "user-2", "diagnostic.ping", "same-key"}, {"server-1", "user-1", "diagnostic.safe", "same-key"}} {
if _, err := store.GameClientBridgeCommands().GetByIdempotency(lookup[0], lookup[1], lookup[2], lookup[3]); !errors.Is(err, ErrNotFound) {
t.Fatalf("idempotency scope leaked for %v: %v", lookup, err)
}
}
retained, err := store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{CompletedBefore: now})
if err != nil || len(retained) != 1 {
t.Fatalf("completed retention query: len=%d err=%v", len(retained), err)
}
}
func TestGameClientBridgeSnapshotRepositoryOrderingLimitAndStreams(t *testing.T) {
store := NewMemoryStore()
now := time.Now().UTC()
for _, snapshot := range []domain.GameClientBridgeSnapshot{
{ID: "snapshot-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current", Sequence: 1, ObservedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)},
{ID: "snapshot-2", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current", Sequence: 2, ObservedAt: now, ExpiresAt: now.Add(time.Hour)},
{ID: "snapshot-expired", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "vehicles", StreamKey: "current", Sequence: 1, ObservedAt: now, ExpiresAt: now.Add(-time.Minute)},
} {
if err := store.GameClientBridgeSnapshots().Create(snapshot); err != nil {
t.Fatalf("create snapshot: %v", err)
}
}
items, err := store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: "server-1", Type: "players", Limit: 1})
if err != nil || len(items) != 1 || items[0].Sequence != 2 {
t.Fatalf("newest snapshot query: %#v err=%v", items, err)
}
expired, err := store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ExpiresBefore: now})
if err != nil || len(expired) != 1 || expired[0].ID != "snapshot-expired" {
t.Fatalf("snapshot retention query: %#v err=%v", expired, err)
}
for _, stream := range []domain.GameClientBridgeSnapshotStream{
{ID: "stream-players", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current", LatestSequence: 2},
{ID: "stream-vehicles", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "vehicles", StreamKey: "current", LatestSequence: 7},
} {
if err := store.GameClientBridgeSnapshotStreams().Create(stream); err != nil {
t.Fatalf("create stream: %v", err)
}
}
streams, err := store.GameClientBridgeSnapshotStreams().List(domain.GameClientBridgeSnapshotStreamFilter{ServerInstanceID: "server-1"})
if err != nil || len(streams) != 2 || streams[0].LatestSequence == streams[1].LatestSequence {
t.Fatalf("independent stream sequences: %#v err=%v", streams, err)
}
}
func TestGameClientBridgeFileStorePersistsAndDeletes(t *testing.T) {
path := filepath.Join(t.TempDir(), "metadata.json")
store, err := NewFileStore(path)
if err != nil {
t.Fatalf("new file store: %v", err)
}
command := domain.GameClientBridgeCommand{ID: "command-1", ServerInstanceID: "server-1", CommandType: "diagnostic.safe", RequesterID: "user-1", IdempotencyKey: "diag-1", State: domain.GameClientBridgeCommandClaimed, Claim: domain.GameClientBridgeClaim{FencingToken: 3}}
snapshot := domain.GameClientBridgeSnapshot{ID: "snapshot-1", ServerInstanceID: "server-1", Type: "health", StreamKey: "current", Sequence: 4}
stream := domain.GameClientBridgeSnapshotStream{ID: "stream-1", ServerInstanceID: "server-1", Type: "health", StreamKey: "current", LatestSequence: 4}
if err := store.GameClientBridgeCommands().Create(command); err != nil {
t.Fatal(err)
}
if err := store.GameClientBridgeSnapshots().Create(snapshot); err != nil {
t.Fatal(err)
}
if err := store.GameClientBridgeSnapshotStreams().Create(stream); err != nil {
t.Fatal(err)
}
restarted, err := NewFileStore(path)
if err != nil {
t.Fatalf("restart file store: %v", err)
}
if got, err := restarted.GameClientBridgeCommands().Get(command.ID); err != nil || got.Claim.FencingToken != 3 {
t.Fatalf("persisted command: %#v err=%v", got, err)
}
if got, err := restarted.GameClientBridgeSnapshots().Get(snapshot.ID); err != nil || got.Sequence != 4 {
t.Fatalf("persisted snapshot: %#v err=%v", got, err)
}
if got, err := restarted.GameClientBridgeSnapshotStreams().Get(stream.ID); err != nil || got.LatestSequence != 4 {
t.Fatalf("persisted stream: %#v err=%v", got, err)
}
if err := restarted.GameClientBridgeSnapshots().Delete(snapshot.ID); err != nil {
t.Fatalf("delete snapshot: %v", err)
}
reloaded, err := NewFileStore(path)
if err != nil {
t.Fatalf("reload file store: %v", err)
}
if _, err := reloaded.GameClientBridgeSnapshots().Get(snapshot.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("deleted snapshot revived: %v", err)
}
}
func TestMySQLSnapshotRoundTripIncludesGameClientBridge(t *testing.T) {
source := &MySQLStore{MemoryStore: NewMemoryStore()}
_ = source.MemoryStore.GameClientBridgeCommands().Create(domain.GameClientBridgeCommand{ID: "command-1"})
_ = source.MemoryStore.GameClientBridgeSnapshots().Create(domain.GameClientBridgeSnapshot{ID: "snapshot-1"})
_ = source.MemoryStore.GameClientBridgeSnapshotStreams().Create(domain.GameClientBridgeSnapshotStream{ID: "stream-1", LatestSequence: 8})
target := &MySQLStore{MemoryStore: NewMemoryStore()}
target.loadSnapshot(source.snapshot())
if _, err := target.MemoryStore.GameClientBridgeCommands().Get("command-1"); err != nil {
t.Fatal(err)
}
if _, err := target.MemoryStore.GameClientBridgeSnapshots().Get("snapshot-1"); err != nil {
t.Fatal(err)
}
if stream, err := target.MemoryStore.GameClientBridgeSnapshotStreams().Get("stream-1"); err != nil || stream.LatestSequence != 8 {
t.Fatalf("stream round trip: %#v err=%v", stream, err)
}
}