package handler import ( "database/sql" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/storage" "github.com/git-pkgs/purl" "github.com/git-pkgs/registries/fetch" ) // seedPackageWithPURL seeds a package using purl.MakePURLString for PURL generation, // matching how the handlers construct PURLs internally. func seedPackageWithPURL(t *testing.T, db *database.DB, store *mockStorage, ecosystem, name, version, filename, content string) { t.Helper() pkgPURL := purl.MakePURLString(ecosystem, name, "") versionPURL := purl.MakePURLString(ecosystem, name, version) pkg := &database.Package{ PURL: pkgPURL, Ecosystem: ecosystem, Name: name, } if err := db.UpsertPackage(pkg); err != nil { t.Fatalf("failed to upsert package: %v", err) } ver := &database.Version{ PURL: versionPURL, PackagePURL: pkgPURL, } if err := db.UpsertVersion(ver); err != nil { t.Fatalf("failed to upsert version: %v", err) } storagePath := storage.ArtifactPath(ecosystem, "", name, version, filename) store.files[storagePath] = []byte(content) art := &database.Artifact{ VersionPURL: versionPURL, Filename: filename, UpstreamURL: "https://example.com/" + filename, StoragePath: sql.NullString{String: storagePath, Valid: true}, ContentHash: sql.NullString{String: "abc123", Valid: true}, Size: sql.NullInt64{Int64: int64(len(content)), Valid: true}, ContentType: sql.NullString{String: "application/octet-stream", Valid: true}, FetchedAt: sql.NullTime{Time: time.Now(), Valid: true}, } if err := db.UpsertArtifact(art); err != nil { t.Fatalf("failed to upsert artifact: %v", err) } } // assertUpstreamProxied verifies that a handler proxies a request to the upstream // server and returns the expected response body. The makeHandler function receives // a configured Proxy and the upstream URL, and returns the handler to test. func assertUpstreamProxied(t *testing.T, wantBody, path string, makeHandler func(*Proxy, string) http.Handler) { t.Helper() upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, wantBody) })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(makeHandler(proxy, upstream.URL)) defer srv.Close() resp, err := http.Get(srv.URL + path) if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != wantBody { t.Errorf("body = %q, want %q", body, wantBody) } } func TestGemHandler_DownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackage(t, db, store, "gem", "rails", "7.1.0", "rails-7.1.0.gem", "gem binary data") h := NewGemHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/gems/rails-7.1.0.gem") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "gem binary data" { t.Errorf("body = %q, want %q", body, "gem binary data") } } func TestGemHandler_DownloadCacheHitMultiHyphen(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackage(t, db, store, "gem", "aws-sdk-s3", "1.142.0", "aws-sdk-s3-1.142.0.gem", "aws gem") h := NewGemHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/gems/aws-sdk-s3-1.142.0.gem") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "aws gem" { t.Errorf("body = %q, want %q", body, "aws gem") } } func TestGemHandler_InvalidFilename(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewGemHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() tests := []struct { path string code int }{ {"/gems/notagem.tar.gz", http.StatusBadRequest}, {"/gems/noversion.gem", http.StatusBadRequest}, {"/gems/.gem", http.StatusBadRequest}, } for _, tt := range tests { resp, err := http.Get(srv.URL + tt.path) if err != nil { t.Fatalf("request to %s failed: %v", tt.path, err) } _ = resp.Body.Close() if resp.StatusCode != tt.code { t.Errorf("GET %s: status = %d, want %d", tt.path, resp.StatusCode, tt.code) } } } func TestGemHandler_UpstreamProxy(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Test", "upstream") w.WriteHeader(http.StatusOK) _, _ = fmt.Fprint(w, "upstream specs data") })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) h := &GemHandler{ proxy: proxy, upstreamURL: upstream.URL, proxyURL: "http://localhost", } proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/versions") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "upstream specs data" { t.Errorf("body = %q, want %q", body, "upstream specs data") } // Metadata caching reads the response body into storage and serves it back, // so arbitrary upstream headers are not forwarded. Content-Type is preserved. } func TestGemHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched gem")), ContentType: "application/octet-stream", } h := NewGemHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/gems/sinatra-3.0.0.gem") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } } func TestGoHandler_DownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackage(t, db, store, "golang", "golang.org/x/text", "v0.14.0", "text@v0.14.0.zip", "go module zip") h := NewGoHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/golang.org/x/text/@v/v0.14.0.zip") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "go module zip" { t.Errorf("body = %q, want %q", body, "go module zip") } } func TestGoHandler_MethodNotAllowed(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewGoHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Post(srv.URL+"/golang.org/x/text/@v/v0.14.0.zip", "", nil) if err != nil { t.Fatalf("request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusMethodNotAllowed { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusMethodNotAllowed) } } func TestGoHandler_NotFound(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewGoHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/some/unknown/path") if err != nil { t.Fatalf("request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) } } func TestGoHandler_UnknownAtVSuffix(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewGoHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/golang.org/x/text/@v/v0.14.0.unknown") if err != nil { t.Fatalf("request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) } } func TestGoHandler_UpstreamProxy(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, "v0.14.0\nv0.13.0\n") })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) h := &GoHandler{ proxy: proxy, upstreamURL: upstream.URL, proxyURL: "http://localhost", } proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) defer srv.Close() tests := []string{ "/golang.org/x/text/@v/list", "/golang.org/x/text/@v/v0.14.0.info", "/golang.org/x/text/@v/v0.14.0.mod", "/golang.org/x/text/@latest", "/sumdb/sum.golang.org/lookup/golang.org/x/text@v0.14.0", } for _, path := range tests { resp, err := http.Get(srv.URL + path) if err != nil { t.Fatalf("GET %s failed: %v", path, err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("GET %s: status = %d, want %d", path, resp.StatusCode, http.StatusOK) } } } func TestGoHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("module zip data")), ContentType: "application/zip", } h := NewGoHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/example.com/mod/@v/v1.0.0.zip") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } } func TestHexHandler_DownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackage(t, db, store, "hex", "phoenix", "1.7.10", "phoenix-1.7.10.tar", "hex tarball") h := NewHexHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/tarballs/phoenix-1.7.10.tar") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "hex tarball" { t.Errorf("body = %q, want %q", body, "hex tarball") } } func TestHexHandler_InvalidFilename(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewHexHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() tests := []struct { path string code int }{ {"/tarballs/notatar.zip", http.StatusBadRequest}, {"/tarballs/noversion.tar", http.StatusBadRequest}, } for _, tt := range tests { resp, err := http.Get(srv.URL + tt.path) if err != nil { t.Fatalf("request to %s failed: %v", tt.path, err) } _ = resp.Body.Close() if resp.StatusCode != tt.code { t.Errorf("GET %s: status = %d, want %d", tt.path, resp.StatusCode, tt.code) } } } func TestHexHandler_UpstreamProxy(t *testing.T) { assertUpstreamProxied(t, "hex registry data", "/packages/phoenix", func(proxy *Proxy, upstreamURL string) http.Handler { h := &HexHandler{proxy: proxy, upstreamURL: upstreamURL, proxyURL: "http://localhost"} return h.Routes() }, ) } func TestHexHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched hex")), ContentType: "application/x-tar", } h := NewHexHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/tarballs/plug-1.15.0.tar") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } } func TestCondaHandler_DownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackageWithPURL(t, db, store, "conda", "main/numpy", "1.24.0", "numpy-1.24.0-py311h64a7726_0.conda", "conda pkg") h := NewCondaHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/main/linux-64/numpy-1.24.0-py311h64a7726_0.conda") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "conda pkg" { t.Errorf("body = %q, want %q", body, "conda pkg") } } func TestCondaHandler_DownloadTarBz2CacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackageWithPURL(t, db, store, "conda", "main/scipy", "1.11.0", "scipy-1.11.0-py311hb2e3ea1_0.tar.bz2", "tar bz2 data") h := NewCondaHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/main/linux-64/scipy-1.11.0-py311hb2e3ea1_0.tar.bz2") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "tar bz2 data" { t.Errorf("body = %q, want %q", body, "tar bz2 data") } } func TestCondaHandler_NonPackageFileProxied(t *testing.T) { assertUpstreamProxied(t, "repodata json", "/main/linux-64/repodata.json", func(proxy *Proxy, upstreamURL string) http.Handler { h := &CondaHandler{proxy: proxy, upstreamURL: upstreamURL, proxyURL: "http://localhost"} return h.Routes() }, ) } func TestCondaHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched conda")), ContentType: "application/octet-stream", } h := NewCondaHandler(proxy, "http://localhost") upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("should not hit upstream for .conda files when fetcher is set") })) defer upstream.Close() h.upstreamURL = upstream.URL proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/conda-forge/linux-64/pandas-2.0.0-py311h320fe9a_0.conda") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := upstream.URL + "/conda-forge/linux-64/pandas-2.0.0-py311h320fe9a_0.conda" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestCRANHandler_SourceDownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackageWithPURL(t, db, store, "cran", "ggplot2", "3.4.0", "ggplot2_3.4.0.tar.gz", "cran source") h := NewCRANHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/src/contrib/ggplot2_3.4.0.tar.gz") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "cran source" { t.Errorf("body = %q, want %q", body, "cran source") } } func TestCRANHandler_BinaryDownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackageWithPURL(t, db, store, "cran", "dplyr", "1.1.0_windows_4.3", "dplyr_1.1.0.zip", "cran binary") h := NewCRANHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/bin/windows/contrib/4.3/dplyr_1.1.0.zip") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "cran binary" { t.Errorf("body = %q, want %q", body, "cran binary") } } func TestCRANHandler_NonPackageFileProxied(t *testing.T) { assertUpstreamProxied(t, "PACKAGES index", "/src/contrib/PACKAGES", func(proxy *Proxy, upstreamURL string) http.Handler { h := &CRANHandler{proxy: proxy, upstreamURL: upstreamURL, proxyURL: "http://localhost"} return h.Routes() }, ) } func TestCRANHandler_SourceNonTarGzProxied(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, "some other file") })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) h := &CRANHandler{ proxy: proxy, upstreamURL: upstream.URL, proxyURL: "http://localhost", } proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/src/contrib/somefile.txt") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } } func TestCRANHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched cran")), ContentType: "application/x-gzip", } h := NewCRANHandler(proxy, "http://localhost") h.upstreamURL = "https://cran.r-project.org" srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/src/contrib/tidyr_1.3.0.tar.gz") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://cran.r-project.org/src/contrib/tidyr_1.3.0.tar.gz" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestCRANHandler_BinaryDownloadCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched binary")), ContentType: "application/zip", } h := NewCRANHandler(proxy, "http://localhost") h.upstreamURL = "https://cran.r-project.org" srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/bin/windows/contrib/4.3/dplyr_1.1.0.zip") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://cran.r-project.org/bin/windows/contrib/4.3/dplyr_1.1.0.zip" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestMavenHandler_DownloadCacheHit(t *testing.T) { proxy, db, store, _ := setupTestProxy(t) seedPackageWithPURL(t, db, store, "maven", "com.google.guava:guava", "32.1.3-jre", "guava-32.1.3-jre.jar", "jar content") h := NewMavenHandler(proxy, "http://localhost", "", "") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } body, _ := io.ReadAll(resp.Body) if string(body) != "jar content" { t.Errorf("body = %q, want %q", body, "jar content") } } func TestMavenHandler_MetadataProxied(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, "") })) defer upstream.Close() proxy, _, _, _ := setupTestProxy(t) h := &MavenHandler{ proxy: proxy, upstreamURL: upstream.URL, proxyURL: "http://localhost", } proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) defer srv.Close() paths := []string{ "/com/google/guava/guava/maven-metadata.xml", "/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.sha1", "/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.md5", } for _, path := range paths { resp, err := http.Get(srv.URL + path) if err != nil { t.Fatalf("GET %s failed: %v", path, err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("GET %s: status = %d, want %d", path, resp.StatusCode, http.StatusOK) } } } func TestMavenHandler_EmptyPathNotFound(t *testing.T) { proxy, _, _, _ := setupTestProxy(t) h := NewMavenHandler(proxy, "http://localhost", "", "") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/") if err != nil { t.Fatalf("request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) } } func TestMavenHandler_ArtifactExtensions(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) extensions := []string{".jar", ".war", ".ear", ".pom", ".aar", ".klib", ".module"} for _, ext := range extensions { fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("artifact")), ContentType: "application/java-archive", } fetcher.fetchCalled = false h := NewMavenHandler(proxy, "http://localhost", "", "") upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("should not proxy artifact file %s to upstream", ext) })) h.upstreamURL = upstream.URL proxy.HTTPClient = upstream.Client() srv := httptest.NewServer(h.Routes()) path := fmt.Sprintf("/com/example/lib/1.0/lib-1.0%s", ext) resp, err := http.Get(srv.URL + path) if err != nil { t.Fatalf("GET %s failed: %v", path, err) } _ = resp.Body.Close() if !fetcher.fetchCalled { t.Errorf("fetcher not called for %s", ext) } srv.Close() upstream.Close() } } func TestMavenHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched jar")), ContentType: "application/java-archive", } h := NewMavenHandler(proxy, "http://localhost", "", "") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestMavenHandler_GradlePluginMarkerFallbackAndCache(t *testing.T) { tests := []struct { name string markerPath string }{ { name: "Spotless", markerPath: "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom", }, { name: "BenManes", markerPath: "/com/github/ben-manes/versions/com.github.ben-manes.versions.gradle.plugin/0.54.0/com.github.ben-manes.versions.gradle.plugin-0.54.0.pom", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) primaryUpstream := "https://repo1.maven.org/maven2" pluginPortalUpstream := "https://plugins.gradle.org/m2" primaryURL := primaryUpstream + tt.markerPath fetcher.fetchErrByURL = map[string]error{ primaryURL: ErrUpstreamNotFound, } fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("")), ContentType: "application/xml", } h := NewMavenHandler(proxy, "http://localhost", primaryUpstream, pluginPortalUpstream) srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + tt.markerPath) if err != nil { t.Fatalf("request failed: %v", err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) } if string(body) != "" { t.Fatalf("body = %q, want %q", body, "") } wantFallbackURL := pluginPortalUpstream + tt.markerPath if fetcher.fetchedURL != wantFallbackURL { t.Fatalf("fallback URL = %q, want %q", fetcher.fetchedURL, wantFallbackURL) } fetcher.fetchCalled = false resp, err = http.Get(srv.URL + tt.markerPath) if err != nil { t.Fatalf("second request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("second status = %d, want %d", resp.StatusCode, http.StatusOK) } if fetcher.fetchCalled { t.Fatal("expected plugin marker POM to be served from cache on second request") } }) } } func TestMavenHandler_GradlePluginMarkerMetadataFallback(t *testing.T) { paths := map[string]string{ "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1": "sha1", "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha256": "sha256", "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.md5": "md5", "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml": "", } primaryHits := map[string]int{} pluginHits := map[string]int{} primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { primaryHits[r.URL.Path]++ if _, ok := paths[r.URL.Path]; ok { http.NotFound(w, r) return } t.Fatalf("unexpected path to primary upstream: %s", r.URL.Path) })) defer primary.Close() pluginPortal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pluginHits[r.URL.Path]++ body, ok := paths[r.URL.Path] if !ok { http.NotFound(w, r) return } _, _ = io.WriteString(w, body) })) defer pluginPortal.Close() proxy, _, _, _ := setupTestProxy(t) proxy.HTTPClient = primary.Client() h := NewMavenHandler(proxy, "http://localhost", primary.URL, pluginPortal.URL) srv := httptest.NewServer(h.Routes()) defer srv.Close() for reqPath, wantBody := range paths { resp, err := http.Get(srv.URL + reqPath) if err != nil { t.Fatalf("GET %s failed: %v", reqPath, err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET %s: status = %d, want %d", reqPath, resp.StatusCode, http.StatusOK) } if string(body) != wantBody { t.Fatalf("GET %s: body = %q, want %q", reqPath, body, wantBody) } if primaryHits[reqPath] == 0 { t.Fatalf("GET %s did not hit primary upstream", reqPath) } if pluginHits[reqPath] == 0 { t.Fatalf("GET %s did not hit plugin portal fallback", reqPath) } } } func TestMavenHandler_GradlePluginImplementationMetadataFallback(t *testing.T) { paths := map[string]string{ "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha1": "impl-sha1", "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha256": "impl-sha256", } primaryHits := map[string]int{} pluginHits := map[string]int{} primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { primaryHits[r.URL.Path]++ if _, ok := paths[r.URL.Path]; ok { http.NotFound(w, r) return } t.Fatalf("unexpected path to primary upstream: %s", r.URL.Path) })) defer primary.Close() pluginPortal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pluginHits[r.URL.Path]++ body, ok := paths[r.URL.Path] if !ok { http.NotFound(w, r) return } _, _ = io.WriteString(w, body) })) defer pluginPortal.Close() proxy, _, _, _ := setupTestProxy(t) proxy.HTTPClient = primary.Client() h := NewMavenHandler(proxy, "http://localhost", primary.URL, pluginPortal.URL) srv := httptest.NewServer(h.Routes()) defer srv.Close() for reqPath, wantBody := range paths { resp, err := http.Get(srv.URL + reqPath) if err != nil { t.Fatalf("GET %s failed: %v", reqPath, err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET %s: status = %d, want %d", reqPath, resp.StatusCode, http.StatusOK) } if string(body) != wantBody { t.Fatalf("GET %s: body = %q, want %q", reqPath, body, wantBody) } if primaryHits[reqPath] == 0 { t.Fatalf("GET %s did not hit primary upstream", reqPath) } if pluginHits[reqPath] == 0 { t.Fatalf("GET %s did not hit plugin portal fallback", reqPath) } } } func TestMavenHandler_GradlePluginImplementation_FallbackToPluginPortal(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) primaryUpstream := "https://repo1.maven.org/maven2" pluginPortalUpstream := "https://plugins.gradle.org/m2" implPath := "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar" primaryURL := primaryUpstream + implPath pluginPortalURL := pluginPortalUpstream + implPath fetcher.fetchErrByURL = map[string]error{ primaryURL: ErrUpstreamNotFound, } fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("plugin impl jar")), ContentType: "application/java-archive", } h := NewMavenHandler(proxy, "http://localhost", primaryUpstream, pluginPortalUpstream) srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + implPath) if err != nil { t.Fatalf("request failed: %v", err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) } if string(body) != "plugin impl jar" { t.Fatalf("body = %q, want %q", body, "plugin impl jar") } if fetcher.fetchedURL != pluginPortalURL { t.Fatalf("implementation artifact should fallback to plugin portal; fetched URL = %q, want %q", fetcher.fetchedURL, pluginPortalURL) } } func TestMavenHandler_GradlePluginImplementation_NotFoundInBothUpstreams(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) primaryUpstream := "https://repo1.maven.org/maven2" pluginPortalUpstream := "https://plugins.gradle.org/m2" implPath := "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar" primaryURL := primaryUpstream + implPath pluginPortalURL := pluginPortalUpstream + implPath fetcher.fetchErrByURL = map[string]error{ primaryURL: ErrUpstreamNotFound, pluginPortalURL: ErrUpstreamNotFound, } h := NewMavenHandler(proxy, "http://localhost", primaryUpstream, pluginPortalUpstream) srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + implPath) if err != nil { t.Fatalf("request failed: %v", err) } _ = resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) } if fetcher.fetchedURL != pluginPortalURL { t.Fatalf("expected fallback attempt to plugin portal; fetched URL = %q, want %q", fetcher.fetchedURL, pluginPortalURL) } } func TestNuGetHandler_DownloadCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched nupkg")), ContentType: "application/octet-stream", } h := NewNuGetHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestConanHandler_RecipeFileCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("conan export")), ContentType: "application/octet-stream", } h := NewConanHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/v2/files/zlib/1.3/_/_/abc123/recipe/conan_export.tgz") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://center.conan.io/v2/files/zlib/1.3/_/_/abc123/recipe/conan_export.tgz" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestConanHandler_PackageFileCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("conan package")), ContentType: "application/octet-stream", } h := NewConanHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/v2/files/zlib/1.3/_/_/abc123/package/def456/ghi789/conan_package.tgz") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://center.conan.io/v2/files/zlib/1.3/_/_/abc123/package/def456/ghi789/conan_package.tgz" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestDebianHandler_DownloadCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched deb")), ContentType: "application/vnd.debian.binary-package", } h := NewDebianHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/pool/main/n/nginx/nginx_1.18.0-6_amd64.deb") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "http://deb.debian.org/debian/pool/main/n/nginx/nginx_1.18.0-6_amd64.deb" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } } func TestRPMHandler_DownloadCacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ Body: io.NopCloser(strings.NewReader("fetched rpm")), ContentType: "application/x-rpm", } h := NewRPMHandler(proxy, "http://localhost") srv := httptest.NewServer(h.Routes()) defer srv.Close() resp, err := http.Get(srv.URL + "/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm") if err != nil { t.Fatalf("request failed: %v", err) } defer func() { _ = resp.Body.Close() }() if !fetcher.fetchCalled { t.Error("expected fetcher to be called on cache miss") } want := "https://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm" if fetcher.fetchedURL != want { t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want) } }