package handler import ( "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/git-pkgs/cooldown" "github.com/git-pkgs/registries/fetch" ) func cargoTestProxy() *Proxy { return &Proxy{ Logger: slog.Default(), HTTPClient: http.DefaultClient, } } func TestCargoBuildIndexPath(t *testing.T) { h := &CargoHandler{} tests := []struct { name string want string }{ {"a", "1/a"}, {"ab", "2/ab"}, {"abc", "3/a/abc"}, {"abcd", "ab/cd/abcd"}, {"serde", "se/rd/serde"}, {"tokio", "to/ki/tokio"}, {"A", "1/a"}, // lowercase {"SERDE", "se/rd/serde"}, // lowercase {"rand_core", "ra/nd/rand_core"}, } for _, tt := range tests { got := h.buildIndexPath(tt.name) if got != tt.want { t.Errorf("buildIndexPath(%q) = %q, want %q", tt.name, got, tt.want) } } } func TestCargoConfigEndpoint(t *testing.T) { h := &CargoHandler{ proxyURL: "http://localhost:8080", } req := httptest.NewRequest(http.MethodGet, "/config.json", nil) w := httptest.NewRecorder() h.handleConfig(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want %d", w.Code, http.StatusOK) } var config CargoConfig if err := json.Unmarshal(w.Body.Bytes(), &config); err != nil { t.Fatalf("failed to parse config: %v", err) } expectedDL := "http://localhost:8080/cargo/crates/{crate}/{version}/download" if config.DL != expectedDL { t.Errorf("DL = %q, want %q", config.DL, expectedDL) } } func TestCargoHandlerUsesConfiguredUpstreams(t *testing.T) { t.Run("index", func(t *testing.T) { var requestPath, authHeader string upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestPath = r.URL.Path authHeader = r.Header.Get("Authorization") if authHeader != "Bearer cargo-token" { w.WriteHeader(http.StatusUnauthorized) return } w.Header().Set("Content-Type", "text/plain") _, _ = io.WriteString(w, `{"name":"serde","vers":"1.0.0"}`) })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) proxy.HTTPClient = upstream.Client() proxy.AuthForURL = func(string) (string, string) { return "Authorization", "Bearer cargo-token" } h := NewCargoHandler( proxy, "http://proxy.test", upstream.URL+"/index/", "https://crates.example.test/files/", ) req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil) w := httptest.NewRecorder() h.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } if requestPath != "/index/se/rd/serde" { t.Errorf("upstream path = %q, want %q", requestPath, "/index/se/rd/serde") } if authHeader != "Bearer cargo-token" { t.Errorf("Authorization = %q, want %q", authHeader, "Bearer cargo-token") } }) t.Run("download", func(t *testing.T) { proxy, _, _, artifactFetcher := setupTestProxy(t) artifactFetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("crate")), ContentType: "application/gzip", } h := NewCargoHandler( proxy, "http://proxy.test", "https://index.example.test/root/", "https://crates.example.test/files/", ) req := httptest.NewRequest(http.MethodGet, "/crates/serde/1.0.0/download", nil) w := httptest.NewRecorder() h.Routes().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } want := "https://crates.example.test/files/serde/serde-1.0.0.crate" if artifactFetcher.fetchedURL != want { t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want) } }) } func TestCargoIndexProxy(t *testing.T) { // Create a mock upstream index server indexContent := `{"name":"serde","vers":"1.0.0","deps":[],"cksum":"abc123"} {"name":"serde","vers":"1.0.1","deps":[],"cksum":"def456"}` upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/se/rd/serde" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "text/plain") _, _ = w.Write([]byte(indexContent)) })) defer upstream.Close() h := &CargoHandler{ proxy: cargoTestProxy(), indexURL: upstream.URL, proxyURL: "http://proxy.local", } req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil) req.SetPathValue("a", "se") req.SetPathValue("b", "rd") req.SetPathValue("name", "serde") w := httptest.NewRecorder() h.handleIndex(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want %d", w.Code, http.StatusOK) } if w.Body.String() != indexContent { t.Errorf("body = %q, want %q", w.Body.String(), indexContent) } } func TestCargoIndexNotFound(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer upstream.Close() h := &CargoHandler{ proxy: cargoTestProxy(), indexURL: upstream.URL, proxyURL: "http://proxy.local", } req := httptest.NewRequest(http.MethodGet, "/no/ne/nonexistent", nil) req.SetPathValue("a", "no") req.SetPathValue("b", "ne") req.SetPathValue("name", "nonexistent") w := httptest.NewRecorder() h.handleIndex(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound) } } func TestCargoRoutes(t *testing.T) { h := &CargoHandler{ proxy: cargoTestProxy(), proxyURL: "http://proxy.local", } routes := h.Routes() // Test that config.json route exists req := httptest.NewRequest(http.MethodGet, "/config.json", nil) w := httptest.NewRecorder() routes.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("config.json status = %d, want %d", w.Code, http.StatusOK) } } type filterTestCase struct { line string expected bool } func TestCargoCooldown(t *testing.T) { now := time.Now() createCase := func(name string, version string, age time.Duration, expected bool) filterTestCase { return filterTestCase{line: `{"name":"` + name + `","vers":"` + version + `","cksum":"abcd","features":{},"yanked":false,"pubtime":"` + now.Add(-1*age).Format(time.RFC3339) + `"}`, expected: expected} } testCases := []filterTestCase{ // one week ago createCase("serde", "1.0.0", 168*time.Hour, true), // one hour ago createCase("serde", "1.0.1", 1*time.Hour, false), // two hours ago with custom filter (1h) createCase("tokio", "1.0.0", 2*time.Hour, true), // one hour ago with custom filter (1h) createCase("tokio", "1.0.0", 1*time.Minute, false), } var testInput strings.Builder var expectedOutput strings.Builder for _, testCase := range testCases { testInput.WriteString(testCase.line + "\n") if testCase.expected { expectedOutput.WriteString(testCase.line + "\n") } } proxy := testProxy() proxy.Cooldown = &cooldown.Config{ Default: "3d", Packages: map[string]string{"pkg:cargo/tokio": "1h"}, } h := &CargoHandler{ proxy: proxy, proxyURL: "http://localhost:8080", } recorder := httptest.NewRecorder() h.applyCooldownFiltering(recorder, []byte(testInput.String())) output := recorder.Body.String() if output != expectedOutput.String() { t.Errorf("output = %q, want %q", output, expectedOutput.String()) } }