package operationlogstore_test import ( "context" "testing" "time" "galaxy/rtmanager/internal/adapters/postgres/internal/pgtest" "galaxy/rtmanager/internal/adapters/postgres/operationlogstore" "galaxy/rtmanager/internal/domain/operation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { pgtest.RunMain(m) } func newStore(t *testing.T) *operationlogstore.Store { t.Helper() pgtest.TruncateAll(t) store, err := operationlogstore.New(operationlogstore.Config{ DB: pgtest.Ensure(t).Pool(), OperationTimeout: pgtest.OperationTimeout, }) require.NoError(t, err) return store } func successStartEntry(gameID string, startedAt time.Time, sourceRef string) operation.OperationEntry { finishedAt := startedAt.Add(time.Second) return operation.OperationEntry{ GameID: gameID, OpKind: operation.OpKindStart, OpSource: operation.OpSourceLobbyStream, SourceRef: sourceRef, ImageRef: "galaxy/game:v1.2.3", ContainerID: "container-1", Outcome: operation.OutcomeSuccess, StartedAt: startedAt, FinishedAt: &finishedAt, } } func TestAppendReturnsPositiveIDs(t *testing.T) { ctx := context.Background() store := newStore(t) startedAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC) id1, err := store.Append(ctx, successStartEntry("game-001", startedAt, "1700000000000-0")) require.NoError(t, err) assert.Greater(t, id1, int64(0)) id2, err := store.Append(ctx, successStartEntry("game-001", startedAt.Add(time.Minute), "1700000000001-0")) require.NoError(t, err) assert.Greater(t, id2, id1) } func TestAppendValidatesEntry(t *testing.T) { ctx := context.Background() store := newStore(t) tests := []struct { name string mutate func(*operation.OperationEntry) }{ {"empty game id", func(e *operation.OperationEntry) { e.GameID = "" }}, {"unknown op kind", func(e *operation.OperationEntry) { e.OpKind = "exotic" }}, {"unknown op source", func(e *operation.OperationEntry) { e.OpSource = "exotic" }}, {"unknown outcome", func(e *operation.OperationEntry) { e.Outcome = "exotic" }}, {"zero started at", func(e *operation.OperationEntry) { e.StartedAt = time.Time{} }}, {"failure without error code", func(e *operation.OperationEntry) { e.Outcome = operation.OutcomeFailure e.ErrorCode = "" }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { entry := successStartEntry("game-001", time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC), "ref") tt.mutate(&entry) _, err := store.Append(ctx, entry) require.Error(t, err) }) } } func TestListByGameReturnsEntriesNewestFirst(t *testing.T) { ctx := context.Background() store := newStore(t) base := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC) for index := range 3 { _, err := store.Append(ctx, successStartEntry("game-001", base.Add(time.Duration(index)*time.Minute), "ref-game-001-")) require.NoError(t, err) } // Foreign-game entry must not appear in the list. _, err := store.Append(ctx, successStartEntry("game-other", base, "ref-other")) require.NoError(t, err) entries, err := store.ListByGame(ctx, "game-001", 10) require.NoError(t, err) require.Len(t, entries, 3) for index := range 2 { assert.True(t, !entries[index].StartedAt.Before(entries[index+1].StartedAt), "entries must be ordered started_at DESC; got %s before %s", entries[index].StartedAt, entries[index+1].StartedAt, ) } } func TestListByGameRespectsLimit(t *testing.T) { ctx := context.Background() store := newStore(t) base := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC) for index := range 5 { _, err := store.Append(ctx, successStartEntry("game-001", base.Add(time.Duration(index)*time.Minute), "ref")) require.NoError(t, err) } entries, err := store.ListByGame(ctx, "game-001", 2) require.NoError(t, err) require.Len(t, entries, 2) } func TestListByGameReturnsEmptyForUnknownGame(t *testing.T) { ctx := context.Background() store := newStore(t) entries, err := store.ListByGame(ctx, "game-missing", 10) require.NoError(t, err) assert.Empty(t, entries) } func TestListByGameRejectsInvalidArgs(t *testing.T) { ctx := context.Background() store := newStore(t) _, err := store.ListByGame(ctx, "", 10) require.Error(t, err) _, err = store.ListByGame(ctx, "game-001", 0) require.Error(t, err) _, err = store.ListByGame(ctx, "game-001", -3) require.Error(t, err) } func TestAppendRoundTripsAllFields(t *testing.T) { ctx := context.Background() store := newStore(t) startedAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC) finishedAt := startedAt.Add(2 * time.Second) original := operation.OperationEntry{ GameID: "game-001", OpKind: operation.OpKindStop, OpSource: operation.OpSourceGMRest, SourceRef: "request-7", ImageRef: "galaxy/game:v2.0.0", ContainerID: "container-X", Outcome: operation.OutcomeFailure, ErrorCode: "container_start_failed", ErrorMessage: "stop deadline exceeded", StartedAt: startedAt, FinishedAt: &finishedAt, } id, err := store.Append(ctx, original) require.NoError(t, err) entries, err := store.ListByGame(ctx, "game-001", 10) require.NoError(t, err) require.Len(t, entries, 1) got := entries[0] assert.Equal(t, id, got.ID) assert.Equal(t, original.GameID, got.GameID) assert.Equal(t, original.OpKind, got.OpKind) assert.Equal(t, original.OpSource, got.OpSource) assert.Equal(t, original.SourceRef, got.SourceRef) assert.Equal(t, original.ImageRef, got.ImageRef) assert.Equal(t, original.ContainerID, got.ContainerID) assert.Equal(t, original.Outcome, got.Outcome) assert.Equal(t, original.ErrorCode, got.ErrorCode) assert.Equal(t, original.ErrorMessage, got.ErrorMessage) assert.True(t, original.StartedAt.Equal(got.StartedAt)) require.NotNil(t, got.FinishedAt) assert.True(t, original.FinishedAt.Equal(*got.FinishedAt)) assert.Equal(t, time.UTC, got.StartedAt.Location()) assert.Equal(t, time.UTC, got.FinishedAt.Location()) } func TestNewRejectsNilDB(t *testing.T) { _, err := operationlogstore.New(operationlogstore.Config{OperationTimeout: time.Second}) require.Error(t, err) } func TestNewRejectsNonPositiveTimeout(t *testing.T) { _, err := operationlogstore.New(operationlogstore.Config{ DB: pgtest.Ensure(t).Pool(), }) require.Error(t, err) }