2026-10-20 11:22:55 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ageMetadataAccess moves a cache row's last access an hour back and returns
|
|
|
|
|
// that time.
|
|
|
|
|
func ageMetadataAccess(t *testing.T, db *database.DB, ecosystem, key string) time.Time {
|
|
|
|
|
t.Helper()
|
|
|
|
|
old := time.Now().Add(-time.Hour).UTC()
|
|
|
|
|
query := db.Rebind(`UPDATE metadata_cache SET last_accessed_at = ? WHERE ecosystem = ? AND name = ?`)
|
|
|
|
|
if _, err := db.Exec(query, old, ecosystem, key); err != nil {
|
|
|
|
|
t.Fatalf("ageing last access: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return old
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertMetadataAccessedAfter(t *testing.T, db *database.DB, ecosystem, key string, old time.Time) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
entry, err := db.GetMetadataCache(ecosystem, key)
|
|
|
|
|
if err != nil || entry == nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache: %v, %v", entry, err)
|
|
|
|
|
}
|
|
|
|
|
if !entry.LastAccessedAt.Valid || !entry.LastAccessedAt.Time.After(old.Add(time.Minute)) {
|
|
|
|
|
t.Errorf("last_accessed_at = %v, want the time it was served from cache", entry.LastAccessedAt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFetchOrCacheMetadata_CacheHitMarksAccess(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write([]byte(`{"v":1}`))
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(upstream.Close)
|
|
|
|
|
|
|
|
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.CacheMetadata = true
|
|
|
|
|
proxy.MetadataTTL = time.Hour
|
|
|
|
|
proxy.HTTPClient = upstream.Client()
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
if _, _, err := proxy.FetchOrCacheMetadata(ctx, "test", "pkg", upstream.URL+"/pkg"); err != nil {
|
|
|
|
|
t.Fatalf("first fetch: %v", err)
|
|
|
|
|
}
|
|
|
|
|
old := ageMetadataAccess(t, db, "test", "pkg")
|
|
|
|
|
if _, _, err := proxy.FetchOrCacheMetadata(ctx, "test", "pkg", upstream.URL+"/pkg"); err != nil {
|
|
|
|
|
t.Fatalf("cached fetch: %v", err)
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, "test", "pkg", old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyCached_NotModifiedMarksAccess(t *testing.T) {
|
|
|
|
|
const body = `{"v":1}`
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("ETag", `"v1"`)
|
|
|
|
|
if r.Header.Get("If-None-Match") == `"v1"` {
|
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(upstream.Close)
|
|
|
|
|
|
|
|
|
|
proxy, db, store, _ := setupTestProxy(t)
|
|
|
|
|
proxy.CacheMetadata = true
|
|
|
|
|
proxy.MetadataTTL = time.Nanosecond
|
|
|
|
|
proxy.HTTPClient = upstream.Client()
|
|
|
|
|
|
|
|
|
|
for range 2 {
|
|
|
|
|
response := httptest.NewRecorder()
|
|
|
|
|
proxy.ProxyCached(response, httptest.NewRequest(http.MethodGet, "/pkg", nil), upstream.URL+"/pkg", "test", "pkg")
|
|
|
|
|
if response.Code != http.StatusOK || response.Body.String() != body {
|
|
|
|
|
t.Fatalf("response = %d %q, want 200 %q", response.Code, response.Body.String(), body)
|
|
|
|
|
}
|
|
|
|
|
if store.stores != 1 {
|
|
|
|
|
t.Fatalf("document stored %d times, want once", store.stores)
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, "test", "pkg", time.Now().Add(-time.Hour))
|
|
|
|
|
ageMetadataAccess(t, db, "test", "pkg")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoadContainerMetadata_MarksAccess(t *testing.T) {
|
|
|
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
|
|
|
h := NewContainerHandlerWithRegistry(proxy, "http://proxy.example.test", "")
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
if _, err := h.storeContainerMetadata(ctx, "oci-test", "key", []byte("{}"), "", "", "application/json", "", time.Time{}, time.Now()); err != nil {
|
|
|
|
|
t.Fatalf("storing: %v", err)
|
|
|
|
|
}
|
|
|
|
|
old := ageMetadataAccess(t, db, "oci-test", "key")
|
|
|
|
|
if entry, _, err := h.loadContainerMetadata(ctx, "oci-test", "key"); err != nil || entry == nil {
|
|
|
|
|
t.Fatalf("loading: %v, %v", entry, err)
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, "oci-test", "key", old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoadContainerManifest_MarksAccess(t *testing.T) {
|
|
|
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
|
|
|
h := NewContainerHandlerWithRegistry(proxy, "http://proxy.example.test", "")
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
manifest := &cachedContainerManifest{body: []byte("{}"), contentType: "application/json", fetchedAt: time.Now()}
|
|
|
|
|
if err := h.storeContainerManifest(ctx, "key", manifest); err != nil {
|
|
|
|
|
t.Fatalf("storing: %v", err)
|
|
|
|
|
}
|
|
|
|
|
old := ageMetadataAccess(t, db, containerManifestCacheEcosystem, "key")
|
|
|
|
|
if got, err := h.loadContainerManifest(ctx, "key"); err != nil || got == nil {
|
|
|
|
|
t.Fatalf("loading: %v, %v", got, err)
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, containerManifestCacheEcosystem, "key", old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoadContainerTags_MarksAccess(t *testing.T) {
|
|
|
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
|
|
|
h := NewContainerHandlerWithRegistry(proxy, "http://proxy.example.test", "")
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
tags := &cachedContainerTags{body: []byte(`{"tags":[]}`), contentType: "application/json", fetchedAt: time.Now()}
|
|
|
|
|
if err := h.storeContainerTags(ctx, "key", tags); err != nil {
|
|
|
|
|
t.Fatalf("storing: %v", err)
|
|
|
|
|
}
|
|
|
|
|
old := ageMetadataAccess(t, db, containerTagsCacheEcosystem, "key")
|
|
|
|
|
if got, err := h.loadContainerTags(ctx, "key"); err != nil || got == nil {
|
|
|
|
|
t.Fatalf("loading: %v, %v", got, err)
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, containerTagsCacheEcosystem, "key", old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestStoredRewrite_MarksAccess(t *testing.T) {
|
|
|
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.CacheMetadata = true
|
|
|
|
|
proxy.MetadataTTL = time.Hour
|
|
|
|
|
proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
|
|
|
|
|
|
|
|
body := []byte(`{}`)
|
|
|
|
|
proxy.cacheMetadataBlob(t.Context(), "npm", "demo", metadataStoragePath("npm", "demo"), &upstreamMetadata{body: body})
|
|
|
|
|
key := rewriteCacheKey("npm", "http://proxy", "demo", body)
|
2026-10-30 15:06:42 +01:00
|
|
|
if _, err := proxy.rewrites.rewrite(t.Context(), key, body, nil, func(b []byte) ([]byte, error) { return b, nil }); err != nil {
|
2026-10-20 11:22:55 +02:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
old := ageMetadataAccess(t, db, "npm", "demo")
|
|
|
|
|
if _, ok := proxy.storedRewrite("npm", "demo", "http://proxy", "demo"); !ok {
|
|
|
|
|
t.Fatal("expected a stored rewrite")
|
|
|
|
|
}
|
|
|
|
|
assertMetadataAccessedAfter(t, db, "npm", "demo", old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestArchHandler_RefreshRestoresEvictedBlob covers a database row whose blob
|
|
|
|
|
// size eviction removed while a refresh re-created the row: the next refresh
|
|
|
|
|
// with the same bytes stores the blob again, so the stale fallback works.
|
|
|
|
|
func TestArchHandler_RefreshRestoresEvictedBlob(t *testing.T) {
|
|
|
|
|
db := []byte("core-db-bytes")
|
|
|
|
|
upstream := newArchUpstream(t, map[string][]byte{"/core/os/x86_64/core.db": db})
|
|
|
|
|
proxy := newArchTestProxy(t, upstream)
|
|
|
|
|
h := NewArchHandler(proxy, "http://proxy.example", map[string]string{"archlinux": upstream.URL})
|
|
|
|
|
path := "/archlinux/core/os/x86_64/core.db"
|
|
|
|
|
|
|
|
|
|
assertArchBody(t, serveArchRequest(h, http.MethodGet, path, nil), "seed", db)
|
|
|
|
|
entry, err := proxy.DB.GetMetadataCache(archEcosystem, h.metadataCacheKey("archlinux", upstream.URL, "core/os/x86_64/core.db"))
|
|
|
|
|
if err != nil || entry == nil {
|
|
|
|
|
t.Fatalf("metadata entry: %v, %v", entry, err)
|
|
|
|
|
}
|
|
|
|
|
if err := proxy.Storage.Delete(t.Context(), entry.StoragePath); err != nil {
|
|
|
|
|
t.Fatalf("deleting blob: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ageMetadata(t, proxy, h, "archlinux", upstream.URL, "core/os/x86_64/core.db")
|
|
|
|
|
assertArchBody(t, serveArchRequest(h, http.MethodGet, path, nil), "refresh", db)
|
|
|
|
|
|
|
|
|
|
ageMetadata(t, proxy, h, "archlinux", upstream.URL, "core/os/x86_64/core.db")
|
|
|
|
|
upstream.available.Store(false)
|
|
|
|
|
assertArchBody(t, serveArchRequest(h, http.MethodGet, path, nil), "stale", db)
|
|
|
|
|
}
|