2026-10-20 21:33:08 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
// upstreamThen serves body once and answers every later request with status.
|
|
|
|
|
func upstreamThen(t *testing.T, body string, status int) *httptest.Server {
|
2026-10-20 21:33:08 +02:00
|
|
|
t.Helper()
|
|
|
|
|
var requests atomic.Int32
|
2026-10-22 09:14:33 +02:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
2026-10-20 21:33:08 +02:00
|
|
|
if requests.Add(1) == 1 {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2026-10-22 09:14:33 +02:00
|
|
|
_, _ = w.Write([]byte(body))
|
2026-10-20 21:33:08 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-10-22 09:14:33 +02:00
|
|
|
w.WriteHeader(status)
|
2026-10-20 21:33:08 +02:00
|
|
|
}))
|
|
|
|
|
t.Cleanup(srv.Close)
|
|
|
|
|
return srv
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
// staleProxy returns a proxy whose cached metadata expires right away.
|
|
|
|
|
func staleProxy(t *testing.T, upstream *httptest.Server) *Proxy {
|
|
|
|
|
t.Helper()
|
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
|
|
|
proxy.CacheMetadata = true
|
|
|
|
|
proxy.MetadataTTL = time.Millisecond
|
|
|
|
|
proxy.HTTPClient = upstream.Client()
|
|
|
|
|
return proxy
|
2026-10-20 21:33:08 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
// requestTwice requests path, lets the cached copy expire, and requests it
|
|
|
|
|
// again.
|
|
|
|
|
func requestTwice(t *testing.T, h http.Handler, path string) *httptest.ResponseRecorder {
|
|
|
|
|
t.Helper()
|
|
|
|
|
first := httptest.NewRecorder()
|
|
|
|
|
h.ServeHTTP(first, httptest.NewRequest(http.MethodGet, path, nil))
|
|
|
|
|
if first.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("first request: status = %d, body %s", first.Code, first.Body)
|
2026-10-21 12:23:33 +02:00
|
|
|
}
|
2026-10-22 09:14:33 +02:00
|
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
|
|
|
|
return w
|
|
|
|
|
}
|
2026-10-21 12:23:33 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
// staleStatuses are upstream answers the metadata cache falls back on.
|
|
|
|
|
var staleStatuses = []int{http.StatusBadGateway, http.StatusNotFound, http.StatusUnauthorized, http.StatusForbidden}
|
2026-10-21 12:23:33 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
const staleComposerDocument = `{"packages":{"acme/widget":[{"name":"acme/widget","version":"1.0.0",` +
|
|
|
|
|
`"dist":{"type":"zip","url":"https://repo.packagist.org/files/acme/widget/1.0.0/widget.zip"}}]},"minified":"composer/2.0"}`
|
2026-10-21 12:23:33 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
func TestComposerStaleMetadataIsMarked(t *testing.T) {
|
|
|
|
|
for _, status := range staleStatuses {
|
2026-10-20 21:33:08 +02:00
|
|
|
t.Run(http.StatusText(status), func(t *testing.T) {
|
2026-10-22 09:14:33 +02:00
|
|
|
upstream := upstreamThen(t, staleComposerDocument, status)
|
|
|
|
|
h := NewComposerHandlerWithUpstreams(staleProxy(t, upstream), "http://proxy.test", upstream.URL, upstream.URL).Routes()
|
2026-10-20 21:33:08 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
w := requestTwice(t, h, "/p2/acme/widget.json")
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200 from the cache, body %s", w.Code, w.Body)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Warning"); got != staleWarning {
|
|
|
|
|
t.Errorf("Warning = %q, want %q", got, staleWarning)
|
2026-10-20 21:33:08 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-20 21:34:07 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
const staleChefUniverse = `{"apt":{"7.5.0":{"location_type":"supermarket",` +
|
|
|
|
|
`"location_path":"https://supermarket.chef.io/api/v1","download_url":` +
|
|
|
|
|
`"https://supermarket.chef.io/api/v1/cookbooks/apt/versions/7.5.0/download","dependencies":{}}}}`
|
2026-10-20 21:34:07 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
func TestChefStaleMetadataIsMarked(t *testing.T) {
|
|
|
|
|
for _, status := range staleStatuses {
|
|
|
|
|
t.Run(http.StatusText(status), func(t *testing.T) {
|
|
|
|
|
upstream := upstreamThen(t, staleChefUniverse, status)
|
|
|
|
|
h := NewChefHandler(staleProxy(t, upstream), chefTestProxyURL, upstream.URL).Routes()
|
2026-10-20 21:34:07 +02:00
|
|
|
|
2026-10-22 09:14:33 +02:00
|
|
|
w := requestTwice(t, h, "/universe")
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200 from the cache, body %s", w.Code, w.Body)
|
2026-10-20 21:34:07 +02:00
|
|
|
}
|
2026-10-22 09:14:33 +02:00
|
|
|
if got := w.Header().Get("Warning"); got != staleWarning {
|
|
|
|
|
t.Errorf("Warning = %q, want %q", got, staleWarning)
|
2026-10-20 21:34:07 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|