package healthsnapshotstore_test import ( "context" "encoding/json" "testing" "time" "galaxy/rtmanager/internal/adapters/postgres/healthsnapshotstore" "galaxy/rtmanager/internal/adapters/postgres/internal/pgtest" "galaxy/rtmanager/internal/domain/health" "galaxy/rtmanager/internal/domain/runtime" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { pgtest.RunMain(m) } func newStore(t *testing.T) *healthsnapshotstore.Store { t.Helper() pgtest.TruncateAll(t) store, err := healthsnapshotstore.New(healthsnapshotstore.Config{ DB: pgtest.Ensure(t).Pool(), OperationTimeout: pgtest.OperationTimeout, }) require.NoError(t, err) return store } func probeFailedSnapshot(gameID string, observedAt time.Time) health.HealthSnapshot { return health.HealthSnapshot{ GameID: gameID, ContainerID: "container-1", Status: health.SnapshotStatusProbeFailed, Source: health.SnapshotSourceProbe, Details: json.RawMessage(`{"consecutive_failures":3,"last_status":503,"last_error":"timeout"}`), ObservedAt: observedAt, } } func TestUpsertAndGetRoundTrip(t *testing.T) { ctx := context.Background() store := newStore(t) snapshot := probeFailedSnapshot("game-001", time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)) require.NoError(t, store.Upsert(ctx, snapshot)) got, err := store.Get(ctx, "game-001") require.NoError(t, err) assert.Equal(t, snapshot.GameID, got.GameID) assert.Equal(t, snapshot.ContainerID, got.ContainerID) assert.Equal(t, snapshot.Status, got.Status) assert.Equal(t, snapshot.Source, got.Source) assert.JSONEq(t, string(snapshot.Details), string(got.Details)) assert.True(t, snapshot.ObservedAt.Equal(got.ObservedAt)) assert.Equal(t, time.UTC, got.ObservedAt.Location()) } func TestUpsertOverwritesPriorSnapshot(t *testing.T) { ctx := context.Background() store := newStore(t) first := probeFailedSnapshot("game-001", time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)) require.NoError(t, store.Upsert(ctx, first)) second := health.HealthSnapshot{ GameID: "game-001", ContainerID: "container-2", Status: health.SnapshotStatusHealthy, Source: health.SnapshotSourceInspect, Details: json.RawMessage(`{"restart_count":0,"state":"running"}`), ObservedAt: first.ObservedAt.Add(time.Minute), } require.NoError(t, store.Upsert(ctx, second)) got, err := store.Get(ctx, "game-001") require.NoError(t, err) assert.Equal(t, "container-2", got.ContainerID) assert.Equal(t, health.SnapshotStatusHealthy, got.Status) assert.Equal(t, health.SnapshotSourceInspect, got.Source) assert.JSONEq(t, string(second.Details), string(got.Details)) assert.True(t, second.ObservedAt.Equal(got.ObservedAt)) } func TestGetReturnsNotFound(t *testing.T) { ctx := context.Background() store := newStore(t) _, err := store.Get(ctx, "game-missing") require.ErrorIs(t, err, runtime.ErrNotFound) } func TestUpsertEmptyDetailsRoundTripsAsEmptyObject(t *testing.T) { ctx := context.Background() store := newStore(t) snapshot := probeFailedSnapshot("game-001", time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)) snapshot.Details = nil require.NoError(t, store.Upsert(ctx, snapshot)) got, err := store.Get(ctx, "game-001") require.NoError(t, err) assert.JSONEq(t, "{}", string(got.Details), "empty json.RawMessage must round-trip as the SQL default {}, got %q", string(got.Details)) } func TestUpsertValidatesSnapshot(t *testing.T) { ctx := context.Background() store := newStore(t) tests := []struct { name string mutate func(*health.HealthSnapshot) }{ {"empty game id", func(s *health.HealthSnapshot) { s.GameID = "" }}, {"unknown status", func(s *health.HealthSnapshot) { s.Status = "exotic" }}, {"unknown source", func(s *health.HealthSnapshot) { s.Source = "exotic" }}, {"zero observed at", func(s *health.HealthSnapshot) { s.ObservedAt = time.Time{} }}, {"invalid json details", func(s *health.HealthSnapshot) { s.Details = json.RawMessage("not json") }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { snapshot := probeFailedSnapshot("game-001", time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)) tt.mutate(&snapshot) err := store.Upsert(ctx, snapshot) require.Error(t, err) }) } } func TestGetRejectsEmptyGameID(t *testing.T) { ctx := context.Background() store := newStore(t) _, err := store.Get(ctx, "") require.Error(t, err) } func TestNewRejectsNilDB(t *testing.T) { _, err := healthsnapshotstore.New(healthsnapshotstore.Config{OperationTimeout: time.Second}) require.Error(t, err) } func TestNewRejectsNonPositiveTimeout(t *testing.T) { _, err := healthsnapshotstore.New(healthsnapshotstore.Config{ DB: pgtest.Ensure(t).Pool(), }) require.Error(t, err) }