mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
1405 lines
44 KiB
Go
1405 lines
44 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
"github.com/git-pkgs/proxy/internal/httpclient"
|
|
"github.com/git-pkgs/proxy/internal/metrics"
|
|
"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)
|
|
sharedArtifact := testArtifact(content, versionPURL, filename, "application/octet-stream")
|
|
|
|
art := &database.Artifact{
|
|
VersionPURL: versionPURL,
|
|
Filename: filename,
|
|
UpstreamURL: "https://example.com/" + filename,
|
|
StoragePath: sql.NullString{String: storagePath, Valid: true},
|
|
ContentHash: sql.NullString{String: sharedArtifact.Digest.Encoded(), 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)
|
|
fetchesBefore := histogramSampleCount(t, metrics.UpstreamFetchDuration.WithLabelValues("gem"))
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("fetched gem")),
|
|
ContentType: "application/octet-stream",
|
|
}
|
|
|
|
upstreamURL := "https://packages.example.com/gem"
|
|
h := NewGemHandlerWithUpstream(proxy, "http://localhost", upstreamURL)
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
path := "/gems/sinatra-3.0.0.gem"
|
|
resp, err := http.Get(srv.URL + path)
|
|
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")
|
|
}
|
|
if want := upstreamURL + path; fetcher.fetchedURL != want {
|
|
t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want)
|
|
}
|
|
if diff := histogramSampleCount(t, metrics.UpstreamFetchDuration.WithLabelValues("gem")) - fetchesBefore; diff != 1 {
|
|
t.Errorf("upstream fetch observations delta = %d, want 1", diff)
|
|
}
|
|
}
|
|
|
|
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",
|
|
}
|
|
|
|
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",
|
|
}
|
|
|
|
upstreamURL := "https://packages.example.com/go"
|
|
h := NewGoHandlerWithUpstream(proxy, "http://localhost", upstreamURL)
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
path := "/example.com/mod/@v/v1.0.0.zip"
|
|
resp, err := http.Get(srv.URL + path)
|
|
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")
|
|
}
|
|
if want := upstreamURL + path; fetcher.fetchedURL != want {
|
|
t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want)
|
|
}
|
|
}
|
|
|
|
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",
|
|
}
|
|
|
|
upstreamURL := "https://packages.example.com/hex"
|
|
h := NewHexHandlerWithUpstreams(proxy, "http://localhost", upstreamURL, "https://packages.example.com/hex-api")
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
path := "/tarballs/plug-1.15.0.tar"
|
|
resp, err := http.Get(srv.URL + path)
|
|
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")
|
|
}
|
|
if want := upstreamURL + path; fetcher.fetchedURL != want {
|
|
t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want)
|
|
}
|
|
}
|
|
|
|
func TestPubHandler_CacheMiss(t *testing.T) {
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("fetched pub package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
|
|
upstreamURL := "https://packages.example.com/pub"
|
|
h := NewPubHandlerWithUpstream(proxy, "http://localhost", upstreamURL)
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
path := "/packages/flutter_bloc/versions/8.1.6.tar.gz"
|
|
resp, err := http.Get(srv.URL + path)
|
|
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")
|
|
}
|
|
if want := upstreamURL + path; fetcher.fetchedURL != want {
|
|
t.Errorf("upstream URL = %q, want %q", fetcher.fetchedURL, want)
|
|
}
|
|
}
|
|
|
|
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, "<metadata/>")
|
|
}))
|
|
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/maven-metadata.xml.sha1",
|
|
"/com/google/guava/guava/1.0-SNAPSHOT/maven-metadata.xml",
|
|
}
|
|
|
|
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_ChecksumsCachedAsArtifacts(t *testing.T) {
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Errorf("checksum %s proxied as metadata", r.URL.Path)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
|
h.upstreamURL = upstream.URL
|
|
proxy.HTTPClient = upstream.Client()
|
|
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
for _, name := range []string{"lib-1.0.jar.sha1", "lib-1.0.pom.md5", "lib-1.0.jar.asc", "lib-1.0-bin.tar.gz.sha512"} {
|
|
path := "/com/example/lib/1.0/" + name
|
|
for i := range 2 {
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("checksum")),
|
|
ContentType: "text/plain",
|
|
}
|
|
fetcher.fetchCalled = false
|
|
|
|
resp, err := http.Get(srv.URL + path)
|
|
if err != nil {
|
|
t.Fatalf("GET %s failed: %v", path, err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK || string(body) != "checksum" {
|
|
t.Errorf("GET %s: status %d body %q", path, resp.StatusCode, body)
|
|
}
|
|
if fetcher.fetchCalled != (i == 0) {
|
|
t.Errorf("GET %s request %d: fetched = %v, want %v", path, i+1, fetcher.fetchCalled, i == 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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("<project/>")),
|
|
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) != "<project/>" {
|
|
t.Fatalf("body = %q, want %q", body, "<project/>")
|
|
}
|
|
|
|
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/maven-metadata.xml": "<metadata/>",
|
|
"/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml.sha1": "sha1",
|
|
}
|
|
|
|
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_GradlePluginChecksumFallbackToPluginPortal(t *testing.T) {
|
|
primaryUpstream := "https://repo1.maven.org/maven2"
|
|
pluginPortalUpstream := "https://plugins.gradle.org/m2"
|
|
paths := []string{
|
|
"/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1",
|
|
"/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.md5",
|
|
"/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha1",
|
|
"/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha256",
|
|
}
|
|
|
|
for _, reqPath := range paths {
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
fetcher.fetchErrByURL = map[string]error{primaryUpstream + reqPath: ErrUpstreamNotFound}
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("checksum")),
|
|
ContentType: "text/plain",
|
|
}
|
|
|
|
h := NewMavenHandler(proxy, "http://localhost", primaryUpstream, pluginPortalUpstream)
|
|
srv := httptest.NewServer(h.Routes())
|
|
|
|
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()
|
|
srv.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK || string(body) != "checksum" {
|
|
t.Fatalf("GET %s: status %d body %q", reqPath, resp.StatusCode, body)
|
|
}
|
|
if want := pluginPortalUpstream + reqPath; fetcher.fetchedURL != want {
|
|
t.Fatalf("GET %s: fetched URL = %q, want %q", reqPath, fetcher.fetchedURL, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
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/conans/zlib/1.3/_/_/revisions/abc123/files/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/conans/zlib/1.3/_/_/revisions/abc123/files/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/conans/zlib/1.3/_/_/revisions/abc123/packages/def456/revisions/ghi789/files/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/conans/zlib/1.3/_/_/revisions/abc123/packages/def456/revisions/ghi789/files/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", "", nil)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestMavenHandler_UpstreamHeldReturns503(t *testing.T) {
|
|
held := &httpclient.HeldError{Host: "repo1.maven.org", Until: time.Now().Add(90 * time.Second), RetryAfter: 90 * time.Second}
|
|
|
|
t.Run("artifact", func(t *testing.T) {
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
fetcher.fetchErr = fmt.Errorf("fetching artifact: %w", &url.Error{Op: "Get", URL: "https://repo1.maven.org/x", Err: held})
|
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/com/example/lib/1.0/lib-1.0.jar")
|
|
if err != nil {
|
|
t.Fatalf("request failed: %v", err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusServiceUnavailable || resp.Header.Get("Retry-After") != "90" {
|
|
t.Errorf("status %d Retry-After %q, want 503 with Retry-After 90", resp.StatusCode, resp.Header.Get("Retry-After"))
|
|
}
|
|
})
|
|
|
|
t.Run("metadata", func(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = &http.Client{Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, held
|
|
})}
|
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/com/example/lib/maven-metadata.xml")
|
|
if err != nil {
|
|
t.Fatalf("request failed: %v", err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusServiceUnavailable || resp.Header.Get("Retry-After") != "90" {
|
|
t.Errorf("status %d Retry-After %q, want 503 with Retry-After 90", resp.StatusCode, resp.Header.Get("Retry-After"))
|
|
}
|
|
})
|
|
}
|
|
|
|
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) }
|
|
|
|
func TestMavenHandler_HeldArtifactMissesSkipCircuitBreaker(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
upstreamCalls := 0
|
|
gate := httpclient.NewRateLimitGate(roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
upstreamCalls++
|
|
return &http.Response{
|
|
StatusCode: http.StatusTooManyRequests,
|
|
Header: http.Header{"Retry-After": []string{"600"}},
|
|
Body: http.NoBody,
|
|
Request: req,
|
|
}, nil
|
|
}))
|
|
req, _ := http.NewRequest(http.MethodGet, "https://repo1.maven.org/maven2/x", nil)
|
|
if resp, err := gate.RoundTrip(req); err == nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
breaker := fetch.NewCircuitBreakerFetcher(fetch.NewFetcher(fetch.WithHTTPClient(&http.Client{Transport: gate})))
|
|
proxy.Fetcher = breaker
|
|
proxy.RateLimits = gate
|
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
for i := range 8 {
|
|
resp, err := http.Get(fmt.Sprintf("%s/com/example/lib/1.%d/lib-1.%d.jar", srv.URL, i, i))
|
|
if err != nil {
|
|
t.Fatalf("request %d failed: %v", i, err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusServiceUnavailable || resp.Header.Get("Retry-After") == "" {
|
|
t.Fatalf("request %d: status %d Retry-After %q, want 503 with Retry-After", i, resp.StatusCode, resp.Header.Get("Retry-After"))
|
|
}
|
|
}
|
|
if upstreamCalls != 1 {
|
|
t.Errorf("upstream calls = %d, want only the one that started the hold", upstreamCalls)
|
|
}
|
|
if state := breaker.GetBreakerState()["repo1.maven.org"]; state == "open" {
|
|
t.Errorf("breaker for repo1.maven.org is open after held misses, want closed")
|
|
}
|
|
}
|
|
|
|
func TestHandlers_UpstreamHeldMetadataReturns503(t *testing.T) {
|
|
held := &httpclient.HeldError{Host: "upstream.example", Until: time.Now().Add(42 * time.Second), RetryAfter: 42 * time.Second}
|
|
tests := []struct {
|
|
name string
|
|
handler func(*Proxy) http.Handler
|
|
path string
|
|
}{
|
|
{"npm metadata", func(p *Proxy) http.Handler { return NewNPMHandler(p, "http://localhost", "").Routes() }, "/lodash"},
|
|
{"pypi simple", func(p *Proxy) http.Handler { return NewPyPIHandler(p, "http://localhost").Routes() }, "/simple/requests/"},
|
|
{"pypi json", func(p *Proxy) http.Handler { return NewPyPIHandler(p, "http://localhost").Routes() }, "/pypi/requests/json"},
|
|
{"cargo index", func(p *Proxy) http.Handler {
|
|
return NewCargoHandler(p, "http://localhost", "", "").Routes()
|
|
}, "/se/rd/serde"},
|
|
{"pub metadata", func(p *Proxy) http.Handler { return NewPubHandler(p, "http://localhost").Routes() }, "/api/packages/http"},
|
|
{"nuget service index", func(p *Proxy) http.Handler { return NewNuGetHandler(p, "http://localhost").Routes() }, "/v3/index.json"},
|
|
{"nuget search", func(p *Proxy) http.Handler { return NewNuGetHandler(p, "http://localhost").Routes() }, "/query?q=json"},
|
|
{"conda repodata", func(p *Proxy) http.Handler { return NewCondaHandler(p, "http://localhost").Routes() }, "/conda-forge/noarch/repodata.json"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = &http.Client{Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, held
|
|
})}
|
|
srv := httptest.NewServer(tt.handler(proxy))
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + tt.path)
|
|
if err != nil {
|
|
t.Fatalf("request failed: %v", err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode != http.StatusServiceUnavailable || resp.Header.Get("Retry-After") != "42" {
|
|
t.Errorf("status %d Retry-After %q, want 503 with Retry-After 42", resp.StatusCode, resp.Header.Get("Retry-After"))
|
|
}
|
|
})
|
|
}
|
|
}
|