package auth import ( "context" "crypto/rand" "crypto/rsa" "sync" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/google/uuid" "github.com/redis/go-redis/v9" ) // newMiniRedis spins up an in-memory Redis and returns a connected client. func newMiniRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) { t.Helper() mr, err := miniredis.Run() if err != nil { t.Fatalf("miniredis: %v", err) } t.Cleanup(mr.Close) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) t.Cleanup(func() { _ = rdb.Close() }) return rdb, mr } // newRSAKey generates a 2048-bit RSA key for signing test tokens. func newRSAKey(t *testing.T) *rsa.PrivateKey { t.Helper() k, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("rsa key: %v", err) } return k } // newTokenManager builds a TokenManager with the given clock and a single kid. func newTokenManager(t *testing.T, rdb *redis.Client, now func() time.Time) *TokenManager { t.Helper() key := newRSAKey(t) tm, err := NewTokenManager(rdb, TokenConfig{ SignKey: key, SignKID: "k1", AccessTTL: 15 * time.Minute, RefreshTTL: 30 * 24 * time.Hour, Now: now, }) if err != nil { t.Fatalf("NewTokenManager: %v", err) } return tm } // -------------------------------------------------------------------------- // fakeStore — in-memory UserStore for unit tests. // -------------------------------------------------------------------------- type trialRecord struct { plan string expiresAt time.Time source string } type fakeStore struct { mu sync.Mutex byEmail map[string]*User trials map[int64]trialRecord nextID int64 } func newFakeStore() *fakeStore { return &fakeStore{byEmail: map[string]*User{}, trials: map[int64]trialRecord{}} } func (f *fakeStore) CreateUserWithTrial(_ context.Context, email, pwHash string, trialDays int) (*User, error) { f.mu.Lock() defer f.mu.Unlock() if _, ok := f.byEmail[email]; ok { return nil, ErrEmailTaken } f.nextID++ u := &User{ ID: f.nextID, UUID: uuid.NewString(), Email: email, PwHash: pwHash, DpUUID: uuid.NewString(), Status: "active", } f.byEmail[email] = u f.trials[u.ID] = trialRecord{ plan: "pro", expiresAt: time.Now().UTC().AddDate(0, 0, trialDays), source: "trial", } return u, nil } func (f *fakeStore) GetUserByEmail(_ context.Context, email string) (*User, error) { f.mu.Lock() defer f.mu.Unlock() u, ok := f.byEmail[email] if !ok { return nil, ErrNotFound } cp := *u return &cp, nil } // setStatus mutates a stored user's status (e.g. to "banned") for tests. func (f *fakeStore) setStatus(email, status string) { f.mu.Lock() defer f.mu.Unlock() if u, ok := f.byEmail[email]; ok { u.Status = status } } // captureMailer records the last code it was asked to send. type captureMailer struct { mu sync.Mutex last map[string]string } func newCaptureMailer() *captureMailer { return &captureMailer{last: map[string]string{}} } func (m *captureMailer) SendCode(_ context.Context, to, code string) error { m.mu.Lock() defer m.mu.Unlock() m.last[to] = code return nil } func (m *captureMailer) SendAlreadyRegistered(_ context.Context, _ string) error { return nil } func (m *captureMailer) codeFor(to string) string { m.mu.Lock() defer m.mu.Unlock() return m.last[to] }