pkg-proxy/internal/server/mirror_api_test.go
Andrew Nesbitt 02738651ab
Fix concurrency, resource, and reliability issues in mirror
- Wire job contexts to server shutdown context so jobs are canceled on
  server stop instead of running indefinitely
- Defer context cancel in runJob so completed jobs don't leak contexts
- Cap error accumulation in progressTracker to 1000 entries to prevent
  OOM on large mirror operations with many failures
- Add panic recovery in errgroup workers to prevent process crashes
- Use defer for db.Close() in runMirror CLI to ensure cleanup on all
  error paths
2026-04-13 09:01:04 +01:00

163 lines
4.1 KiB
Go

package server
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/mirror"
"github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/registries/fetch"
"github.com/go-chi/chi/v5"
)
func setupMirrorAPI(t *testing.T) *MirrorAPIHandler {
t.Helper()
dbPath := t.TempDir() + "/test.db"
db, err := database.Create(dbPath)
if err != nil {
t.Fatalf("creating database: %v", err)
}
if err := db.MigrateSchema(); err != nil {
t.Fatalf("migrating schema: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
storeDir := t.TempDir()
store, err := storage.OpenBucket(context.Background(), "file://"+storeDir)
if err != nil {
t.Fatalf("opening storage: %v", err)
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
fetcher := fetch.NewFetcher()
resolver := fetch.NewResolver()
proxy := handler.NewProxy(db, store, fetcher, resolver, logger)
m := mirror.New(proxy, db, store, logger, 1)
js := mirror.NewJobStore(context.Background(), m)
return NewMirrorAPIHandler(js)
}
func TestMirrorAPICreateJob(t *testing.T) {
h := setupMirrorAPI(t)
body, _ := json.Marshal(mirror.JobRequest{
PURLs: []string{"pkg:npm/lodash@4.17.21"},
})
req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
w := httptest.NewRecorder()
h.HandleCreate(w, req)
if w.Code != http.StatusAccepted {
t.Errorf("status = %d, want %d", w.Code, http.StatusAccepted)
}
var resp map[string]string
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("decoding response: %v", err)
}
if resp["id"] == "" {
t.Error("expected non-empty job ID")
}
}
func TestMirrorAPICreateInvalidBody(t *testing.T) {
h := setupMirrorAPI(t)
req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader([]byte("not json")))
w := httptest.NewRecorder()
h.HandleCreate(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestMirrorAPICreateEmptyRequest(t *testing.T) {
h := setupMirrorAPI(t)
body, _ := json.Marshal(mirror.JobRequest{})
req := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
w := httptest.NewRecorder()
h.HandleCreate(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestMirrorAPIGetNotFound(t *testing.T) {
h := setupMirrorAPI(t)
r := chi.NewRouter()
r.Get("/api/mirror/{id}", h.HandleGet)
req := httptest.NewRequest("GET", "/api/mirror/nonexistent", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
}
func TestMirrorAPICancelNotFound(t *testing.T) {
h := setupMirrorAPI(t)
r := chi.NewRouter()
r.Delete("/api/mirror/{id}", h.HandleCancel)
req := httptest.NewRequest("DELETE", "/api/mirror/nonexistent", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
}
func TestMirrorAPICreateAndGetJob(t *testing.T) {
h := setupMirrorAPI(t)
// Create a job
body, _ := json.Marshal(mirror.JobRequest{
PURLs: []string{"pkg:npm/lodash@4.17.21"},
})
createReq := httptest.NewRequest("POST", "/api/mirror", bytes.NewReader(body))
createW := httptest.NewRecorder()
h.HandleCreate(createW, createReq)
var createResp map[string]string
_ = json.NewDecoder(createW.Body).Decode(&createResp)
jobID := createResp["id"]
// Get the job
r := chi.NewRouter()
r.Get("/api/mirror/{id}", h.HandleGet)
getReq := httptest.NewRequest("GET", "/api/mirror/"+jobID, nil)
getW := httptest.NewRecorder()
r.ServeHTTP(getW, getReq)
if getW.Code != http.StatusOK {
t.Errorf("status = %d, want %d", getW.Code, http.StatusOK)
}
var job mirror.Job
if err := json.NewDecoder(getW.Body).Decode(&job); err != nil {
t.Fatalf("decoding job: %v", err)
}
if job.ID != jobID {
t.Errorf("job ID = %q, want %q", job.ID, jobID)
}
}