ResumeLens/internal/api/integration_test.go
2026-04-07 13:14:52 -07:00

164 lines
3.9 KiB
Go

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)
}
}