package api import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/go-chi/chi/v5" "git.gophernest.net/azpect/ResumeLens/internal/handlers" "git.gophernest.net/azpect/ResumeLens/internal/models" ) func makeAnalyzeRequest(t *testing.T, ip string) *http.Request { t.Helper() body := &bytes.Buffer{} w := multipart.NewWriter(body) part, err := w.CreateFormFile("resume", "resume.pdf") if err != nil { t.Fatalf("create form file: %v", err) } if _, err := part.Write([]byte("fake-pdf")); err != nil { t.Fatalf("write form file: %v", err) } if err := w.WriteField("job_description", "Go role"); err != nil { t.Fatalf("write field: %v", err) } if err := w.Close(); err != nil { t.Fatalf("close writer: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/analyze", body) req.Header.Set("Content-Type", w.FormDataContentType()) req.RemoteAddr = ip + ":12345" return req } func TestE2E_8_2_6_RateLimitEnforcementScenario(t *testing.T) { resetRateLimiter() restore := handlers.SetAnalyzeResumeForTesting(func(_ io.Reader, _ string) (*models.AnalysisResult, error) { return &models.AnalysisResult{OverallScore: 80, Summary: "ok"}, nil }) t.Cleanup(restore) r := chi.NewRouter() Mount(r) for i := 0; i < 10; i++ { rr := httptest.NewRecorder() r.ServeHTTP(rr, makeAnalyzeRequest(t, "127.0.0.1")) if rr.Code != http.StatusOK { t.Fatalf("request %d expected 200, got %d", i+1, rr.Code) } } blocked := httptest.NewRecorder() r.ServeHTTP(blocked, makeAnalyzeRequest(t, "127.0.0.1")) if blocked.Code != http.StatusTooManyRequests { t.Fatalf("expected 11th request to be 429, got %d", blocked.Code) } } func TestE2E_8_3_2_NonAIEndpointsFast(t *testing.T) { resetRateLimiter() r := chi.NewRouter() Mount(r) start := time.Now() req := httptest.NewRequest(http.MethodGet, "/api/unknown", nil) rr := httptest.NewRecorder() r.ServeHTTP(rr, req) dur := time.Since(start) if rr.Code != http.StatusNotFound { t.Fatalf("expected 404 for unknown endpoint, got %d", rr.Code) } if dur >= time.Second { t.Fatalf("expected non-AI endpoint under 1 second, got %v", dur) } } func TestEdge_10_4_1_MultipleSimultaneousRequestsDifferentIPs(t *testing.T) { resetRateLimiter() restore := handlers.SetAnalyzeResumeForTesting(func(_ io.Reader, _ string) (*models.AnalysisResult, error) { return &models.AnalysisResult{OverallScore: 80, Summary: "ok"}, nil }) t.Cleanup(restore) r := chi.NewRouter() Mount(r) var wg sync.WaitGroup results := make(chan int, 50) for i := 0; i < 50; i++ { wg.Add(1) ip := fmt.Sprintf("10.0.0.%d", i+1) go func(clientIP string) { defer wg.Done() rr := httptest.NewRecorder() r.ServeHTTP(rr, makeAnalyzeRequest(t, clientIP)) results <- rr.Code }(ip) } wg.Wait() close(results) for code := range results { if code != http.StatusOK { t.Fatalf("expected all concurrent different-IP requests to succeed, got status %d", code) } } } func TestEdge_10_4_2_RateLimiterThreadSafetySameIP(t *testing.T) { resetRateLimiter() restore := handlers.SetAnalyzeResumeForTesting(func(_ io.Reader, _ string) (*models.AnalysisResult, error) { return &models.AnalysisResult{OverallScore: 80, Summary: "ok"}, nil }) t.Cleanup(restore) r := chi.NewRouter() Mount(r) var wg sync.WaitGroup results := make(chan int, 20) for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() rr := httptest.NewRecorder() r.ServeHTTP(rr, makeAnalyzeRequest(t, "127.0.0.1")) results <- rr.Code }() } wg.Wait() close(results) okCount := 0 blockedCount := 0 for code := range results { switch code { case http.StatusOK: okCount++ case http.StatusTooManyRequests: blockedCount++ default: t.Fatalf("unexpected status code: %d", code) } } if okCount != 10 || blockedCount != 10 { t.Fatalf("expected 10 success and 10 blocked, got %d success and %d blocked", okCount, blockedCount) } }