Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/server/mirror_swift_test.go
Simon Chrzanowski 8ea75a30a3 Check cached Swift documents against their digest and resolve source PURLs
Release lists and manifests record their sha256 digest and are read back
through storedMetadataEntry and openMetadataEntry, so a corrupted copy is
fetched again. pkg:swift PURLs naming a source repository are looked up
through the registry's /identifiers.
2026-10-26 16:43:42 +01:00

223 lines
8.8 KiB
Go

package server
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"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"
)
// swiftUpstream serves the Swift registry API for acme.widget with releases
// 1.0.0, on the first page of the release list, and 1.1.0, on the second.
// Each release has a Package.swift that links a manifest for Swift 5.8.
type swiftUpstream struct {
*httptest.Server
// checksum overrides the archive checksum the release metadata lists.
checksum string
hits atomic.Int32
archiveHits atomic.Int32
}
func swiftArchive(version string) string { return "zip of acme.widget " + version }
func newSwiftUpstream(t *testing.T) *swiftUpstream {
t.Helper()
u := &swiftUpstream{}
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u.hits.Add(1)
w.Header().Set("Content-Version", "1")
rest, ok := strings.CutPrefix(r.URL.Path, "/acme/widget")
switch {
case r.URL.Path == "/identifiers":
if r.URL.Query().Get("url") != "https://github.com/acme/widget" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"identifiers":["acme.widget"]}`)
case !ok:
http.NotFound(w, r)
case rest == "":
page := map[string]string{"": "1.0.0", "page=2": "1.1.0"}[r.URL.RawQuery]
if page == "" {
http.NotFound(w, r)
return
}
if r.URL.RawQuery == "" {
w.Header().Set("Link", fmt.Sprintf(`<%s/acme/widget?page=2>; rel="next"`, u.URL))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"releases": map[string]any{
page: map[string]string{"url": u.URL + "/acme/widget/" + page},
}})
case strings.HasSuffix(rest, ".zip"):
u.archiveHits.Add(1)
_, _ = io.WriteString(w, swiftArchive(strings.TrimSuffix(rest[1:], ".zip")))
case strings.HasSuffix(rest, "/Package.swift"):
version := strings.TrimSuffix(rest[1:], "/Package.swift")
w.Header().Set("Content-Type", "text/x-swift")
if r.URL.Query().Get("swift-version") == "5.8" {
_, _ = io.WriteString(w, "// swift-tools-version:5.8\n// "+version)
return
}
w.Header().Set("Link", fmt.Sprintf(`<%s/acme/widget/%s/Package.swift?swift-version=5.8>; rel="alternate"; filename="Package@swift-5.8.swift"; swift-tools-version="5.8"`, u.URL, version))
_, _ = io.WriteString(w, "// swift-tools-version:5.9\n// "+version)
default:
version := rest[1:]
sum := sha256.Sum256([]byte(swiftArchive(version)))
checksum := hex.EncodeToString(sum[:])
if u.checksum != "" {
checksum = u.checksum
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "acme.widget", "version": version,
"resources": []map[string]string{{"name": "source-archive", "type": "application/zip", "checksum": checksum}},
})
}
}))
t.Cleanup(u.Close)
return u
}
// mirrorSwift mirrors purls from upstream the way proxy mirror does, with
// metadata caching on.
func mirrorSwift(t *testing.T, upstream *swiftUpstream, purls ...string) (*mirror.Progress, *handler.Proxy) {
t.Helper()
cfg := config.Default()
cfg.Upstream.Swift = upstream.URL
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)
proxy.CacheMetadata = true
m := mirror.New(proxy, db, store, logger, 1)
m.Versions = NewMirrorVersions(cfg.Upstream, proxy.HTTPClient)
progress, err := m.Run(context.Background(), &mirror.PURLSource{PURLs: purls, Versions: m.Versions})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
return progress, proxy
}
func swiftRegistryGet(t *testing.T, routes http.Handler, path, accept string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Accept", accept)
rec := httptest.NewRecorder()
routes.ServeHTTP(rec, req)
return rec
}
func TestMirrorSwiftReleaseIsServedFromCache(t *testing.T) {
upstream := newSwiftUpstream(t)
progress, proxy := mirrorSwift(t, upstream, "pkg:swift/Acme/Widget@1.0.0")
if progress.Completed != 1 || upstream.archiveHits.Load() != 1 {
t.Fatalf("completed = %d, archive hits = %d, errors = %v", progress.Completed, upstream.archiveHits.Load(), progress.Errors)
}
// SwiftPM resolves and downloads the release with the upstream down:
// the release list and its pages, the release metadata, both manifests
// and the archive come from the cache.
upstream.Close()
routes := handler.NewSwiftHandler(proxy, "http://proxy.test", upstream.URL).Routes()
const jsonType = "application/vnd.swift.registry.v1+json"
const swiftType = "application/vnd.swift.registry.v1+swift"
checks := []struct {
path, accept, body, link string
}{
{"/acme/widget", jsonType, "http://proxy.test/swift/acme/widget/1.0.0", "<http://proxy.test/swift/acme/widget?page=2>"},
{"/acme/widget?page=2", jsonType, "http://proxy.test/swift/acme/widget/1.1.0", ""},
{"/acme/widget/1.0.0", jsonType, `"source-archive"`, ""},
{"/acme/widget/1.0.0/Package.swift", swiftType, "swift-tools-version:5.9", "<http://proxy.test/swift/acme/widget/1.0.0/Package.swift?swift-version=5.8>"},
{"/acme/widget/1.0.0/Package.swift?swift-version=5.8.0", swiftType, "swift-tools-version:5.8", ""},
{"/acme/widget/1.0.0.zip", "application/vnd.swift.registry.v1+zip", swiftArchive("1.0.0"), ""},
}
for _, check := range checks {
rec := swiftRegistryGet(t, routes, check.path, check.accept)
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), check.body) {
t.Errorf("%s: status = %d, body %q; want 200 with %q", check.path, rec.Code, strings.TrimSpace(rec.Body.String()), check.body)
}
if link := rec.Header().Get("Link"); !strings.Contains(link, check.link) {
t.Errorf("%s: Link = %q, want %q", check.path, link, check.link)
}
}
}
func TestMirrorSwiftRefusesChecksumMismatch(t *testing.T) {
upstream := newSwiftUpstream(t)
upstream.checksum = strings.Repeat("0", 64)
progress, proxy := mirrorSwift(t, upstream, "pkg:swift/acme/widget@1.0.0")
if progress.Failed != 1 || progress.Completed != 0 {
t.Fatalf("completed = %d, failed = %d, errors = %v; want the archive refused", progress.Completed, progress.Failed, progress.Errors)
}
upstream.Close()
routes := handler.NewSwiftHandler(proxy, "http://proxy.test", upstream.URL).Routes()
if rec := swiftRegistryGet(t, routes, "/acme/widget/1.0.0.zip", "application/vnd.swift.registry.v1+zip"); rec.Code == http.StatusOK {
t.Error("the refused archive was cached")
}
}
func TestMirrorSwiftListsReleases(t *testing.T) {
upstream := newSwiftUpstream(t)
progress, _ := mirrorSwift(t, upstream, "pkg:swift/acme/widget")
if progress.Total != 2 || progress.Completed != 2 || upstream.archiveHits.Load() != 2 {
t.Errorf("total = %d, completed = %d, archive hits = %d, errors = %v; want both releases mirrored",
progress.Total, progress.Completed, upstream.archiveHits.Load(), progress.Errors)
}
}
func TestMirrorSwiftResolvesRepositoryPURLs(t *testing.T) {
upstream := newSwiftUpstream(t)
progress, proxy := mirrorSwift(t, upstream, "pkg:swift/github.com/acme/widget@1.0.0", "pkg:swift/github.com/acme/widget", "pkg:swift/github.com/acme/other@1.0.0")
// The listed PURL's 1.0.0 is the versioned one, so it's mirrored once.
if progress.Completed != 2 || progress.Failed != 1 || upstream.archiveHits.Load() != 2 {
t.Fatalf("completed = %d, failed = %d, archive hits = %d, errors = %v; want both acme.widget releases mirrored and the unknown repository refused",
progress.Completed, progress.Failed, upstream.archiveHits.Load(), progress.Errors)
}
if len(progress.Errors) != 1 || !strings.Contains(progress.Errors[0].Error, "no package for https://github.com/acme/other") {
t.Errorf("errors = %v; want the unknown repository named", progress.Errors)
}
// The lookup is cached too, for clients that resolve a repository URL
// through the registry.
upstream.Close()
routes := handler.NewSwiftHandler(proxy, "http://proxy.test", upstream.URL).Routes()
rec := swiftRegistryGet(t, routes, "/identifiers?url=https%3A%2F%2Fgithub.com%2Facme%2Fwidget", "application/vnd.swift.registry.v1+json")
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "acme.widget") {
t.Errorf("identifiers: status = %d, body %q; want the cached lookup", rec.Code, rec.Body.String())
}
}