2026-10-18 21:32:02 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
"github.com/git-pkgs/cooldown"
|
2026-11-03 20:10:24 +01:00
|
|
|
registryclient "github.com/git-pkgs/registries/client"
|
2026-10-18 21:32:02 +02:00
|
|
|
"github.com/git-pkgs/registries/fetch"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// composerLookupUpstream serves v/p's metadata listing versions and the
|
|
|
|
|
// archives they point at, counts metadata and archive requests, and fails
|
2026-11-03 20:10:24 +01:00
|
|
|
// every request while down is set, with downStatus or 503. A version listed in
|
|
|
|
|
// published carries that publish time.
|
2026-10-18 21:32:02 +02:00
|
|
|
type composerLookupUpstream struct {
|
2026-11-03 20:10:24 +01:00
|
|
|
h http.Handler
|
|
|
|
|
handler *ComposerHandler
|
|
|
|
|
proxy *Proxy
|
|
|
|
|
versions atomic.Value // []string
|
|
|
|
|
published map[string]time.Time
|
|
|
|
|
down atomic.Bool
|
|
|
|
|
downStatus atomic.Int32
|
|
|
|
|
metadata atomic.Int32
|
|
|
|
|
archives atomic.Int32
|
|
|
|
|
routeURL string
|
2026-10-18 21:32:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setupComposerLookup(t *testing.T, cacheMetadata bool, versions ...string) *composerLookupUpstream {
|
|
|
|
|
t.Helper()
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
u := &composerLookupUpstream{published: map[string]time.Time{}}
|
2026-10-18 21:32:02 +02:00
|
|
|
u.versions.Store(versions)
|
|
|
|
|
var srv *httptest.Server
|
|
|
|
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if u.down.Load() {
|
2026-11-03 20:10:24 +01:00
|
|
|
status := int(u.downStatus.Load())
|
|
|
|
|
if status == 0 {
|
|
|
|
|
status = http.StatusServiceUnavailable
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Retry-After", "30")
|
|
|
|
|
http.Error(w, "down", status)
|
2026-10-18 21:32:02 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case r.URL.Path == "/p2/v/p.json":
|
|
|
|
|
u.metadata.Add(1)
|
|
|
|
|
var entries []string
|
|
|
|
|
for _, v := range u.versions.Load().([]string) {
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
var published string
|
|
|
|
|
if at, ok := u.published[v]; ok {
|
|
|
|
|
published = fmt.Sprintf(`"time":%q,`, at.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
entries = append(entries, fmt.Sprintf(`{"version":%q,%s"dist":{"url":%q,"type":"zip"}}`, v, published, srv.URL+"/archives/"+v+".zip"))
|
2026-10-18 21:32:02 +02:00
|
|
|
}
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"packages":{"v/p":[%s]}}`, strings.Join(entries, ","))
|
|
|
|
|
case r.URL.Path == "/p2/v/p~dev.json":
|
|
|
|
|
u.metadata.Add(1)
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
case strings.HasPrefix(r.URL.Path, "/archives/"):
|
|
|
|
|
u.archives.Add(1)
|
|
|
|
|
_, _ = io.WriteString(w, "archive "+r.URL.Path)
|
|
|
|
|
default:
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(srv.Close)
|
|
|
|
|
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.CacheMetadata = cacheMetadata
|
|
|
|
|
proxy.MetadataTTL = time.Hour
|
|
|
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
|
|
|
proxy.Fetcher = fetcher
|
|
|
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
u.proxy = proxy
|
|
|
|
|
u.handler = NewComposerHandlerWithUpstreams(proxy, "http://proxy", srv.URL, srv.URL)
|
|
|
|
|
u.h = u.handler.Routes()
|
|
|
|
|
u.routeURL = srv.URL
|
2026-10-18 21:32:02 +02:00
|
|
|
return u
|
|
|
|
|
}
|
|
|
|
|
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
// route sends v/p to a custom repository, which is the same server under
|
|
|
|
|
// another name.
|
|
|
|
|
func (u *composerLookupUpstream) route() {
|
|
|
|
|
u.h = u.handler.WithPackageRoutes(map[string]string{"v/*": u.routeURL}).Routes()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 21:32:02 +02:00
|
|
|
func (u *composerLookupUpstream) get(t *testing.T, target string) *httptest.ResponseRecorder {
|
|
|
|
|
t.Helper()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
u.h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
|
|
|
|
|
return w
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u *composerLookupUpstream) download(t *testing.T, version string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
w := u.get(t, "/files/v/p/"+version+"/"+version+".zip")
|
|
|
|
|
if want := "archive /archives/" + version + ".zip"; w.Code != http.StatusOK || w.Body.String() != want {
|
|
|
|
|
t.Fatalf("download %s: status %d, body %q, want 200 %q", version, w.Code, w.Body.String(), want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerDownloadResolvesFromCachedMetadata checks that downloads find
|
|
|
|
|
// their archive in the metadata Composer was just served, instead of fetching
|
|
|
|
|
// it from upstream again for every archive.
|
|
|
|
|
func TestComposerDownloadResolvesFromCachedMetadata(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, true, "1.0.0")
|
|
|
|
|
if w := u.get(t, "/p2/v/p.json"); w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("metadata: status %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
if n := u.metadata.Load(); n != 1 {
|
|
|
|
|
t.Errorf("upstream metadata requests = %d, want 1 (the metadata request itself)", n)
|
|
|
|
|
}
|
|
|
|
|
if n := u.archives.Load(); n != 1 {
|
|
|
|
|
t.Errorf("upstream archive requests = %d, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedArchiveServedWhileUpstreamDown checks that a cached
|
|
|
|
|
// archive stays installable when upstream fails, rather than failing on the
|
|
|
|
|
// metadata lookup that only names where to fetch it from.
|
|
|
|
|
func TestComposerCachedArchiveServedWhileUpstreamDown(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, true, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.down.Store(true)
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerDownloadOfVersionNewerThanCachedMetadata checks that a version
|
|
|
|
|
// published after the metadata was cached, which a lock file can name before
|
|
|
|
|
// the cached copy expires, is still looked up upstream.
|
|
|
|
|
func TestComposerDownloadOfVersionNewerThanCachedMetadata(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, true, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.versions.Store([]string{"1.0.0", "1.1.0"})
|
|
|
|
|
u.download(t, "1.1.0")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerDownloadWithoutMetadataCache checks that without cache_metadata
|
|
|
|
|
// a download asks upstream for the metadata once, as it always has.
|
|
|
|
|
func TestComposerDownloadWithoutMetadataCache(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
if n := u.metadata.Load(); n != 1 {
|
|
|
|
|
t.Errorf("upstream metadata requests = %d, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
|
2026-11-03 20:10:24 +01:00
|
|
|
// TestComposerDownloadLookupUpstreamError checks that a download whose
|
|
|
|
|
// metadata lookup fails upstream answers 502, or 503 while upstream holds
|
|
|
|
|
// requests, and not 404: Composer treats 404 as the version being gone.
|
|
|
|
|
func TestComposerDownloadLookupUpstreamError(t *testing.T) {
|
|
|
|
|
for _, status := range []int{http.StatusInternalServerError, http.StatusServiceUnavailable} {
|
|
|
|
|
t.Run(http.StatusText(status), func(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
u.downStatus.Store(int32(status))
|
|
|
|
|
u.down.Store(true)
|
|
|
|
|
if w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip"); w.Code != http.StatusBadGateway {
|
|
|
|
|
t.Errorf("status = %d, want 502", w.Code)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Run("rate limited", func(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
gate := registryclient.NewRateLimitGate(http.DefaultTransport)
|
|
|
|
|
u.proxy.HTTPClient = &http.Client{Transport: gate}
|
|
|
|
|
u.proxy.RateLimits = gate
|
|
|
|
|
u.downStatus.Store(http.StatusTooManyRequests)
|
|
|
|
|
u.down.Store(true)
|
|
|
|
|
w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip")
|
|
|
|
|
if w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") == "" {
|
|
|
|
|
t.Errorf("status = %d, Retry-After %q; want 503 with Retry-After", w.Code, w.Header().Get("Retry-After"))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("not listed", func(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
if w := u.get(t, "/files/v/p/2.0.0/2.0.0.zip"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("status = %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Serve cached Composer releases without a metadata lookup
Every Composer archive download resolved its dist from the package
metadata before checking the artifact cache. With cache_metadata off,
the default, that meant an uncoalesced upstream GET of the p2 document
for every download of an archive the proxy already had: three downloads
of a cached archive made three metadata requests. With the cache on,
each download after the TTL revalidated and rescanned the document.
The lookup is only needed for the publish time cooldown checks
downloads against, since lock files name archive URLs directly. Like
the npm handler, record that time in the versions table when the dist
is first resolved, and on later downloads of a tagged release read it
from there and serve the cached archive straight away. Without a stored
time, as for archives cached by an older proxy, the download takes the
metadata path, which then records it. The time is stored even with
cooldown off, so enabling cooldown later keeps the fast path.
The cache still enforces the denylist and verifies the stored digest,
and an archive is only ever cached under a name that was checked
against the metadata, so those guarantees hold. Branches, whose
archive and time change with every push, routed packages, whose
versions row would not say which repository the time came from, and
requests with a query string such as ?sha= keep resolving from the
metadata.
2026-10-23 13:22:56 +02:00
|
|
|
// storedPublishTime returns the publish time recorded for v/p at version.
|
|
|
|
|
func (u *composerLookupUpstream) storedPublishTime(t *testing.T, version string) (time.Time, bool) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
ver, err := u.proxy.DB.GetVersionByPURL(canonicalVersionPURL("composer", "v/p", version))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("reading versions row: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if ver == nil || !ver.PublishedAt.Valid {
|
|
|
|
|
return time.Time{}, false
|
|
|
|
|
}
|
|
|
|
|
return ver.PublishedAt.Time, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u *composerLookupUpstream) wantMetadataRequests(t *testing.T, want int32) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if n := u.metadata.Load(); n != want {
|
|
|
|
|
t.Errorf("upstream metadata requests = %d, want %d", n, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedReleaseSkipsMetadata checks that a tagged release whose
|
|
|
|
|
// archive is cached is served without asking upstream for the metadata
|
|
|
|
|
// again, with and without cache_metadata, and with cooldown, which takes the
|
|
|
|
|
// publish time recorded when the archive was first resolved.
|
|
|
|
|
func TestComposerCachedReleaseSkipsMetadata(t *testing.T) {
|
|
|
|
|
for _, tt := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
cacheMetadata bool
|
|
|
|
|
cooldown bool
|
|
|
|
|
}{
|
|
|
|
|
{"metadata not cached", false, false},
|
|
|
|
|
{"metadata cached", true, false},
|
|
|
|
|
{"cooldown", false, true},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, tt.cacheMetadata, "1.0.0")
|
|
|
|
|
u.published["1.0.0"] = time.Now().Add(-30 * 24 * time.Hour)
|
|
|
|
|
if tt.cooldown {
|
|
|
|
|
u.proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
|
}
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
// With cache_metadata the cached copy expires; the archive must
|
|
|
|
|
// not need it.
|
|
|
|
|
u.proxy.MetadataTTL = 0
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.wantMetadataRequests(t, 1)
|
|
|
|
|
if n := u.archives.Load(); n != 1 {
|
|
|
|
|
t.Errorf("upstream archive requests = %d, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerDownloadStoresPublishTime checks that resolving a tagged
|
|
|
|
|
// release records its publish time, so later downloads can check cooldown
|
|
|
|
|
// without the metadata.
|
|
|
|
|
func TestComposerDownloadStoresPublishTime(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
published := time.Date(2024, 5, 6, 7, 8, 9, 0, time.UTC)
|
|
|
|
|
u.published["1.0.0"] = published
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
got, ok := u.storedPublishTime(t, "1.0.0")
|
|
|
|
|
if !ok || !got.Equal(published) {
|
|
|
|
|
t.Errorf("stored publish time = %v, %v, want %v", got, ok, published)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedReleaseCooldownFromStoredTime checks that a cached
|
|
|
|
|
// archive whose stored publish time is still inside the cooldown window is
|
|
|
|
|
// withheld without the metadata, including when cooldown was enabled or
|
|
|
|
|
// raised after the archive was cached.
|
|
|
|
|
func TestComposerCachedReleaseCooldownFromStoredTime(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0", "2.0.0")
|
|
|
|
|
u.published["1.0.0"] = time.Now().Add(-5 * 24 * time.Hour)
|
|
|
|
|
u.published["2.0.0"] = time.Now().Add(-1 * time.Hour)
|
|
|
|
|
u.download(t, "2.0.0")
|
|
|
|
|
u.proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
|
|
|
|
|
if w := u.get(t, "/files/v/p/2.0.0/2.0.0.zip"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("2.0.0 after enabling cooldown: status %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
u.proxy.Cooldown = &cooldown.Config{Default: "7d"}
|
|
|
|
|
if w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("1.0.0 after raising cooldown: status %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
u.wantMetadataRequests(t, 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedReleaseWithoutStoredTime checks that with cooldown a
|
|
|
|
|
// cached archive the proxy recorded no publish time for, such as one cached
|
|
|
|
|
// by an older version, is checked against the metadata, which then records
|
|
|
|
|
// the time.
|
|
|
|
|
func TestComposerCachedReleaseWithoutStoredTime(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
u.published["1.0.0"] = time.Now().Add(-1 * time.Hour)
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
if _, err := u.proxy.DB.Exec(`UPDATE versions SET published_at = NULL`); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u.proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
|
if w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("status %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
u.wantMetadataRequests(t, 2)
|
|
|
|
|
if _, ok := u.storedPublishTime(t, "1.0.0"); !ok {
|
|
|
|
|
t.Error("publish time not recorded from the metadata")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedDevVersionUsesMetadata checks that a branch, whose
|
|
|
|
|
// archive and publish time change with every push, is still resolved from
|
|
|
|
|
// the metadata on each download and records no publish time.
|
|
|
|
|
func TestComposerCachedDevVersionUsesMetadata(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "dev-main")
|
|
|
|
|
u.published["dev-main"] = time.Now().Add(-30 * 24 * time.Hour)
|
|
|
|
|
u.download(t, "dev-main")
|
|
|
|
|
before := u.metadata.Load()
|
|
|
|
|
u.download(t, "dev-main")
|
|
|
|
|
if u.metadata.Load() == before {
|
|
|
|
|
t.Error("cached branch archive served without consulting the metadata")
|
|
|
|
|
}
|
|
|
|
|
if _, ok := u.storedPublishTime(t, "dev-main"); ok {
|
|
|
|
|
t.Error("publish time recorded for a branch")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedRoutedReleaseUsesMetadata checks that a routed package
|
|
|
|
|
// keeps resolving every download from its route's metadata: the versions
|
|
|
|
|
// table is keyed by a PURL that does not say which repository the version
|
|
|
|
|
// came from, so it neither stores nor trusts a publish time for it.
|
|
|
|
|
func TestComposerCachedRoutedReleaseUsesMetadata(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
u.published["1.0.0"] = time.Now().Add(-1 * time.Hour)
|
|
|
|
|
u.route()
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
u.wantMetadataRequests(t, 2)
|
|
|
|
|
if _, ok := u.storedPublishTime(t, "1.0.0"); ok {
|
|
|
|
|
t.Error("publish time recorded for a routed package")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A time stored for the public package of the same name is not used.
|
|
|
|
|
if err := u.proxy.DB.SetVersionPublishedAt(canonicalVersionPURL("composer", "v/p", "1.0.0"),
|
|
|
|
|
canonicalPackagePURL("composer", "v/p"), time.Now().Add(-30*24*time.Hour)); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
u.proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
|
if w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip"); w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("routed release in cooldown: status %d, want 404", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestComposerCachedReleaseDenied checks that the denylist still blocks a
|
|
|
|
|
// cached release served without the metadata.
|
|
|
|
|
func TestComposerCachedReleaseDenied(t *testing.T) {
|
|
|
|
|
u := setupComposerLookup(t, false, "1.0.0")
|
|
|
|
|
u.download(t, "1.0.0")
|
|
|
|
|
setTestDenylist(t, u.proxy, "pkg:composer/v/p@1.0.0")
|
|
|
|
|
if w := u.get(t, "/files/v/p/1.0.0/1.0.0.zip"); w.Code != http.StatusForbidden {
|
|
|
|
|
t.Errorf("status %d, want 403", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|