package auth import ( "context" "image/png" "strings" "testing" "time" ) func TestRenderCaptchaPNG(t *testing.T) { pngBytes, err := RenderCaptchaPNG("4837") if err != nil { t.Fatalf("render: %v", err) } img, err := png.Decode(strings.NewReader(string(pngBytes))) if err != nil { t.Fatalf("decode png: %v", err) } if img.Bounds().Dx() < 50 || img.Bounds().Dy() < 20 { t.Fatalf("canvas too small: %dx%d", img.Bounds().Dx(), img.Bounds().Dy()) } } func TestRenderCaptchaPNGInvalid(t *testing.T) { if _, err := RenderCaptchaPNG("12a4"); err == nil { t.Fatal("expected error for non-digit text") } if _, err := RenderCaptchaPNG(""); err == nil { t.Fatal("expected error for empty text") } } func TestMemoryCaptchaStore(t *testing.T) { s := NewMemoryCaptchaStore(5 * time.Minute) cap, err := s.New() if err != nil { t.Fatalf("new: %v", err) } if len(cap.Text) != 4 { t.Fatalf("captcha text len = %d, want 4", len(cap.Text)) } if !s.Verify(cap.ID, cap.Text) { t.Fatal("verify should succeed") } if s.Verify(cap.ID, cap.Text) { t.Fatal("captcha must be one-time") } } func TestMemoryCaptchaStoreExpired(t *testing.T) { s := NewMemoryCaptchaStore(-time.Second) // 立即过期 cap, err := s.New() if err != nil { t.Fatalf("new: %v", err) } if s.Verify(cap.ID, cap.Text) { t.Fatal("expired captcha should fail") } } func TestRateLimiter(t *testing.T) { l := NewRateLimiter(3, time.Minute) key := "admin-login:admin" if !l.Allow(key) { t.Fatal("first attempt should be allowed") } for i := 0; i < 3; i++ { l.RecordFailure(key) } if l.Allow(key) { t.Fatal("should be locked after max failures") } l.Reset(key) if !l.Allow(key) { t.Fatal("should be allowed after reset") } } func TestDBResetTokenStore(t *testing.T) { db := testDB(t) s := NewDBResetTokenStore(db) ctx := context.Background() token, err := s.Create(ctx, 42, time.Minute, "127.0.0.1") if err != nil { t.Fatalf("create: %v", err) } if token == "" { t.Fatal("token should not be empty") } adminID, err := s.Consume(ctx, token) if err != nil { t.Fatalf("consume: %v", err) } if adminID != 42 { t.Fatalf("adminID = %d, want 42", adminID) } // 一次性 if _, err := s.Consume(ctx, token); err != ErrResetTokenInvalid { t.Fatalf("second consume err = %v, want ErrResetTokenInvalid", err) } // 无效令牌 if _, err := s.Consume(ctx, "not-a-token"); err != ErrResetTokenInvalid { t.Fatalf("bad token err = %v, want ErrResetTokenInvalid", err) } // 过期令牌 dbToken, err := s.Create(ctx, 7, -time.Minute, "") if err != nil { t.Fatalf("create expired: %v", err) } if _, err := s.Consume(ctx, dbToken); err != ErrResetTokenInvalid { t.Fatalf("expired token err = %v, want ErrResetTokenInvalid", err) } }