package testutils import ( "context" "sync" "github.com/redis/go-redis/v9" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "go.uber.org/zap" ) var ( redisContainer testcontainers.Container redisClient *redis.Client redisOnce sync.Once redisErr error ) // GetTestRedisClient ensures the Redis container is running and returns a client. // It uses a singleton pattern to start the container only once per test run. func GetTestRedisClient(ctx context.Context) (*redis.Client, error) { redisOnce.Do(func() { redisErr = setupRedisContainer(ctx) }) return redisClient, redisErr } func setupRedisContainer(ctx context.Context) error { logger := zap.NewNop() if zap.L() != nil { logger = zap.L() } logger.Info("Starting Redis testcontainer") req := testcontainers.ContainerRequest{ Image: "redis:7-alpine", ExposedPorts: []string{"6379/tcp"}, WaitingFor: wait.ForLog("Ready to accept connections"), } var containerErr error redisContainer, containerErr = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if containerErr != nil { logger.Error("Failed to start Redis testcontainer", zap.Error(containerErr)) return containerErr } endpoint, err := redisContainer.Endpoint(ctx, "") if err != nil { return err } redisClient = redis.NewClient(&redis.Options{ Addr: endpoint, }) // Wait for Redis to be ready if err := redisClient.Ping(ctx).Err(); err != nil { return err } logger.Info("Redis testcontainer started successfully") return nil } // TerminateRedisContainer allows manual termination if needed (mostly for cleanup) func TerminateRedisContainer(ctx context.Context) error { if redisContainer != nil { return redisContainer.Terminate(ctx) } return nil }