pkg-proxy/internal/handler/container_test.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")
}
}