2026-10-22 16:00:58 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/mirror"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/storage"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// newTarballUpstream serves body for path, compared case-insensitively since
|
|
|
|
|
// scoped npm names arrive with an escaped slash, and counts the requests.
|
|
|
|
|
func newTarballUpstream(t *testing.T, path, body string) (*httptest.Server, *atomic.Int32) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var hits atomic.Int32
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !strings.EqualFold(r.URL.EscapedPath(), path) && !strings.EqualFold(r.URL.Path, path) {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
hits.Add(1)
|
|
|
|
|
_, _ = io.WriteString(w, body)
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(upstream.Close)
|
|
|
|
|
return upstream, &hits
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mirrorWithConfig mirrors purls with the proxy proxy mirror builds for cfg.
|
|
|
|
|
func mirrorWithConfig(t *testing.T, cfg *config.Config, purls ...string) (*mirror.Progress, *handler.Proxy) {
|
2026-10-25 14:25:40 +01:00
|
|
|
t.Helper()
|
|
|
|
|
return mirrorWithVersions(t, cfg, nil, purls...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mirrorWithVersions mirrors purls, listing versions and files with versions,
|
|
|
|
|
// or from the configured upstreams when it is nil.
|
|
|
|
|
func mirrorWithVersions(t *testing.T, cfg *config.Config, versions mirror.VersionLister, purls ...string) (*mirror.Progress, *handler.Proxy) {
|
2026-10-22 16:00:58 +02:00
|
|
|
t.Helper()
|
|
|
|
|
cfg.Upstream.AllowLoopback = true
|
|
|
|
|
db, err := database.Create(t.TempDir() + "/test.db")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
|
store, err := storage.OpenBucket(context.Background(), "file://"+t.TempDir())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
|
|
|
proxy, _ := NewMirrorProxy(cfg, db, store, logger)
|
2026-10-25 19:37:22 +01:00
|
|
|
proxy.CacheMetadata = true // as proxy mirror does
|
2026-10-25 12:36:36 +01:00
|
|
|
m := mirror.New(proxy, db, store, logger, 1)
|
2026-10-25 19:37:22 +01:00
|
|
|
m.Charts = handler.NewHelmHandler(proxy, cfg.BaseURL, cfg.Upstream.Helm)
|
2026-10-25 14:25:40 +01:00
|
|
|
m.Versions = versions
|
2026-10-25 19:37:46 +01:00
|
|
|
m.JuliaUpstream = cfg.Upstream.Julia
|
2026-11-04 15:44:01 +01:00
|
|
|
m.APKRepositories = cfg.Upstream.APK
|
2026-10-25 14:25:40 +01:00
|
|
|
if versions == nil {
|
|
|
|
|
m.Versions = NewMirrorVersions(cfg.Upstream, proxy.HTTPClient)
|
|
|
|
|
}
|
2026-10-25 19:37:22 +01:00
|
|
|
progress, err := m.Run(context.Background(), &mirror.PURLSource{PURLs: purls, Versions: m.Versions})
|
2026-10-22 16:00:58 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Run() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
return progress, proxy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMirrorFetchesFromConfiguredUpstream(t *testing.T) {
|
|
|
|
|
upstream, hits := newTarballUpstream(t, "/left-pad/-/left-pad-1.3.0.tgz", "tarball")
|
|
|
|
|
cfg := config.Default()
|
|
|
|
|
cfg.Upstream.NPM = upstream.URL
|
|
|
|
|
|
|
|
|
|
progress, _ := mirrorWithConfig(t, cfg, "pkg:npm/left-pad@1.3.0")
|
|
|
|
|
|
|
|
|
|
if progress.Completed != 1 || hits.Load() != 1 {
|
|
|
|
|
t.Errorf("completed = %d, upstream hits = %d, errors = %v; want the tarball from upstream.npm", progress.Completed, hits.Load(), progress.Errors)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMirrorFetchesRoutedNPMPackagesFromTheirRoute(t *testing.T) {
|
|
|
|
|
const tarballPath = "/@acme/widget/-/widget-1.0.0.tgz"
|
|
|
|
|
public, publicHits := newTarballUpstream(t, tarballPath, "public")
|
|
|
|
|
private, privateHits := newTarballUpstream(t, tarballPath, "private")
|
|
|
|
|
cfg := config.Default()
|
|
|
|
|
cfg.Upstream.NPM = public.URL
|
|
|
|
|
cfg.Upstream.NPMRoutes = map[string]string{"@acme/*": private.URL}
|
|
|
|
|
|
|
|
|
|
progress, proxy := mirrorWithConfig(t, cfg, "pkg:npm/%40acme/widget@1.0.0")
|
|
|
|
|
|
|
|
|
|
if progress.Completed != 1 || privateHits.Load() != 1 {
|
|
|
|
|
t.Errorf("completed = %d, route hits = %d, errors = %v; want the tarball from the route", progress.Completed, privateHits.Load(), progress.Errors)
|
|
|
|
|
}
|
|
|
|
|
if publicHits.Load() != 0 {
|
|
|
|
|
t.Error("a routed package was mirrored from upstream.npm")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A client gets the mirrored tarball from the cache once the route is down.
|
|
|
|
|
private.Close()
|
|
|
|
|
npm := handler.NewNPMHandler(proxy, "http://proxy.test", cfg.Upstream.NPM).WithPackageRoutes(cfg.Upstream.NPMRoutes)
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
npm.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tarballPath, nil))
|
|
|
|
|
if rec.Code != http.StatusOK || rec.Body.String() != "private" {
|
|
|
|
|
t.Errorf("tarball from cache: status = %d, body = %q, want 200 and the mirrored tarball", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMirrorResolverBaseURLs(t *testing.T) {
|
|
|
|
|
u := config.Default().Upstream
|
|
|
|
|
u.NPM = "https://npm.example.com/"
|
|
|
|
|
u.CargoDownload = "https://crates.example.com/crates"
|
|
|
|
|
u.Gem = "https://gems.example.com"
|
|
|
|
|
u.Go = "https://goproxy.example.com"
|
|
|
|
|
u.Hex = "https://hex.example.com"
|
|
|
|
|
u.Pub = "https://pub.example.com"
|
|
|
|
|
u.Maven = "https://maven.example.com/maven2"
|
|
|
|
|
u.NuGet = "https://nuget.example.com"
|
|
|
|
|
|
|
|
|
|
resolver := newMirrorResolver(u)
|
|
|
|
|
tests := []struct {
|
|
|
|
|
ecosystem, name, version, want string
|
|
|
|
|
}{
|
|
|
|
|
{"npm", "left-pad", "1.3.0", "https://npm.example.com/left-pad/-/left-pad-1.3.0.tgz"},
|
|
|
|
|
{"cargo", "serde", "1.0.0", "https://crates.example.com/crates/serde/serde-1.0.0.crate"},
|
|
|
|
|
{"gem", "rake", "13.2.1", "https://gems.example.com/downloads/rake-13.2.1.gem"},
|
|
|
|
|
{"golang", "golang.org/x/mod", "v0.20.0", "https://goproxy.example.com/golang.org/x/mod/@v/v0.20.0.zip"},
|
|
|
|
|
{"hex", "jason", "1.4.4", "https://hex.example.com/tarballs/jason-1.4.4.tar"},
|
|
|
|
|
{"pub", "http", "1.2.2", "https://pub.example.com/packages/http/versions/1.2.2.tar.gz"},
|
|
|
|
|
{"maven", "org.example:lib", "1.0", "https://maven.example.com/maven2/org/example/lib/1.0/lib-1.0.jar"},
|
|
|
|
|
{"nuget", "Newtonsoft.Json", "13.0.3", "https://nuget.example.com/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg"},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
info, err := resolver.Resolve(context.Background(), tt.ecosystem, tt.name, tt.version)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("%s: Resolve() error = %v", tt.ecosystem, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if info.URL != tt.want {
|
|
|
|
|
t.Errorf("%s: URL = %q, want %q", tt.ecosystem, info.URL, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|