package service import ( "context" "encoding/json" "os" "testing" "time" "github.com/go-redis/redis/v8" "github.com/stretchr/testify/require" ) type redisCommandStub struct { scans map[string][]string stateValue string deleted []string } func (h *redisCommandStub) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { return ctx, nil } func (h *redisCommandStub) AfterProcess(ctx context.Context, cmd redis.Cmder) error { switch cmd.Name() { case "ping": if status, ok := cmd.(*redis.StatusCmd); ok { status.SetVal("PONG") } case "scan": if scan, ok := cmd.(*redis.ScanCmd); ok { scan.SetVal(h.scans[scanPattern(cmd.Args())], 0) } case "get": if str, ok := cmd.(*redis.StringCmd); ok { if h.stateValue == "" { str.SetErr(redis.Nil) return nil } str.SetVal(h.stateValue) } case "del": args := cmd.Args() keys := make([]string, 0, len(args)-1) for _, arg := range args[1:] { keys = append(keys, arg.(string)) } h.deleted = append(h.deleted, keys...) if count, ok := cmd.(*redis.IntCmd); ok { count.SetVal(int64(len(keys))) } } cmd.SetErr(nil) return nil } func (h *redisCommandStub) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { return ctx, nil } func (h *redisCommandStub) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { return nil } func scanPattern(args []interface{}) string { for index := 0; index < len(args)-1; index++ { value, ok := args[index].(string) if ok && value == "match" { if pattern, ok := args[index+1].(string); ok { return pattern } } } return "" } func newStubbedRedisService(stub *redisCommandStub) *RedisService { client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:1", MaxRetries: -1, }) client.AddHook(stub) return &RedisService{Client: client} } func TestRedisServiceGetIdentityCacheStatusReadsStateAndCountsCacheKeys(t *testing.T) { now := time.Date(2026, 6, 9, 3, 20, 0, 0, time.UTC) state, err := json.Marshal(identityMirrorStateStore{ Status: "ready", LastRefreshedAt: &now, ObservedCount: 42, UpdatedAt: &now, }) require.NoError(t, err) stub := &redisCommandStub{ stateValue: string(state), scans: map[string][]string{ "identity:mirror:*": {"identity:mirror:state", "identity:mirror:user:1"}, "identity:index:*": {"identity:index:email:a", "identity:mirror:user:1"}, }, } service := newStubbedRedisService(stub) status, err := service.GetIdentityCacheStatus(context.Background()) require.NoError(t, err) require.Equal(t, "ready", status.Status) require.True(t, status.RedisReady) require.Equal(t, int64(42), status.ObservedCount) require.Equal(t, int64(3), status.KeyCount) require.Equal(t, &now, status.LastRefreshedAt) require.Equal(t, &now, status.UpdatedAt) } func TestRedisServiceFlushIdentityCacheDeletesOnlyIdentityMirrorAndIndexKeys(t *testing.T) { stub := &redisCommandStub{ scans: map[string][]string{ "identity:mirror:*": {"identity:mirror:state", "identity:mirror:user:1"}, "identity:index:*": {"identity:index:email:a", "identity:mirror:user:1"}, }, } service := newStubbedRedisService(stub) result, err := service.FlushIdentityCache(context.Background()) require.NoError(t, err) require.Equal(t, "success", result.Status) require.Equal(t, int64(3), result.FlushedKeys) require.ElementsMatch(t, []string{ "identity:mirror:state", "identity:mirror:user:1", "identity:index:email:a", }, stub.deleted) } func TestRedisServiceGetIdentityCacheStatusReturnsUnavailableWithoutClient(t *testing.T) { status, err := (*RedisService)(nil).GetIdentityCacheStatus(context.Background()) require.NoError(t, err) require.Equal(t, "unavailable", status.Status) require.False(t, status.RedisReady) require.NotEmpty(t, status.LastError) } func TestRedisServiceFlushIdentityCacheFailsWithoutClient(t *testing.T) { _, err := (*RedisService)(nil).FlushIdentityCache(context.Background()) require.ErrorIs(t, err, os.ErrInvalid) }