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/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { t.Parallel() server := miniredis.RunT(t) tests := []struct { name string cfg Config wantErr string }{ { name: "valid config", cfg: Config{ Addr: server.Addr(), DB: 1, KeyPrefix: "authsession:send-email-code-throttle:", OperationTimeout: 250 * time.Millisecond, }, }, { name: "empty addr", cfg: Config{ KeyPrefix: "authsession:send-email-code-throttle:", OperationTimeout: 250 * time.Millisecond, }, wantErr: "redis addr must not be empty", }, { name: "negative db", cfg: Config{ Addr: server.Addr(), DB: -1, KeyPrefix: "authsession:send-email-code-throttle:", OperationTimeout: 250 * time.Millisecond, }, wantErr: "redis db must not be negative", }, { name: "empty key prefix", cfg: Config{ Addr: server.Addr(), OperationTimeout: 250 * time.Millisecond, }, wantErr: "redis key prefix must not be empty", }, { name: "non-positive timeout", cfg: Config{ Addr: server.Addr(), KeyPrefix: "authsession:send-email-code-throttle:", }, wantErr: "operation timeout must be positive", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() protector, err := New(tt.cfg) if tt.wantErr != "" { require.Error(t, err) assert.ErrorContains(t, err, tt.wantErr) return } require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, protector.Close()) }) }) } } func TestProtectorPing(t *testing.T) { t.Parallel() server := miniredis.RunT(t) protector := newTestProtector(t, server, Config{}) require.NoError(t, protector.Ping(context.Background())) } 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.Addr == "" { cfg.Addr = server.Addr() } if cfg.KeyPrefix == "" { cfg.KeyPrefix = "authsession:send-email-code-throttle:" } if cfg.OperationTimeout == 0 { cfg.OperationTimeout = 250 * time.Millisecond } protector, err := New(cfg) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, protector.Close()) }) return protector }