2026-11-05 18:41:03 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-11-05 20:00:19 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-11-05 18:41:03 +00:00
|
|
|
"net/http"
|
2026-11-05 20:00:19 +00:00
|
|
|
"net/http/httptest"
|
2026-11-05 18:41:03 +00:00
|
|
|
"testing"
|
2026-11-05 20:00:19 +00:00
|
|
|
"time"
|
2026-11-05 18:41:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestGemDownloadSharesPublishTimeMetadata(t *testing.T) {
|
|
|
|
|
u, proxy, h := gemCacheSetup(t)
|
|
|
|
|
wantStatus(t, serveGet(t, h, gemInfoPath), http.StatusOK, "")
|
|
|
|
|
seedPackage(t, proxy.DB, proxy.Storage.(*mockStorage), "gem", "testgem", "1.0.0", "testgem-1.0.0.gem", "gem bytes")
|
|
|
|
|
wantStatus(t, serveGet(t, h, "/gems/testgem-1.0.0.gem"), http.StatusOK, "")
|
|
|
|
|
if n, _ := u.counts(gemVersionsPath); n != 1 {
|
|
|
|
|
t.Fatalf("versions API fetched %d times, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 20:00:19 +00:00
|
|
|
type downloadPublishTimeCase struct {
|
|
|
|
|
name string
|
|
|
|
|
down bool
|
|
|
|
|
absent bool
|
|
|
|
|
untimed bool
|
|
|
|
|
refresh bool
|
|
|
|
|
wantStatus int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDownloadCooldownRefreshesMissingPublishTimes(t *testing.T) {
|
|
|
|
|
for _, c := range downloadCooldownCases() {
|
|
|
|
|
if c.ecosystem != "gem" && c.ecosystem != "hex" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, scenario := range []downloadPublishTimeCase{
|
|
|
|
|
{name: "refreshed times", refresh: true, wantStatus: http.StatusNotFound},
|
|
|
|
|
{name: "stale times and listed version", down: true, wantStatus: http.StatusNotFound},
|
|
|
|
|
{name: "stale times and absent version", down: true, absent: true, wantStatus: http.StatusOK},
|
|
|
|
|
{name: "fresh times without version", wantStatus: http.StatusOK},
|
|
|
|
|
{name: "stale times with untimed version", down: true, untimed: true, wantStatus: http.StatusOK},
|
|
|
|
|
} {
|
|
|
|
|
for _, cached := range []bool{false, true} {
|
|
|
|
|
if !cached && scenario.wantStatus == http.StatusOK {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
t.Run(fmt.Sprintf("%s/%s/cached=%t", c.ecosystem, scenario.name, cached), func(t *testing.T) {
|
|
|
|
|
assertDownloadPublishTime(t, c, scenario, cached)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func assertDownloadPublishTime(t *testing.T, c downloadCooldownCase, scenario downloadPublishTimeCase, cached bool) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
u := newCooldownCountingUpstream(t)
|
|
|
|
|
proxy := cooldownCacheProxy(t, u)
|
|
|
|
|
fetcher := proxy.Fetcher.(*mockFetcher)
|
|
|
|
|
fetcher.fetchErr = errors.New("unchecked artifact download")
|
|
|
|
|
h := c.handler(proxy, u.URL)
|
|
|
|
|
docPath, timesPath := "/info/demo", "/api/v1/versions/demo.json"
|
|
|
|
|
if c.ecosystem == "hex" {
|
|
|
|
|
docPath, timesPath = "/packages/demo", "/api/packages/demo"
|
|
|
|
|
}
|
|
|
|
|
oldTimes := map[string]time.Time{cooldownOldVersion: time.Now().Add(-30 * 24 * time.Hour)}
|
|
|
|
|
if scenario.untimed {
|
|
|
|
|
oldTimes[cooldownRecentVersion] = time.Time{}
|
|
|
|
|
}
|
|
|
|
|
setDownloadMetadata(t, u, c, docPath, oldTimes)
|
|
|
|
|
setDownloadMetadata(t, u, c, timesPath, oldTimes)
|
|
|
|
|
wantStatus(t, serveGet(t, h, docPath), http.StatusOK, "")
|
|
|
|
|
|
|
|
|
|
newTimes := map[string]time.Time{
|
|
|
|
|
cooldownOldVersion: oldTimes[cooldownOldVersion],
|
|
|
|
|
cooldownUntimed: time.Now().Add(-time.Minute),
|
|
|
|
|
}
|
|
|
|
|
if !scenario.absent {
|
|
|
|
|
newTimes[cooldownRecentVersion] = time.Now().Add(-time.Hour)
|
|
|
|
|
}
|
|
|
|
|
setDownloadMetadata(t, u, c, docPath, newTimes)
|
|
|
|
|
expireCachedMetadata(t, proxy, c.ecosystem, docPath[1:])
|
|
|
|
|
if scenario.refresh {
|
|
|
|
|
setDownloadMetadata(t, u, c, timesPath, newTimes)
|
|
|
|
|
}
|
|
|
|
|
u.setDown(timesPath, scenario.down)
|
|
|
|
|
if cached {
|
|
|
|
|
c.seedCachedArtifact(t, proxy, cooldownRecentVersion)
|
|
|
|
|
}
|
|
|
|
|
wantStatus(t, serveGet(t, h, c.path(cooldownRecentVersion)), scenario.wantStatus, "")
|
|
|
|
|
if fetcher.fetchCalled {
|
|
|
|
|
t.Error("fetched an unchecked artifact")
|
|
|
|
|
}
|
|
|
|
|
if n, _ := u.counts(timesPath); n != 2 {
|
|
|
|
|
t.Errorf("publish times fetched %d times, want 2", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setDownloadMetadata(t *testing.T, u *cooldownCountingUpstream, c downloadCooldownCase, path string, times map[string]time.Time) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
c.metadata(w, httptest.NewRequest(http.MethodGet, path, nil), times)
|
|
|
|
|
wantStatus(t, w, http.StatusOK, "")
|
|
|
|
|
u.set(path, w.Body.Bytes())
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 18:41:03 +00:00
|
|
|
func TestHexDownloadSharesPublishTimeMetadata(t *testing.T) {
|
|
|
|
|
u, proxy, h, _ := hexCacheSetup(t, 0)
|
|
|
|
|
wantStatus(t, serveGet(t, h, hexPackagePath), http.StatusOK, "")
|
|
|
|
|
seedPackage(t, proxy.DB, proxy.Storage.(*mockStorage), "hex", "testpkg", "1.0.0", "testpkg-1.0.0.tar", "hex bytes")
|
|
|
|
|
wantStatus(t, serveGet(t, h, "/tarballs/testpkg-1.0.0.tar"), http.StatusOK, "")
|
|
|
|
|
if n, _ := u.counts(hexAPIPath); n != 1 {
|
|
|
|
|
t.Fatalf("Hex API fetched %d times, want 1", n)
|
|
|
|
|
}
|
|
|
|
|
}
|