2026-08-15 09:45:05 +01:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-15 10:34:22 +01:00
|
|
|
"context"
|
2026-08-15 09:45:05 +01:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/packageurl"
|
|
|
|
|
"github.com/git-pkgs/registries/fetch"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestSwiftPackageReleasesRewritesRegistryURLs(t *testing.T) {
|
|
|
|
|
var gotAccept string
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/registry/apple/swift-argument-parser" {
|
|
|
|
|
t.Errorf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
gotAccept = r.Header.Get("Accept")
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
w.Header().Set("Content-Version", "1")
|
2026-08-15 18:04:53 +01:00
|
|
|
w.Header().Add("Link", `</registry/apple/swift-argument-parser?page=2>; rel="next"`)
|
2026-08-15 09:45:05 +01:00
|
|
|
_, _ = io.WriteString(w, `{"releases":{"1.2.0":{"url":"/registry/apple/swift-argument-parser/1.2.0"},"1.1.0":{}}}`)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
|
2026-08-15 19:05:23 +01:00
|
|
|
req := httptest.NewRequest(http.MethodGet, "/APPLE/SWIFT-ARGUMENT-PARSER", nil)
|
2026-08-15 09:45:05 +01:00
|
|
|
req.Header.Set("Accept", swiftAcceptJSON)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if gotAccept != swiftAcceptJSON {
|
|
|
|
|
t.Errorf("upstream Accept = %q, want %q", gotAccept, swiftAcceptJSON)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Version"); got != "1" {
|
|
|
|
|
t.Errorf("Content-Version = %q, want 1", got)
|
|
|
|
|
}
|
2026-08-15 18:04:53 +01:00
|
|
|
if got := w.Header().Get("Link"); got != `<https://proxy.example/swift/apple/swift-argument-parser?page=2>; rel="next"` {
|
|
|
|
|
t.Errorf("Link = %q", got)
|
|
|
|
|
}
|
2026-08-15 09:45:05 +01:00
|
|
|
|
|
|
|
|
var body struct {
|
|
|
|
|
Releases map[string]struct {
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
} `json:"releases"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
|
|
|
t.Fatalf("decoding response: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := body.Releases["1.2.0"].URL; got != "https://proxy.example/swift/apple/swift-argument-parser/1.2.0" {
|
|
|
|
|
t.Errorf("release URL = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := body.Releases["1.1.0"].URL; got != "" {
|
|
|
|
|
t.Errorf("release without upstream URL gained URL %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwiftReleaseMetadataSupportsJSONExtensionAndHead(t *testing.T) {
|
|
|
|
|
var requestMethods []string
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
requestMethods = append(requestMethods, r.Method)
|
|
|
|
|
if r.URL.Path != "/registry/apple/example/1.2.3" {
|
|
|
|
|
t.Errorf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = io.WriteString(w, `{"id":"apple.example","version":"1.2.3","resources":[]}`)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
|
|
|
|
|
|
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodHead} {
|
2026-08-15 19:05:23 +01:00
|
|
|
req := httptest.NewRequest(method, "/APPLE/EXAMPLE/1.2.3.json", nil)
|
2026-08-15 09:45:05 +01:00
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("%s status = %d, want 200", method, w.Code)
|
|
|
|
|
}
|
|
|
|
|
if method == http.MethodHead && w.Body.Len() != 0 {
|
|
|
|
|
t.Errorf("HEAD response body length = %d, want 0", w.Body.Len())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(requestMethods) != 2 || requestMethods[0] != http.MethodGet || requestMethods[1] != http.MethodGet {
|
|
|
|
|
t.Errorf("upstream methods = %v, want metadata GETs", requestMethods)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwiftManifestProxiesQueryAndRewritesLinks(t *testing.T) {
|
|
|
|
|
var upstream *httptest.Server
|
|
|
|
|
var gotAccept string
|
|
|
|
|
upstream = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodGet {
|
|
|
|
|
t.Errorf("upstream method = %s, want GET", r.Method)
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path != "/registry/apple/example/1.2.3/Package.swift" {
|
|
|
|
|
t.Errorf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("swift-version"); got != "5.9" {
|
|
|
|
|
t.Errorf("swift-version = %q, want 5.9", got)
|
|
|
|
|
}
|
|
|
|
|
gotAccept = r.Header.Get("Accept")
|
|
|
|
|
w.Header().Set("Content-Type", "text/x-swift")
|
|
|
|
|
w.Header().Add("Link", fmt.Sprintf(`<%s/registry/apple/example/1.2.3/Package.swift?swift-version=5.8>; rel="alternate"; filename="Package@swift-5.8.swift"`, upstream.URL))
|
|
|
|
|
w.Header().Add("Link", `<https://github.com/apple/example>; rel="canonical"`)
|
|
|
|
|
_, _ = io.WriteString(w, "// swift-tools-version: 5.9\n")
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
|
2026-08-15 19:05:23 +01:00
|
|
|
req := httptest.NewRequest(http.MethodGet, "/APPLE/EXAMPLE/1.2.3/Package.swift?swift-version=5.9", nil)
|
2026-08-15 09:45:05 +01:00
|
|
|
req.Header.Set("Accept", swiftAcceptManifest)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if gotAccept != swiftAcceptManifest {
|
|
|
|
|
t.Errorf("upstream Accept = %q, want %q", gotAccept, swiftAcceptManifest)
|
|
|
|
|
}
|
|
|
|
|
links := strings.Join(w.Header().Values("Link"), ",")
|
|
|
|
|
if !strings.Contains(links, "https://proxy.example/swift/apple/example/1.2.3/Package.swift?swift-version=5.8") {
|
|
|
|
|
t.Errorf("internal manifest Link was not rewritten: %q", links)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(links, "https://github.com/apple/example") {
|
|
|
|
|
t.Errorf("external canonical Link was changed: %q", links)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Version"); got != "1" {
|
|
|
|
|
t.Errorf("Content-Version = %q, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwiftSourceArchiveCachesAndPreservesSecurityMetadata(t *testing.T) {
|
|
|
|
|
archive := []byte("swift source archive")
|
|
|
|
|
checksumBytes := sha256.Sum256(archive)
|
|
|
|
|
checksum := hex.EncodeToString(checksumBytes[:])
|
|
|
|
|
signature := base64.StdEncoding.EncodeToString([]byte("signature"))
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/registry/apple/example/1.2.3" {
|
|
|
|
|
t.Errorf("metadata path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q,"signing":{"signatureBase64Encoded":%q,"signatureFormat":"cms-1.0.0"}}]}`, checksum, signature)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, db, _, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader(string(archive))),
|
|
|
|
|
Size: int64(len(archive)),
|
|
|
|
|
ContentType: "application/zip",
|
|
|
|
|
}
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
|
|
|
|
|
|
|
|
|
|
requestArchive := func(method string) *httptest.ResponseRecorder {
|
|
|
|
|
req := httptest.NewRequest(method, "/apple/example/1.2.3.zip", nil)
|
|
|
|
|
req.Header.Set("Accept", swiftAcceptArchive)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
return w
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w := requestArchive(http.MethodGet)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.Bytes(); string(got) != string(archive) {
|
|
|
|
|
t.Errorf("archive body = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if !fetcher.fetchCalled {
|
|
|
|
|
t.Fatal("archive fetcher was not called")
|
|
|
|
|
}
|
|
|
|
|
if got := fetcher.fetchedURL; got != upstream.URL+"/registry/apple/example/1.2.3.zip" {
|
|
|
|
|
t.Errorf("fetched URL = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := fetcher.fetchedHeader.Get("Accept"); got != swiftAcceptArchive {
|
|
|
|
|
t.Errorf("archive Accept = %q, want %q", got, swiftAcceptArchive)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Digest"); got != "sha-256="+base64.StdEncoding.EncodeToString(checksumBytes[:]) {
|
|
|
|
|
t.Errorf("Digest = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("X-Swift-Package-Signature"); got != signature {
|
|
|
|
|
t.Errorf("signature = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("X-Swift-Package-Signature-Format"); got != "cms-1.0.0" {
|
|
|
|
|
t.Errorf("signature format = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Disposition"); got != `attachment; filename="example-1.2.3.zip"` {
|
|
|
|
|
t.Errorf("Content-Disposition = %q", got)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
|
2026-08-16 23:10:33 +01:00
|
|
|
if strings.HasPrefix(packagePURL, "pkg:swift/") {
|
|
|
|
|
t.Fatalf("registry identity produced source PURL %q", packagePURL)
|
|
|
|
|
}
|
2026-08-15 09:45:05 +01:00
|
|
|
versionRecord, err := db.GetVersionByPURL(versionPURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("cached Swift version %q not found: %v", versionPURL, err)
|
|
|
|
|
}
|
|
|
|
|
if versionRecord == nil {
|
|
|
|
|
t.Fatalf("cached Swift version %q not found", versionPURL)
|
|
|
|
|
}
|
2026-08-16 23:10:33 +01:00
|
|
|
if versionRecord.PackagePURL != packagePURL {
|
|
|
|
|
t.Errorf("cached package PURL = %q, want %q", versionRecord.PackagePURL, packagePURL)
|
|
|
|
|
}
|
2026-08-15 09:45:05 +01:00
|
|
|
|
|
|
|
|
fetcher.fetchCalled = false
|
|
|
|
|
w = requestArchive(http.MethodHead)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("HEAD status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if w.Body.Len() != 0 {
|
|
|
|
|
t.Errorf("HEAD body length = %d, want 0", w.Body.Len())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Length"); got != fmt.Sprint(len(archive)) {
|
|
|
|
|
t.Errorf("HEAD Content-Length = %q", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w = requestArchive(http.MethodGet)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != string(archive) {
|
|
|
|
|
t.Fatalf("cached response = %d %q", w.Code, w.Body.Bytes())
|
|
|
|
|
}
|
|
|
|
|
if fetcher.fetchCalled {
|
|
|
|
|
t.Error("cached archive contacted artifact upstream")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 10:34:22 +01:00
|
|
|
func TestSwiftSourceArchiveRejectsChecksumMismatch(t *testing.T) {
|
|
|
|
|
archive := []byte("unexpected archive")
|
|
|
|
|
expectedChecksum := sha256.Sum256([]byte("expected archive"))
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(expectedChecksum[:]))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, db, store, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader(string(archive))),
|
|
|
|
|
Size: int64(len(archive)),
|
|
|
|
|
ContentType: "application/zip",
|
|
|
|
|
}
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil))
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusBadGateway {
|
|
|
|
|
t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if len(store.files) != 0 {
|
|
|
|
|
t.Errorf("mismatched archive remained in storage: %v", store.files)
|
|
|
|
|
}
|
2026-08-20 09:18:05 +01:00
|
|
|
packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
|
2026-08-16 23:10:33 +01:00
|
|
|
cached, err := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip")
|
2026-08-15 10:34:22 +01:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("checking cache: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cached != nil {
|
|
|
|
|
t.Error("mismatched archive gained a cache record")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 19:05:23 +01:00
|
|
|
func TestSwiftSourceArchiveCanonicalizesPackageIdentity(t *testing.T) {
|
|
|
|
|
archive := []byte("swift source archive")
|
|
|
|
|
checksumBytes := sha256.Sum256(archive)
|
|
|
|
|
checksum := hex.EncodeToString(checksumBytes[:])
|
|
|
|
|
var metadataPaths []string
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
metadataPaths = append(metadataPaths, r.URL.Path)
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, db, store, fetcher := setupTestProxy(t)
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
|
|
|
|
|
requestArchive := func(path string) {
|
|
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader(string(archive))),
|
|
|
|
|
Size: int64(len(archive)),
|
|
|
|
|
ContentType: "application/zip",
|
|
|
|
|
}
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("GET %s status = %d, want 200; body: %s", path, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestArchive("/apple/example/1.2.3.zip")
|
|
|
|
|
requestArchive("/APPLE/EXAMPLE/1.2.3.zip")
|
|
|
|
|
|
|
|
|
|
if len(store.files) != 1 {
|
|
|
|
|
t.Errorf("cached files = %d, want 1", len(store.files))
|
|
|
|
|
}
|
|
|
|
|
for _, path := range metadataPaths {
|
|
|
|
|
if path != "/apple/example/1.2.3" {
|
|
|
|
|
t.Errorf("metadata path = %q, want canonical lowercase path", path)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
canonicalPURL, _ := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
|
2026-08-15 19:05:23 +01:00
|
|
|
canonical, err := db.GetPackageByPURL(canonicalPURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("getting canonical package: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if canonical == nil {
|
|
|
|
|
t.Fatalf("canonical package %q not found", canonicalPURL)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
nonCanonicalPURL, _ := packageurl.MakeCacheStrings("swift", "APPLE/EXAMPLE", "1.2.3")
|
2026-08-16 23:10:33 +01:00
|
|
|
if nonCanonicalPURL != canonicalPURL {
|
|
|
|
|
t.Errorf("uppercase cache PURL = %q, want %q", nonCanonicalPURL, canonicalPURL)
|
2026-08-15 19:05:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
func TestSwiftSourceArchiveHeadDiscardsCachedChecksumMismatch(t *testing.T) {
|
2026-08-15 10:34:22 +01:00
|
|
|
archive := []byte("cached archive")
|
2026-08-20 09:18:05 +01:00
|
|
|
upstreamChecksum := sha256.Sum256([]byte("upstream archive"))
|
2026-08-15 10:34:22 +01:00
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
var probed bool
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if strings.HasSuffix(r.URL.Path, ".zip") {
|
|
|
|
|
probed = true
|
|
|
|
|
w.Header().Set("Content-Range", "bytes 0-0/456")
|
|
|
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
|
|
|
_, _ = w.Write([]byte("x"))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-15 10:34:22 +01:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2026-08-20 09:18:05 +01:00
|
|
|
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(upstreamChecksum[:]))
|
2026-08-15 10:34:22 +01:00
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
proxy, db, store, fetcher := setupTestProxy(t)
|
2026-08-15 10:34:22 +01:00
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader(string(archive))),
|
|
|
|
|
Size: int64(len(archive)),
|
|
|
|
|
ContentType: "application/zip",
|
|
|
|
|
}
|
2026-08-20 09:18:05 +01:00
|
|
|
packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3")
|
2026-08-16 23:10:33 +01:00
|
|
|
cached, err := proxy.getOrFetchArtifactFromURLWithCachePURLs(
|
|
|
|
|
context.Background(), "swift", "apple/example", "1.2.3", "example-1.2.3.zip",
|
|
|
|
|
packagePURL, versionPURL, upstream.URL+"/apple/example/1.2.3.zip", nil, "",
|
2026-08-15 10:34:22 +01:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("seeding cache: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_ = cached.Reader.Close()
|
|
|
|
|
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil))
|
|
|
|
|
|
2026-08-20 09:18:05 +01:00
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !probed {
|
|
|
|
|
t.Error("stale cache entry was not replaced by an upstream probe")
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Length"); got != "456" {
|
|
|
|
|
t.Errorf("Content-Length = %q, want 456 from upstream probe", got)
|
2026-08-15 10:34:22 +01:00
|
|
|
}
|
|
|
|
|
if len(store.files) != 0 {
|
2026-08-20 09:18:05 +01:00
|
|
|
t.Errorf("mismatched cached archive remained in storage: %v", store.files)
|
|
|
|
|
}
|
|
|
|
|
if rec, _ := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip"); rec != nil {
|
|
|
|
|
t.Error("mismatched cache record was not cleared")
|
2026-08-15 10:34:22 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwiftSourceArchiveRequiresReleaseMetadata(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, store, fetcher := setupTestProxy(t)
|
|
|
|
|
fetcher.artifact = &fetch.Artifact{
|
|
|
|
|
Body: io.NopCloser(strings.NewReader("signed archive")),
|
|
|
|
|
ContentType: "application/zip",
|
|
|
|
|
}
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil))
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusBadGateway {
|
|
|
|
|
t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if fetcher.fetchCalled {
|
|
|
|
|
t.Error("archive was fetched without release security metadata")
|
|
|
|
|
}
|
|
|
|
|
if len(store.files) != 0 {
|
|
|
|
|
t.Errorf("archive was cached without release security metadata: %v", store.files)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 18:04:53 +01:00
|
|
|
func TestSwiftSourceArchiveColdHeadUsesRangeGetAcrossRedirect(t *testing.T) {
|
2026-08-15 10:34:22 +01:00
|
|
|
checksum := strings.Repeat("a", sha256.Size*2)
|
|
|
|
|
var archiveAccept string
|
|
|
|
|
var archiveMethod string
|
2026-08-15 18:04:53 +01:00
|
|
|
var archiveRange string
|
|
|
|
|
download := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
archiveMethod = r.Method
|
|
|
|
|
archiveRange = r.Header.Get("Range")
|
|
|
|
|
w.Header().Set("Content-Range", "bytes 0-0/123")
|
|
|
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
|
|
|
_, _ = w.Write([]byte("x"))
|
|
|
|
|
}))
|
|
|
|
|
defer download.Close()
|
|
|
|
|
|
2026-08-15 10:34:22 +01:00
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch r.URL.Path {
|
|
|
|
|
case "/apple/example/1.2.3":
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum)
|
|
|
|
|
case "/apple/example/1.2.3.zip":
|
|
|
|
|
archiveAccept = r.Header.Get("Accept")
|
2026-08-15 18:04:53 +01:00
|
|
|
http.Redirect(w, r, download.URL, http.StatusSeeOther)
|
2026-08-15 10:34:22 +01:00
|
|
|
default:
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.HTTPClient = upstream.Client()
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil))
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
2026-08-15 18:04:53 +01:00
|
|
|
if archiveMethod != http.MethodGet {
|
|
|
|
|
t.Errorf("download method = %q, want GET", archiveMethod)
|
|
|
|
|
}
|
|
|
|
|
if archiveRange != "bytes=0-0" {
|
|
|
|
|
t.Errorf("download Range = %q, want bytes=0-0", archiveRange)
|
2026-08-15 10:34:22 +01:00
|
|
|
}
|
|
|
|
|
if archiveAccept != swiftAcceptArchive {
|
|
|
|
|
t.Errorf("upstream Accept = %q, want %q", archiveAccept, swiftAcceptArchive)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Length"); got != "123" {
|
|
|
|
|
t.Errorf("Content-Length = %q, want 123", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 09:45:05 +01:00
|
|
|
func TestSwiftIdentifiersAndPublishingUnsupported(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/registry/identifiers" {
|
|
|
|
|
t.Errorf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("url"); got != "https://github.com/apple/example" {
|
|
|
|
|
t.Errorf("lookup URL = %q", got)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = io.WriteString(w, `{"identifiers":["apple.example"]}`)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/identifiers?url=https%3A%2F%2Fgithub.com%2Fapple%2Fexample", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "apple.example") {
|
|
|
|
|
t.Fatalf("identifier response = %d %q", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/identifiers", nil)
|
|
|
|
|
w = httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Errorf("missing URL status = %d, want 400", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req = httptest.NewRequest(http.MethodPut, "/apple/example/1.2.3", strings.NewReader("ignored"))
|
|
|
|
|
w = httptest.NewRecorder()
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
|
|
|
t.Errorf("publish status = %d, want 405", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Allow"); got != "GET, HEAD" {
|
|
|
|
|
t.Errorf("Allow = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwiftIdentifierValidation(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
value string
|
|
|
|
|
valid func(string) bool
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{"scope", "apple", validSwiftScope, true},
|
|
|
|
|
{"scope hyphen", "swift-server", validSwiftScope, true},
|
|
|
|
|
{"scope underscore", "swift_server", validSwiftScope, false},
|
|
|
|
|
{"scope repeated separator", "swift--server", validSwiftScope, false},
|
|
|
|
|
{"package", "swift-argument_parser", validSwiftPackageName, true},
|
|
|
|
|
{"package repeated separators", "swift-_argument", validSwiftPackageName, false},
|
|
|
|
|
{"package trailing separator", "example-", validSwiftPackageName, false},
|
|
|
|
|
{"package non-ASCII", "café", validSwiftPackageName, false},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
|
if got := test.valid(test.value); got != test.want {
|
|
|
|
|
t.Errorf("validation of %q = %v, want %v", test.value, got, test.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|