Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/mirror_apk_test.go
2026-11-05 12:34:32 +01:00

174 lines
6.1 KiB
Go

package server
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/handler"
)
const (
apkBusyboxPath = "/v3.20/main/x86_64/busybox-1.36.1-r29.apk"
apkHtopPath = "/v3.20/community/x86_64/htop-3.3.0-r0.apk"
apkMainIndex = "/v3.20/main/x86_64/APKINDEX.tar.gz"
apkCommIndex = "/v3.20/community/x86_64/APKINDEX.tar.gz"
)
// apkUpstream serves an Alpine mirror with busybox in v3.20/main and htop in
// v3.20/community, for x86_64. It counts requests by path.
type apkUpstream struct {
*httptest.Server
mu sync.Mutex
files map[string]string
hits map[string]int
}
func apkIndexArchive(t *testing.T, index string) string {
t.Helper()
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
if err := tw.WriteHeader(&tar.Header{Name: "APKINDEX", Mode: 0o644, Size: int64(len(index))}); err != nil {
t.Fatal(err)
}
_, _ = io.WriteString(tw, index)
_ = tw.Close()
_ = zw.Close()
return buf.String()
}
func newAPKUpstream(t *testing.T) *apkUpstream {
t.Helper()
u := &apkUpstream{
files: map[string]string{
apkMainIndex: apkIndexArchive(t, "P:busybox\nV:1.36.1-r29\nA:x86_64\nS:18\n\nP:musl\nV:1.2.5-r0\nA:x86_64\nS:15\n\n"),
apkCommIndex: apkIndexArchive(t, "P:htop\nV:3.3.0-r0\nA:x86_64\nS:15\n\n"),
apkBusyboxPath: "busybox 1.36.1-r29",
apkHtopPath: "htop 3.3.0-r0 x86",
"/v3.20/main/x86_64/musl-1.2.5-r0.apk": "musl 1.2.5-r0",
},
hits: map[string]int{},
}
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u.mu.Lock()
body, ok := u.files[r.URL.Path]
u.hits[r.URL.Path]++
u.mu.Unlock()
if !ok {
http.NotFound(w, r)
return
}
_, _ = io.WriteString(w, body)
}))
t.Cleanup(u.Close)
return u
}
func (u *apkUpstream) hitsFor(path string) int {
u.mu.Lock()
defer u.mu.Unlock()
return u.hits[path]
}
func apkMirrorConfig(upstream *apkUpstream) *config.Config {
cfg := config.Default()
cfg.Upstream.APK = map[string]string{"alpine": upstream.URL}
return cfg
}
func getAPK(proxy *handler.Proxy, upstream *apkUpstream, path string) *httptest.ResponseRecorder {
rec := httptest.NewRecorder()
h := handler.NewAPKHandler(proxy, "http://proxy.test", map[string]string{"alpine": upstream.URL})
h.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/alpine"+path, nil))
return rec
}
func TestMirrorAPKPackagesAreServedFromCache(t *testing.T) {
upstream := newAPKUpstream(t)
progress, proxy := mirrorWithConfig(t, apkMirrorConfig(upstream),
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.20.3",
"pkg:apk/alpine/htop@3.3.0-r0?arch=x86_64&distro=alpine-3.20.3",
"pkg:apk/alpine/musl@1.2.5-r0?arch=x86_64&distro=alpine-3.20.3")
if progress.Completed != 3 || progress.Failed != 0 {
t.Fatalf("completed = %d, failed = %d, errors = %v", progress.Completed, progress.Failed, progress.Errors)
}
for _, path := range []string{apkBusyboxPath, apkHtopPath, apkMainIndex, apkCommIndex} {
if got := upstream.hitsFor(path); got != 1 {
t.Errorf("%s fetched %d times, want 1", path, got)
}
}
// apk gets the packages from the cache once the upstream is down.
upstream.Close()
for path, want := range map[string]string{
apkBusyboxPath: "busybox 1.36.1-r29",
apkHtopPath: "htop 3.3.0-r0 x86",
} {
if rec := getAPK(proxy, upstream, path); rec.Code != http.StatusOK || rec.Body.String() != want {
t.Errorf("%s from cache: status = %d, body = %q, want 200 and %q", path, rec.Code, rec.Body, want)
}
}
}
func TestMirrorAPKReadsV2IndexBesideV3(t *testing.T) {
// A repository moving to apk v3 serves Packages.adb next to
// APKINDEX.tar.gz; the mirror reads the v2 index.
upstream := newAPKUpstream(t)
upstream.files["/v3.20/main/x86_64/Packages.adb"] = "ADB.v3-index"
progress, _ := mirrorWithConfig(t, apkMirrorConfig(upstream),
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.20.3")
if progress.Completed != 1 || progress.Failed != 0 {
t.Fatalf("completed = %d, failed = %d, errors = %v", progress.Completed, progress.Failed, progress.Errors)
}
}
func TestMirrorAPKRefusesIncompletePURLs(t *testing.T) {
upstream := newAPKUpstream(t)
tests := map[string]string{
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64": "distro",
"pkg:apk/alpine/busybox@1.36.1-r29?distro=alpine-3.20.3": "arch",
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.20-evil": "distro",
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.21.0_alpha20240807": "distro",
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=debian-12": "distro",
"pkg:apk/other/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.20.3": "upstream.apk",
"pkg:apk/alpine/busybox@9.9.9-r0?arch=x86_64&distro=alpine-3.20.3": "isn't in main or community of alpine v3.20",
"pkg:apk/alpine/busybox@1.36.1-r29?arch=../../etc&distro=alpine-3.20.3": "arch",
}
for p, want := range tests {
t.Run(p, func(t *testing.T) {
progress, _ := mirrorWithConfig(t, apkMirrorConfig(upstream), p)
if progress.Failed != 1 || len(progress.Errors) != 1 || !strings.Contains(progress.Errors[0].Error, want) {
t.Errorf("failed = %d, errors = %v; want one error mentioning %q", progress.Failed, progress.Errors, want)
}
})
}
}
func TestMirrorAPKIndexErrorsLeaveOutTheURL(t *testing.T) {
upstream := newAPKUpstream(t)
upstream.files[apkMainIndex] = "not an index"
progress, _ := mirrorWithConfig(t, apkMirrorConfig(upstream),
"pkg:apk/alpine/busybox@1.36.1-r29?arch=x86_64&distro=alpine-3.20.3")
if progress.Failed != 1 || len(progress.Errors) != 1 {
t.Fatalf("failed = %d, errors = %v; want one error", progress.Failed, progress.Errors)
}
msg := progress.Errors[0].Error
if !strings.Contains(msg, "alpine v3.20/main/x86_64") || strings.Contains(msg, upstream.URL) {
t.Errorf("error = %q; want the repository and directory without %s", msg, upstream.URL)
}
}