package sendemailcodeabuse import ( "context" "testing" "time" "galaxy/authsession/internal/domain/challenge" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/ports" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newRedisClient(t *testing.T, server *miniredis.Miniredis) *redis.Client { t.Helper() client := redis.NewClient(&redis.Options{ Addr: server.Addr(), Protocol: 2, DisableIdentity: true, }) t.Cleanup(func() { assert.NoError(t, client.Close()) }) return client } func TestNew(t *testing.T) { t.Parallel() server := miniredis.RunT(t) client := newRedisClient(t, server) validCfg := Config{ KeyPrefix: "authsession:send-email-code-throttle:", OperationTimeout: 250 * time.Millisecond, } tests := []struct { name string client *redis.Client cfg Config wantErr string }{ {name: "valid config", client: client, cfg: validCfg}, {name: "nil client", client: nil, cfg: validCfg, wantErr: "nil redis client"}, { name: "empty key prefix", client: client, cfg: Config{OperationTimeout: 250 * time.Millisecond}, wantErr: "redis key prefix must not be empty", }, { name: "non-positive timeout", client: client, cfg: Config{KeyPrefix: "authsession:send-email-code-throttle:"}, wantErr: "operation timeout must be positive", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() protector, err := New(tt.client, tt.cfg) if tt.wantErr != "" { require.Error(t, err) assert.ErrorContains(t, err, tt.wantErr) return } require.NoError(t, err) require.NotNil(t, protector) }) } } func TestProtectorCheckAndReserve(t *testing.T) { t.Parallel() server := miniredis.RunT(t) protector := newTestProtector(t, server, Config{}) email := common.Email("pilot@example.com") now := time.Unix(10, 0).UTC() result, err := protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{ Email: email, Now: now, }) require.NoError(t, err) assert.Equal(t, ports.SendEmailCodeAbuseOutcomeAllowed, result.Outcome) key := protector.lookupKey(email) assert.True(t, server.Exists(key)) ttl := server.TTL(key) assert.LessOrEqual(t, ttl, challenge.ResendThrottleCooldown) assert.GreaterOrEqual(t, ttl, challenge.ResendThrottleCooldown-2*time.Second) result, err = protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{ Email: email, Now: now.Add(30 * time.Second), }) require.NoError(t, err) assert.Equal(t, ports.SendEmailCodeAbuseOutcomeThrottled, result.Outcome) ttlAfterThrottle := server.TTL(key) assert.LessOrEqual(t, ttlAfterThrottle, ttl) server.FastForward(challenge.ResendThrottleCooldown) result, err = protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{ Email: email, Now: now.Add(challenge.ResendThrottleCooldown), }) require.NoError(t, err) assert.Equal(t, ports.SendEmailCodeAbuseOutcomeAllowed, result.Outcome) } func TestProtectorNilContext(t *testing.T) { t.Parallel() server := miniredis.RunT(t) protector := newTestProtector(t, server, Config{}) _, err := protector.CheckAndReserve(nil, ports.SendEmailCodeAbuseInput{ Email: common.Email("pilot@example.com"), Now: time.Unix(10, 0).UTC(), }) require.Error(t, err) assert.ErrorContains(t, err, "nil context") } func newTestProtector(t *testing.T, server *miniredis.Miniredis, cfg Config) *Protector { t.Helper() if cfg.KeyPrefix == "" { cfg.KeyPrefix = "authsession:send-email-code-throttle:" } if cfg.OperationTimeout == 0 { cfg.OperationTimeout = 250 * time.Millisecond } protector, err := New(newRedisClient(t, server), cfg) require.NoError(t, err) return protector }