package testkit import ( "context" "fmt" "sync" "time" "galaxy/authsession/internal/domain/challenge" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/ports" ) // InMemorySendEmailCodeAbuseProtector is a deterministic map-backed // SendEmailCodeAbuseProtector double suitable for service tests. type InMemorySendEmailCodeAbuseProtector struct { mu sync.Mutex // Err is returned directly from CheckAndReserve when set. Err error reservedUntil map[common.Email]time.Time } // CheckAndReserve applies the fixed resend cooldown using input.Now as the // authoritative decision timestamp. func (p *InMemorySendEmailCodeAbuseProtector) CheckAndReserve(ctx context.Context, input ports.SendEmailCodeAbuseInput) (ports.SendEmailCodeAbuseResult, error) { if err := ctx.Err(); err != nil { return ports.SendEmailCodeAbuseResult{}, err } if err := input.Validate(); err != nil { return ports.SendEmailCodeAbuseResult{}, fmt.Errorf("check send email code abuse: %w", err) } if p.Err != nil { return ports.SendEmailCodeAbuseResult{}, p.Err } p.mu.Lock() defer p.mu.Unlock() if p.reservedUntil == nil { p.reservedUntil = make(map[common.Email]time.Time) } reservedUntil, exists := p.reservedUntil[input.Email] if exists && input.Now.Before(reservedUntil) { return ports.SendEmailCodeAbuseResult{ Outcome: ports.SendEmailCodeAbuseOutcomeThrottled, }, nil } p.reservedUntil[input.Email] = input.Now.UTC().Add(challenge.ResendThrottleCooldown) return ports.SendEmailCodeAbuseResult{ Outcome: ports.SendEmailCodeAbuseOutcomeAllowed, }, nil } var _ ports.SendEmailCodeAbuseProtector = (*InMemorySendEmailCodeAbuseProtector)(nil)