Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/server/cooldown_profiles_test.go

253 lines
8.5 KiB
Go
Raw Permalink Normal View History

package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/git-pkgs/proxy/internal/config"
)
func TestCooldownProfilesThroughServerStart(t *testing.T) {
if os.Getenv("PROXY_TEST_PROFILES") != "1" {
cmd := exec.Command(os.Args[0], "-test.run=^TestCooldownProfilesThroughServerStart$")
cmd.Env = append(os.Environ(), "PROXY_TEST_PROFILES=1")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("profile server: %v\n%s", err, output)
}
return
}
upstream, counts := profileTestUpstream(t)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = listener.Close() })
cfg := config.Default()
cfg.BaseURL = "http://" + listener.Addr().String()
cfg.Database.Path = filepath.Join(t.TempDir(), "cache.db")
cfg.Storage.URL = "file://" + t.TempDir()
cfg.CacheMetadata = true
cfg.MetadataTTL = "1h"
cfg.Upstream.AllowLoopback = true
cfg.Upstream.NPM = upstream.URL + "/npm"
cfg.Upstream.Cargo = upstream.URL + "/cargo"
cfg.Upstream.CargoDownload = upstream.URL + "/crates"
cfg.Upstream.Gem = upstream.URL + "/gem"
cfg.Upstream.Go = upstream.URL + "/go"
cfg.Cooldown = config.CooldownConfig{Profiles: map[string]config.CooldownConfig{
"3d": {Default: "3d"}, "7d": {Default: "7d"},
}}
if err := cfg.Validate(); err != nil {
t.Fatal(err)
}
s, err := New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil)), BuildInfo{})
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- s.Start(listener) }()
client := &http.Client{Timeout: 5 * time.Second}
defer func() {
client.CloseIdleConnections()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
t.Error(err)
}
if err := <-done; !errors.Is(err, http.ErrServerClosed) {
t.Error(err)
}
}()
get := func(path string, headers map[string]string) (*http.Response, string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, cfg.BaseURL+path, nil)
if err != nil {
t.Fatal(err)
}
for name, value := range headers {
req.Header.Set(name, value)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return resp, string(body)
}
assertProfileVersionLists(t, get)
assertProfileRouting(t, cfg.BaseURL, get, counts)
assertConcurrentProfileRequests(t, client, cfg.BaseURL)
}
type profileGetter func(string, map[string]string) (*http.Response, string)
func assertProfileVersionLists(t *testing.T, get profileGetter) {
t.Helper()
for _, registry := range []struct{ path, old, middle, newest string }{
{"npm/demo", `"1.0.0"`, `"2.0.0"`, `"3.0.0"`},
{"cargo/de/mo/demo", `"vers":"1.0.0"`, `"vers":"2.0.0"`, `"vers":"3.0.0"`},
{"gem/info/demo", "1.0.0 |", "2.0.0 |", "3.0.0 |"},
{"go/example.com/demo/@v/list", "v1.0.0", "v2.0.0", "v3.0.0"},
} {
t.Run(registry.path, func(t *testing.T) {
for _, profile := range []struct {
prefix string
middle, newest bool
}{
{"/cooldown/3d/", true, false}, {"/cooldown/7d/", false, false}, {"/", true, true},
} {
resp, body := get(profile.prefix+registry.path, nil)
if resp.StatusCode != http.StatusOK || !strings.Contains(body, registry.old) ||
strings.Contains(body, registry.middle) != profile.middle || strings.Contains(body, registry.newest) != profile.newest {
t.Fatalf("%s%s: status %d, body %s", profile.prefix, registry.path, resp.StatusCode, body)
}
}
})
}
}
func assertProfileRouting(t *testing.T, baseURL string, get profileGetter, counts func(string) int) {
t.Helper()
for _, profile := range []string{"3d", "7d"} {
_, body := get("/cooldown/"+profile+"/cargo/config.json", nil)
if !strings.Contains(body, baseURL+"/cooldown/"+profile+"/cargo/crates/") {
t.Fatalf("Cargo download URL lost profile: %s", body)
}
_, body = get("/cooldown/"+profile+"/npm/demo", nil)
if !strings.Contains(body, baseURL+"/cooldown/"+profile+"/npm/demo/-/") {
t.Fatalf("npm download URL lost profile: %s", body)
}
}
loose, _ := get("/cooldown/3d/npm/demo", nil)
strict, _ := get("/cooldown/7d/npm/demo", map[string]string{"If-None-Match": loose.Header.Get("ETag")})
if strict.StatusCode != http.StatusOK || strict.Header.Get("ETag") == loose.Header.Get("ETag") {
t.Fatalf("cross-profile validator returned status %d", strict.StatusCode)
}
resp, _ := get("/cooldown/7d/npm/demo", map[string]string{"If-None-Match": strict.Header.Get("ETag")})
if resp.StatusCode != http.StatusNotModified {
t.Fatalf("same-profile revalidation returned %d", resp.StatusCode)
}
for _, tc := range []struct {
path string
status int
}{
{"/cooldown/3d/npm/demo/-/demo-2.0.0.tgz", http.StatusOK},
{"/cooldown/7d/npm/demo/-/demo-2.0.0.tgz", http.StatusNotFound},
{"/cooldown/3d/npm/demo/-/demo-2.0.0.tgz", http.StatusOK},
{"/cooldown/7d/go/example.com/demo/@v/v2.0.0.mod", http.StatusForbidden},
{"/cooldown/unknown/npm/demo", http.StatusNotFound},
{"/cooldown/3d/ui/", http.StatusNotFound},
{"/cooldown/3d/api/packages", http.StatusNotFound},
{"/cooldown/3d/metrics", http.StatusNotFound},
} {
resp, _ := get(tc.path, nil)
if resp.StatusCode != tc.status {
t.Errorf("%s: status %d, want %d", tc.path, resp.StatusCode, tc.status)
}
}
if got := counts("/npm/demo/-/demo-2.0.0.tgz"); got != 1 {
t.Errorf("artifact upstream fetches = %d, want 1", got)
}
}
func assertConcurrentProfileRequests(t *testing.T, client *http.Client, baseURL string) {
t.Helper()
var wg sync.WaitGroup
for range 8 {
for _, profile := range []string{"3d", "7d"} {
wg.Go(func() {
resp, err := client.Get(baseURL + "/cooldown/" + profile + "/npm/demo")
if err != nil {
t.Error(err)
return
}
defer func() { _ = resp.Body.Close() }()
var doc struct{ Versions map[string]json.RawMessage }
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
t.Error(err)
return
}
_, middle := doc.Versions["2.0.0"]
if middle != (profile == "3d") {
t.Errorf("%s served another profile's versions", profile)
}
})
}
}
wg.Wait()
}
func profileTestUpstream(t *testing.T) (*httptest.Server, func(string) int) {
t.Helper()
var mu sync.Mutex
counts := make(map[string]int)
ages := []time.Duration{10 * 24 * time.Hour, 5 * 24 * time.Hour, 24 * time.Hour}
var upstream *httptest.Server
upstream = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
counts[r.URL.Path]++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/npm/demo":
versions := map[string]any{}
times := map[string]string{}
for i, age := range ages {
version := fmt.Sprintf("%d.0.0", i+1)
times[version] = time.Now().Add(-age).UTC().Format(time.RFC3339)
versions[version] = map[string]any{"name": "demo", "version": version, "dist": map[string]string{
"tarball": upstream.URL + "/npm/demo/-/demo-" + version + ".tgz",
}}
}
_ = json.NewEncoder(w).Encode(map[string]any{"name": "demo", "versions": versions, "time": times, "dist-tags": map[string]string{"latest": "3.0.0"}})
case "/npm/demo/-/demo-2.0.0.tgz":
_, _ = io.WriteString(w, "cached package bytes")
case "/cargo/de/mo/demo":
for i, age := range ages {
_, _ = fmt.Fprintf(w, "{\"name\":\"demo\",\"vers\":\"%d.0.0\",\"deps\":[],\"features\":{},\"yanked\":false,\"pubtime\":%q}\n", i+1, time.Now().Add(-age).UTC().Format(time.RFC3339))
}
case "/gem/info/demo":
_, _ = io.WriteString(w, "---\n1.0.0 |checksum:a\n2.0.0 |checksum:b\n3.0.0 |checksum:c\n")
case "/gem/api/v1/versions/demo.json":
versions := make([]map[string]string, 0, len(ages))
for i, age := range ages {
versions = append(versions, map[string]string{"number": fmt.Sprintf("%d.0.0", i+1), "created_at": time.Now().Add(-age).UTC().Format(time.RFC3339)})
}
_ = json.NewEncoder(w).Encode(versions)
case "/go/example.com/demo/@v/list":
_, _ = io.WriteString(w, "v1.0.0\nv2.0.0\nv3.0.0\n")
default:
for i, age := range ages {
if r.URL.Path == fmt.Sprintf("/go/example.com/demo/@v/v%d.0.0.info", i+1) {
_ = json.NewEncoder(w).Encode(map[string]string{"Version": fmt.Sprintf("v%d.0.0", i+1), "Time": time.Now().Add(-age).UTC().Format(time.RFC3339)})
return
}
}
http.NotFound(w, r)
}
}))
t.Cleanup(upstream.Close)
return upstream, func(path string) int {
mu.Lock()
defer mu.Unlock()
return counts[path]
}
}