forked from mirrors/pkg-proxy
* Fix container blob caching by passing auth token to fetcher The container handler was calling GetOrFetchArtifactFromURL without authentication headers, causing Docker Hub to return 401. The fallback proxyBlobWithAuth path had auth but bypassed the cache entirely. Now passes the Bearer token through GetOrFetchArtifactFromURLWithHeaders so blobs are both authenticated and cached. Fixes git-pkgs/proxy#43 * Update registries to v0.4.0 Replace pre-release pseudo-version with the released v0.4.0 now that git-pkgs/registries#13 has been merged.
244 lines
6.2 KiB
Go
244 lines
6.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
func TestContainerHandler_parseBlobPath(t *testing.T) {
|
|
h := &ContainerHandler{}
|
|
|
|
tests := []struct {
|
|
path string
|
|
wantName string
|
|
wantDigest string
|
|
}{
|
|
{
|
|
path: "library/nginx/blobs/sha256:abc123def456",
|
|
wantName: "library/nginx",
|
|
wantDigest: "sha256:abc123def456",
|
|
},
|
|
{
|
|
path: "myorg/myrepo/blobs/sha256:0123456789abcdef",
|
|
wantName: "myorg/myrepo",
|
|
wantDigest: "sha256:0123456789abcdef",
|
|
},
|
|
{
|
|
path: "deep/nested/repo/name/blobs/sha256:fedcba9876543210",
|
|
wantName: "deep/nested/repo/name",
|
|
wantDigest: "sha256:fedcba9876543210",
|
|
},
|
|
{
|
|
path: "invalid/path",
|
|
wantName: "",
|
|
wantDigest: "",
|
|
},
|
|
{
|
|
path: "repo/blobs/md5:invalid",
|
|
wantName: "",
|
|
wantDigest: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
name, digest := h.parseBlobPath(tt.path)
|
|
if name != tt.wantName {
|
|
t.Errorf("parseBlobPath() name = %q, want %q", name, tt.wantName)
|
|
}
|
|
if digest != tt.wantDigest {
|
|
t.Errorf("parseBlobPath() digest = %q, want %q", digest, tt.wantDigest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerHandler_parseManifestPath(t *testing.T) {
|
|
h := &ContainerHandler{}
|
|
|
|
tests := []struct {
|
|
path string
|
|
wantName string
|
|
wantReference string
|
|
}{
|
|
{
|
|
path: "library/nginx/manifests/latest",
|
|
wantName: "library/nginx",
|
|
wantReference: "latest",
|
|
},
|
|
{
|
|
path: "myorg/myrepo/manifests/v1.0.0",
|
|
wantName: "myorg/myrepo",
|
|
wantReference: "v1.0.0",
|
|
},
|
|
{
|
|
path: "repo/manifests/sha256:abc123",
|
|
wantName: "repo",
|
|
wantReference: "sha256:abc123",
|
|
},
|
|
{
|
|
path: "invalid/path",
|
|
wantName: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
name, ref := h.parseManifestPath(tt.path)
|
|
if name != tt.wantName {
|
|
t.Errorf("parseManifestPath() name = %q, want %q", name, tt.wantName)
|
|
}
|
|
if ref != tt.wantReference {
|
|
t.Errorf("parseManifestPath() reference = %q, want %q", ref, tt.wantReference)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerHandler_parseTagsListPath(t *testing.T) {
|
|
h := &ContainerHandler{}
|
|
|
|
tests := []struct {
|
|
path string
|
|
wantName string
|
|
}{
|
|
{
|
|
path: "library/nginx/tags/list",
|
|
wantName: "library/nginx",
|
|
},
|
|
{
|
|
path: "myorg/myrepo/tags/list",
|
|
wantName: "myorg/myrepo",
|
|
},
|
|
{
|
|
path: "invalid/path",
|
|
wantName: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
name := h.parseTagsListPath(tt.path)
|
|
if name != tt.wantName {
|
|
t.Errorf("parseTagsListPath() = %q, want %q", name, tt.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerHandler_BlobDownload_CachesWithAuth(t *testing.T) {
|
|
// Set up a mock auth server that returns a token
|
|
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"token": "test-token-123"})
|
|
}))
|
|
defer authServer.Close()
|
|
|
|
// Set up mock fetcher that captures headers
|
|
var capturedHeaders http.Header
|
|
mf := &mockFetcherWithHeaders{
|
|
fetchFn: func(_ context.Context, _ string, headers http.Header) (*fetch.Artifact, error) {
|
|
capturedHeaders = headers
|
|
return &fetch.Artifact{
|
|
Body: io.NopCloser(bytes.NewReader([]byte("blob-content"))),
|
|
Size: 12,
|
|
ContentType: "application/octet-stream",
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
db, err := database.Create(dir + "/test.db")
|
|
if err != nil {
|
|
t.Fatalf("failed to create test database: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
store := newMockStorage()
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
proxy := &Proxy{
|
|
DB: db,
|
|
Storage: store,
|
|
Fetcher: mf,
|
|
Logger: logger,
|
|
HTTPClient: &http.Client{},
|
|
}
|
|
|
|
h := &ContainerHandler{
|
|
proxy: proxy,
|
|
registryURL: "https://registry-1.docker.io",
|
|
authURL: authServer.URL,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
handler := h.Routes()
|
|
req := httptest.NewRequest(http.MethodGet, "/library/nginx/blobs/sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("got status %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
|
|
// Verify auth header was passed to the fetcher
|
|
if capturedHeaders == nil {
|
|
t.Fatal("expected headers to be passed to fetcher, got nil")
|
|
}
|
|
auth := capturedHeaders.Get("Authorization")
|
|
if auth != "Bearer test-token-123" {
|
|
t.Errorf("Authorization = %q, want %q", auth, "Bearer test-token-123")
|
|
}
|
|
|
|
// Verify response headers
|
|
if got := w.Header().Get("Docker-Content-Digest"); got != "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd" {
|
|
t.Errorf("Docker-Content-Digest = %q, want digest", got)
|
|
}
|
|
}
|
|
|
|
// mockFetcherWithHeaders captures headers passed to FetchWithHeaders.
|
|
type mockFetcherWithHeaders struct {
|
|
fetchFn func(ctx context.Context, url string, headers http.Header) (*fetch.Artifact, error)
|
|
}
|
|
|
|
func (f *mockFetcherWithHeaders) Fetch(ctx context.Context, url string) (*fetch.Artifact, error) {
|
|
return f.FetchWithHeaders(ctx, url, nil)
|
|
}
|
|
|
|
func (f *mockFetcherWithHeaders) FetchWithHeaders(ctx context.Context, url string, headers http.Header) (*fetch.Artifact, error) {
|
|
return f.fetchFn(ctx, url, headers)
|
|
}
|
|
|
|
func (f *mockFetcherWithHeaders) Head(_ context.Context, _ string) (int64, string, error) {
|
|
return 0, "", nil
|
|
}
|
|
|
|
func TestContainerHandler_Routes_VersionCheck(t *testing.T) {
|
|
h := NewContainerHandler(nil, "http://localhost:8080")
|
|
|
|
handler := h.Routes()
|
|
if handler == nil {
|
|
t.Fatal("Routes() returned nil")
|
|
}
|
|
|
|
// Test /v2/ version check endpoint
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("version check: got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if got := w.Header().Get("Docker-Distribution-Api-Version"); got != "registry/2.0" {
|
|
t.Errorf("Docker-Distribution-Api-Version = %q, want %q", got, "registry/2.0")
|
|
}
|
|
}
|