Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
2026-03-18 10:59:29 +00:00
|
|
|
_, _ = fmt.Fprint(w, "upstream specs data")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}))
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
2026-03-19 21:06:02 +00:00
|
|
|
// Metadata caching reads the response body into storage and serves it back,
|
|
|
|
|
// so arbitrary upstream headers are not forwarded. Content-Type is preserved.
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
_, _ = fmt.Fprint(w, "v0.14.0\nv0.13.0\n")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}))
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
)
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
)
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
if !fetcher.fetchCalled {
|
|
|
|
|
t.Error("expected fetcher to be called on cache miss")
|
|
|
|
|
}
|
2026-04-01 15:22:52 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
)
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCRANHandler_SourceNonTarGzProxied(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2026-03-18 10:59:29 +00:00
|
|
|
_, _ = fmt.Fprint(w, "some other file")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}))
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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")
|
2026-04-01 15:22:52 +01:00
|
|
|
h.upstreamURL = "https://cran.r-project.org"
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
if !fetcher.fetchCalled {
|
|
|
|
|
t.Error("expected fetcher to be called on cache miss")
|
|
|
|
|
}
|
2026-04-01 15:22:52 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
2026-05-22 18:05:20 +02:00
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
_, _ = fmt.Fprint(w, "<metadata/>")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}))
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
2026-05-22 18:05:20 +02:00
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
srv := httptest.NewServer(h.Routes())
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(srv.URL + "/")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-05-22 18:05:20 +02:00
|
|
|
extensions := []string{".jar", ".war", ".ear", ".pom", ".aar", ".klib", ".module"}
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
for _, ext := range extensions {
|
|
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader("artifact")),
|
|
|
|
|
ContentType: "application/java-archive",
|
|
|
|
|
}
|
|
|
|
|
fetcher.fetchCalled = false
|
|
|
|
|
|
2026-05-22 18:05:20 +02:00
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
_ = resp.Body.Close()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:05:20 +02:00
|
|
|
h := NewMavenHandler(proxy, "http://localhost", "", "")
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
defer func() { _ = resp.Body.Close() }()
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
|
|
|
|
|
if !fetcher.fetchCalled {
|
|
|
|
|
t.Error("expected fetcher to be called on cache miss")
|
|
|
|
|
}
|
2026-04-01 15:22:52 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:05:20 +02:00
|
|
|
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/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": "<metadata/>",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 15:22:52 +01:00
|
|
|
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)
|
|
|
|
|
}
|
Add handler download flow and server utility tests
Covers HTTP download paths for gem, hex, go, conda, cran, and maven
handlers with cache hit, invalid input, and upstream proxy scenarios.
Adds server tests for formatTimeAgo, formatSize, categorizeLicense,
LoggerMiddleware, search/pagination, and API packages list endpoint.
2026-03-17 20:31:54 +00:00
|
|
|
}
|