2026-07-26 20:07:46 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/registries/fetch"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestArtifactDownloadUpstreamNotFoundReturns404(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
path string
|
|
|
|
|
handler func(p *Proxy) http.Handler
|
|
|
|
|
}{
|
|
|
|
|
{"debian", "/pool/main/n/nginx/nginx_1.18.0-6_amd64.deb",
|
feat(debian): serve additional APT archives at /debian/{name}/
A Debian release is served by more than one archive. Security updates live on
a different host than the main archive, so a single upstream.debian URL cannot
serve a complete suite set and -security suites are unreachable.
Add upstream.debian_repositories, a name-to-URL map served at /debian/{name}/,
modelled on upstream.apk. The field is additive: upstream.debian keeps serving
/debian/pool/... and /debian/dists/... with unchanged cache identities, so
existing deployments and their warm caches are unaffected, and the scalar
field and PROXY_UPSTREAM_DEBIAN are untouched.
Named repositories scope both caches by name, since the same filename can hold
different bytes in different archives. Metadata keys are hashed over name,
upstream URL, and path, as APKHandler does. Names are validated through
validateNamedUpstreams, and "pool" and "dists" are refused because they would
shadow the main archive's own paths.
An unconfigured first path segment stays a main-archive path rather than
returning 404 as the APK handler does: the main archive is unnamed and serves
paths of its own at the root.
2026-09-14 15:41:13 -07:00
|
|
|
func(p *Proxy) http.Handler { return NewDebianHandler(p, "http://localhost", "", nil).Routes() }},
|
2026-07-26 20:07:46 +02:00
|
|
|
{"rpm", "/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewRPMHandler(p, "http://localhost").Routes() }},
|
Add Alpine APK repository proxy support (#293)
* Add Alpine APK repository proxy support
- Serve named APK repositories at /apk/{repository}/ with the official
Alpine mirror as the default repository
- Cache v2 APKINDEX.tar.gz and v3 Packages.adb indexes and detached
signatures via the metadata cache, serving stored bytes unchanged so
apk signature verification keeps working
- Cache .apk packages in the shared artifact cache keyed by the full
repository path, since APK filenames do not include the architecture
- Add configurable upstream repositories via upstream.apk with
validation, plus dashboard registry instructions
- Add tests for index/signature byte fidelity, per-arch caching, cache
hits, offline reads, upstream authentication, and 404 handling
- Document apk usage in README, config example, and configuration docs
* Serve APK package HEAD requests without a body
Use the method-aware serveArtifact helper (as container.go does) so HEAD
responses carry Content-Length but omit the body; add a regression test.
* Drop doubled blank line in docs/configuration.md
---------
Co-authored-by: Andrew Nesbitt <andrewnez@gmail.com>
2026-09-02 13:36:00 +02:00
|
|
|
{"apk", "/alpine/v3.22/main/x86_64/busybox-1.37.0-r12.apk",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewAPKHandler(p, "http://localhost", nil).Routes() }},
|
2026-10-11 23:41:16 +02:00
|
|
|
{"arch", "/archlinux/core/os/x86_64/bash-5.2.037-1-x86_64.pkg.tar.zst",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewArchHandler(p, "http://localhost", nil).Routes() }},
|
2026-07-26 20:07:46 +02:00
|
|
|
{"nuget", "/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewNuGetHandler(p, "http://localhost").Routes() }},
|
|
|
|
|
{"pypi", "/packages/packages/ab/cd/ef0123456789/requests-2.31.0-py3-none-any.whl",
|
2026-08-29 11:19:54 +01:00
|
|
|
func(p *Proxy) http.Handler {
|
|
|
|
|
return NewPyPIHandlerWithUpstreams(p, "http://localhost", "", "").Routes()
|
|
|
|
|
}},
|
2026-07-26 20:07:46 +02:00
|
|
|
{"cran", "/src/contrib/ggplot2_3.4.4.tar.gz",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewCRANHandler(p, "http://localhost").Routes() }},
|
|
|
|
|
{"conda", "/conda-forge/linux-64/numpy-1.26.0-py311_0.tar.bz2",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewCondaHandler(p, "http://localhost").Routes() }},
|
2026-10-11 17:56:15 +02:00
|
|
|
{"conan", "/v2/conans/zlib/1.3.1/_/_/revisions/0/files/conan_sources.tgz",
|
2026-07-26 20:07:46 +02:00
|
|
|
func(p *Proxy) http.Handler { return NewConanHandler(p, "http://localhost").Routes() }},
|
|
|
|
|
{"gem", "/gems/rails-7.1.0.gem",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewGemHandler(p, "http://localhost").Routes() }},
|
|
|
|
|
{"hex", "/tarballs/phoenix-1.7.10.tar",
|
|
|
|
|
func(p *Proxy) http.Handler { return NewHexHandler(p, "http://localhost").Routes() }},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.fetchErr = fetch.ErrNotFound
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404 for missing upstream artifact, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestJuliaPackageUpstreamNotFoundReturns404(t *testing.T) {
|
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.fetchErr = fetch.ErrNotFound
|
|
|
|
|
|
|
|
|
|
dead := httptest.NewServer(http.NotFoundHandler())
|
|
|
|
|
defer dead.Close()
|
|
|
|
|
|
|
|
|
|
h := NewJuliaHandler(proxy, "http://localhost")
|
|
|
|
|
h.upstreamURL = dead.URL
|
|
|
|
|
|
|
|
|
|
srv := httptest.NewServer(h.Routes())
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(srv.URL +
|
|
|
|
|
"/package/7876af07-990d-54b4-ab0e-23690620f79a/0123456789abcdef0123456789abcdef01234567")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404 for missing upstream package, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestComposerDownloadUpstreamNotFoundReturns404(t *testing.T) {
|
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.fetchErr = fetch.ErrNotFound
|
|
|
|
|
|
|
|
|
|
meta := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path == "/p2/monolog/monolog.json" {
|
|
|
|
|
_, _ = w.Write([]byte(`{
|
|
|
|
|
"packages": {
|
|
|
|
|
"monolog/monolog": [
|
|
|
|
|
{"version": "2.9.1", "dist": {"url": "https://example.com/monolog-2.9.1.zip", "type": "zip"}}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}`))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}))
|
|
|
|
|
defer meta.Close()
|
|
|
|
|
|
|
|
|
|
h := &ComposerHandler{proxy: proxy, repoURL: meta.URL, proxyURL: "http://localhost"}
|
|
|
|
|
srv := httptest.NewServer(h.Routes())
|
|
|
|
|
defer srv.Close()
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(srv.URL + "/files/monolog/monolog/2.9.1/monolog-2.9.1.zip")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404 for missing upstream dist, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestContainerBlobUpstreamNotFoundReturns404(t *testing.T) {
|
2026-08-13 07:35:07 +01:00
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.fetchErr = fetch.ErrNotFound
|
2026-07-26 20:07:46 +02:00
|
|
|
|
|
|
|
|
h := &ContainerHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
registryURL: "https://registry-1.docker.io",
|
|
|
|
|
proxyURL: "http://localhost:8080",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
|
|
|
"/library/nginx/blobs/sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.Routes().ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404 for missing upstream blob, got %d; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), "BLOB_UNKNOWN") {
|
|
|
|
|
t.Errorf("want BLOB_UNKNOWN error code in body, got: %s", w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|