package harness import ( "context" "sync" "testing" "github.com/redis/go-redis/v9" testcontainers "github.com/testcontainers/testcontainers-go" rediscontainer "github.com/testcontainers/testcontainers-go/modules/redis" ) const redisImage = "redis:7" // RedisEnv carries the per-package Redis fixture. The container is // started lazily on the first EnsureRedis call and torn down by // ShutdownRedis at TestMain exit. Both stream consumers and the // per-game lease store hit this real Redis (miniredis would suffice // for streams alone, but the lease semantics and eviction-by-TTL we // rely on in `health_test` are easier to verify against a real // daemon). type RedisEnv struct { container *rediscontainer.RedisContainer addr string } // Addr returns the externally reachable host:port of the Redis // container. Both the runtime under test and the harness-owned client // connect through the same endpoint. func (env *RedisEnv) Addr() string { return env.addr } // NewClient opens a fresh `*redis.Client` against the harness Redis. // Tests close their client through `t.Cleanup`; the harness keeps no // shared client to avoid cross-test connection-pool surprises. func (env *RedisEnv) NewClient(t testing.TB) *redis.Client { t.Helper() client := redis.NewClient(&redis.Options{Addr: env.addr}) t.Cleanup(func() { _ = client.Close() }) return client } var ( redisOnce sync.Once redisEnv *RedisEnv redisErr error ) // EnsureRedis starts the per-package Redis container on first // invocation and returns it. When Docker is unavailable the helper // calls `t.Skip` so the suite stays green on hosts without a daemon. func EnsureRedis(t testing.TB) *RedisEnv { t.Helper() redisOnce.Do(func() { redisEnv, redisErr = startRedis() }) if redisErr != nil { t.Skipf("rtmanager integration: redis container start failed (Docker unavailable?): %v", redisErr) } return redisEnv } // FlushRedis drops every key on the harness Redis. Tests call it from // their setup so streams, offset records, and leases from previous // scenarios do not leak. func FlushRedis(t testing.TB) { t.Helper() env := EnsureRedis(t) client := redis.NewClient(&redis.Options{Addr: env.addr}) defer func() { _ = client.Close() }() if _, err := client.FlushAll(context.Background()).Result(); err != nil { t.Fatalf("flush rtmanager redis: %v", err) } } // ShutdownRedis terminates the shared container. `TestMain` invokes it // after `m.Run`. func ShutdownRedis() { if redisEnv == nil { return } if redisEnv.container != nil { _ = testcontainers.TerminateContainer(redisEnv.container) } redisEnv = nil } func startRedis() (*RedisEnv, error) { ctx := context.Background() container, err := rediscontainer.Run(ctx, redisImage) if err != nil { return nil, err } addr, err := container.Endpoint(ctx, "") if err != nil { _ = testcontainers.TerminateContainer(container) return nil, err } return &RedisEnv{ container: container, addr: addr, }, nil }