164 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|