mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-09-16 15:52:05 -04:00
160 lines
5.2 KiB
Go
160 lines
5.2 KiB
Go
|
|
package handler
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// These tests cover an upstream re-publishing a version: the cache holds one
|
||
|
|
// artifact and upstream now declares another digest for it.
|
||
|
|
|
||
|
|
const (
|
||
|
|
stalePkgPURL = "pkg:npm/pkg"
|
||
|
|
staleVersionPURL = "pkg:npm/pkg@1.0.0"
|
||
|
|
staleFilename = "pkg-1.0.0.tgz"
|
||
|
|
staleStoragePath = "npm/pkg/1.0.0/pkg-1.0.0.tgz"
|
||
|
|
staleURL = "https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz"
|
||
|
|
)
|
||
|
|
|
||
|
|
// seedCachedArtifact commits content the way a fetch does.
|
||
|
|
func seedCachedArtifact(t *testing.T, proxy *Proxy, store *mockStorage, content string) {
|
||
|
|
t.Helper()
|
||
|
|
ctx := context.Background()
|
||
|
|
if _, _, err := store.Store(ctx, staleStoragePath, strings.NewReader(content)); err != nil {
|
||
|
|
t.Fatalf("seeding storage: %v", err)
|
||
|
|
}
|
||
|
|
artifact := testArtifact(content, staleVersionPURL, staleFilename, "application/gzip")
|
||
|
|
if err := proxy.updateCacheDB("npm", "pkg", stalePkgPURL, staleURL, staleStoragePath, artifact); err != nil {
|
||
|
|
t.Fatalf("seeding cache record: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// cachedDigest reports the digest the cache record holds, or "" without one.
|
||
|
|
func cachedDigest(t *testing.T, proxy *Proxy) string {
|
||
|
|
t.Helper()
|
||
|
|
record, err := proxy.DB.GetCachedArtifact(stalePkgPURL, staleVersionPURL, staleFilename)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("reading cache record: %v", err)
|
||
|
|
}
|
||
|
|
if record == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return record.Artifact.Digest.Encoded()
|
||
|
|
}
|
||
|
|
|
||
|
|
func bytesPresent(store *mockStorage) bool {
|
||
|
|
r, err := store.Open(context.Background(), staleStoragePath)
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
_ = r.Close()
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStaleCacheCheckHasNoSideEffects(t *testing.T) {
|
||
|
|
proxy, _, store, _ := setupTestProxy(t)
|
||
|
|
seedCachedArtifact(t, proxy, store, "old bytes")
|
||
|
|
|
||
|
|
res, err := proxy.getCachedArtifactWithUpstreamHash(context.Background(),
|
||
|
|
stalePkgPURL, staleVersionPURL, staleFilename, sha256Hex("new bytes"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("cache check failed: %v", err)
|
||
|
|
}
|
||
|
|
if res != nil {
|
||
|
|
drain(res)
|
||
|
|
t.Fatal("stale entry was served")
|
||
|
|
}
|
||
|
|
if got := cachedDigest(t, proxy); got != sha256Hex("old bytes") {
|
||
|
|
t.Errorf("record digest = %q, want the stale one kept: the check must not discard", got)
|
||
|
|
}
|
||
|
|
if !bytesPresent(store) {
|
||
|
|
t.Error("stale bytes were deleted by the check")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStaleCacheIsDiscardedBeforeTheFetch(t *testing.T) {
|
||
|
|
proxy, _, store, fetcher := setupTestProxy(t)
|
||
|
|
seedCachedArtifact(t, proxy, store, "old bytes")
|
||
|
|
boom := errors.New("upstream unavailable")
|
||
|
|
fetcher.fetchErr = boom
|
||
|
|
|
||
|
|
_, err := proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
|
||
|
|
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+sha256Hex("new bytes"))
|
||
|
|
if !errors.Is(err, boom) {
|
||
|
|
t.Fatalf("got %v, want the fetch failure", err)
|
||
|
|
}
|
||
|
|
if got := cachedDigest(t, proxy); got != "" {
|
||
|
|
t.Errorf("stale record survived a failed refresh, digest = %q", got)
|
||
|
|
}
|
||
|
|
if bytesPresent(store) {
|
||
|
|
t.Error("stale bytes survived a failed refresh")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStaleCacheIsReplacedByTheFetch(t *testing.T) {
|
||
|
|
proxy, _, store, fetcher := setupTestProxy(t)
|
||
|
|
seedCachedArtifact(t, proxy, store, "old bytes")
|
||
|
|
fetcher.artifact = artifactBody("new bytes")
|
||
|
|
upstream := sha256Hex("new bytes")
|
||
|
|
|
||
|
|
res, err := proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
|
||
|
|
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+upstream)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("refresh failed: %v", err)
|
||
|
|
}
|
||
|
|
got, err := io.ReadAll(res.Reader)
|
||
|
|
_ = res.Reader.Close()
|
||
|
|
if err != nil || string(got) != "new bytes" {
|
||
|
|
t.Fatalf("got %q (err %v), want the refreshed bytes", got, err)
|
||
|
|
}
|
||
|
|
if !fetcher.fetchCalled {
|
||
|
|
t.Error("stale entry was served without a fetch")
|
||
|
|
}
|
||
|
|
if d := cachedDigest(t, proxy); d != upstream {
|
||
|
|
t.Errorf("record digest = %q, want %q", d, upstream)
|
||
|
|
}
|
||
|
|
|
||
|
|
fetcher.fetchCalled = false
|
||
|
|
res, err = proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
|
||
|
|
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+upstream)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("request after refresh failed: %v", err)
|
||
|
|
}
|
||
|
|
drain(res)
|
||
|
|
if fetcher.fetchCalled || !res.Cached {
|
||
|
|
t.Errorf("request after refresh: fetched=%v cached=%v, want served from cache", fetcher.fetchCalled, res.Cached)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLateLeaderKeepsRefreshedEntry is the race, at the point it would happen:
|
||
|
|
// a caller whose cache check saw a stale entry reaches the coalescing step
|
||
|
|
// after another caller's fetch replaced it. It must serve the replacement.
|
||
|
|
func TestLateLeaderKeepsRefreshedEntry(t *testing.T) {
|
||
|
|
proxy, _, store, fetcher := setupTestProxy(t)
|
||
|
|
seedCachedArtifact(t, proxy, store, "new bytes")
|
||
|
|
fetcher.fetchErr = errors.New("must not fetch")
|
||
|
|
upstream := sha256Hex("new bytes")
|
||
|
|
|
||
|
|
res, err := proxy.coalescedFetchFromURL(context.Background(),
|
||
|
|
"npm", "pkg", "1.0.0", staleFilename, stalePkgPURL, staleVersionPURL, staleURL, nil, upstream)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("late leader failed: %v", err)
|
||
|
|
}
|
||
|
|
got, err := io.ReadAll(res.Reader)
|
||
|
|
_ = res.Reader.Close()
|
||
|
|
if err != nil || string(got) != "new bytes" {
|
||
|
|
t.Fatalf("got %q (err %v), want the refreshed bytes", got, err)
|
||
|
|
}
|
||
|
|
if fetcher.fetchCalled {
|
||
|
|
t.Error("refreshed entry was fetched again")
|
||
|
|
}
|
||
|
|
if d := cachedDigest(t, proxy); d != upstream {
|
||
|
|
t.Errorf("refreshed record was discarded, digest = %q", d)
|
||
|
|
}
|
||
|
|
if !bytesPresent(store) {
|
||
|
|
t.Error("refreshed bytes were deleted")
|
||
|
|
}
|
||
|
|
}
|