223 lines
6.7 KiB
Go
223 lines
6.7 KiB
Go
package steamcache
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func steamGet(t *testing.T, sc *SteamCache, path string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("User-Agent", "Valve/Steam HTTP Client 1.0")
|
|
rec := httptest.NewRecorder()
|
|
sc.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func TestNegativeCache404(t *testing.T) {
|
|
var upstreamHits atomic.Int64
|
|
f := func(w http.ResponseWriter, r *http.Request) {
|
|
upstreamHits.Add(1)
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}
|
|
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
|
|
sc.ResetMetrics()
|
|
|
|
rec1 := steamGet(t, sc, "/depot/gone/chunk")
|
|
if rec1.Code != http.StatusNotFound {
|
|
t.Fatalf("first request: expected 404, got %d body=%q", rec1.Code, rec1.Body.String())
|
|
}
|
|
if n := upstreamHits.Load(); n != 1 {
|
|
t.Fatalf("first request: expected 1 upstream hit, got %d", n)
|
|
}
|
|
|
|
rec2 := steamGet(t, sc, "/depot/gone/chunk")
|
|
if rec2.Code != http.StatusNotFound {
|
|
t.Fatalf("second request: expected 404, got %d", rec2.Code)
|
|
}
|
|
if n := upstreamHits.Load(); n != 1 {
|
|
t.Fatalf("repeated miss must not re-hit upstream within TTL, got %d", n)
|
|
}
|
|
|
|
stats := sc.GetMetrics()
|
|
if stats.NegativeCacheHits < 1 {
|
|
t.Errorf("expected NegativeCacheHits >= 1, got %d", stats.NegativeCacheHits)
|
|
}
|
|
if stats.CacheHits < 1 {
|
|
t.Errorf("negative hit should also count as cache_hits, got %d", stats.CacheHits)
|
|
}
|
|
if stats.UpstreamErrors != 1 {
|
|
t.Errorf("first 404 should count upstream error once, got %d", stats.UpstreamErrors)
|
|
}
|
|
|
|
mrec := httptest.NewRecorder()
|
|
sc.ServeHTTP(mrec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
|
if !bytes.Contains(mrec.Body.Bytes(), []byte("negative_cache_hits")) {
|
|
t.Errorf("/metrics missing negative_cache_hits:\n%s", mrec.Body.String())
|
|
}
|
|
if ct := mrec.Header().Get("Content-Type"); ct != "text/plain; version=0.0.4; charset=utf-8" {
|
|
t.Errorf("/metrics Content-Type=%q, want Prometheus text 0.0.4", ct)
|
|
}
|
|
}
|
|
|
|
func TestNegativeCache410(t *testing.T) {
|
|
var upstreamHits atomic.Int64
|
|
f := func(w http.ResponseWriter, r *http.Request) {
|
|
upstreamHits.Add(1)
|
|
w.WriteHeader(http.StatusGone)
|
|
}
|
|
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
|
|
|
|
rec1 := steamGet(t, sc, "/depot/gone410/chunk")
|
|
if rec1.Code != http.StatusGone {
|
|
t.Fatalf("first request: expected 410, got %d", rec1.Code)
|
|
}
|
|
rec2 := steamGet(t, sc, "/depot/gone410/chunk")
|
|
if rec2.Code != http.StatusGone {
|
|
t.Fatalf("second request: expected 410, got %d", rec2.Code)
|
|
}
|
|
if n := upstreamHits.Load(); n != 1 {
|
|
t.Fatalf("410 negative cache should suppress second upstream hit, got %d", n)
|
|
}
|
|
if sc.GetMetrics().NegativeCacheHits < 1 {
|
|
t.Errorf("expected NegativeCacheHits >= 1 after cached 410, got %d", sc.GetMetrics().NegativeCacheHits)
|
|
}
|
|
}
|
|
|
|
func TestNegativeCacheExpiredRefetch(t *testing.T) {
|
|
var upstreamHits atomic.Int64
|
|
f := func(w http.ResponseWriter, r *http.Request) {
|
|
upstreamHits.Add(1)
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}
|
|
s := httptest.NewServer(http.HandlerFunc(f))
|
|
t.Cleanup(s.Close)
|
|
|
|
sc, err := NewWithOptions(Options{
|
|
Address: "127.0.0.1:0",
|
|
MemorySize: "1MB",
|
|
DiskSize: "0",
|
|
DiskPath: t.TempDir(),
|
|
Upstream: s.URL,
|
|
MemoryGC: "lru",
|
|
DiskGC: "lru",
|
|
MaxConcurrentRequests: 10,
|
|
MaxRequestsPerClient: 5,
|
|
MaxObjectSize: "0",
|
|
NegativeTTL: "1s",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewWithOptions: %v", err)
|
|
}
|
|
t.Cleanup(func() { sc.Shutdown() })
|
|
|
|
if rec := steamGet(t, sc, "/depot/ttl/chunk"); rec.Code != http.StatusNotFound {
|
|
t.Fatalf("first request: expected 404, got %d", rec.Code)
|
|
}
|
|
if n := upstreamHits.Load(); n != 1 {
|
|
t.Fatalf("first request: expected 1 upstream hit, got %d", n)
|
|
}
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
rec := steamGet(t, sc, "/depot/ttl/chunk")
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 after expiry poll, got %d", rec.Code)
|
|
}
|
|
if upstreamHits.Load() >= 2 {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("expired negative entry did not re-fetch upstream; hits=%d", upstreamHits.Load())
|
|
}
|
|
|
|
func TestSerializeNegativeHeader(t *testing.T) {
|
|
raw := []byte("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\ngone")
|
|
pos, err := serializeRawResponse(raw)
|
|
if err != nil {
|
|
t.Fatalf("serialize positive: %v", err)
|
|
}
|
|
posFile, err := deserializeCacheFile(pos)
|
|
if err != nil {
|
|
t.Fatalf("deserialize positive: %v", err)
|
|
}
|
|
if posFile.ExpiresUnix != 0 {
|
|
t.Errorf("positive entry ExpiresUnix=%d, want 0", posFile.ExpiresUnix)
|
|
}
|
|
|
|
expires := time.Now().Add(5 * time.Minute).Unix()
|
|
neg, err := serializeCacheFile(raw, expires)
|
|
if err != nil {
|
|
t.Fatalf("serialize negative: %v", err)
|
|
}
|
|
negFile, err := deserializeCacheFile(neg)
|
|
if err != nil {
|
|
t.Fatalf("deserialize negative: %v", err)
|
|
}
|
|
if negFile.ExpiresUnix != expires {
|
|
t.Errorf("ExpiresUnix=%d, want %d", negFile.ExpiresUnix, expires)
|
|
}
|
|
if !bytes.Equal(negFile.Response, raw) {
|
|
t.Error("negative raw response not preserved")
|
|
}
|
|
}
|
|
|
|
func TestNewInvalidNegativeTTL(t *testing.T) {
|
|
sc, err := New("127.0.0.1:0", "1MB", "0", t.TempDir(), "", "lru", "lru", 10, 5, "0", nil, "not-a-duration", "", 0)
|
|
if err == nil {
|
|
if sc != nil {
|
|
sc.Shutdown()
|
|
}
|
|
t.Fatal("expected error for invalid negative ttl")
|
|
}
|
|
if sc != nil {
|
|
t.Error("expected nil SteamCache on invalid negative ttl")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid negative ttl") {
|
|
t.Errorf("err %q missing invalid negative ttl", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteTextNegativeCacheHits(t *testing.T) {
|
|
body := []byte("ok")
|
|
f := func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write(body)
|
|
}
|
|
sc, _ := newTestCacheWithFakeUpstream(t, f, "1MB", "0")
|
|
srv := newCacheServer(t, sc)
|
|
c := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/metrics", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest: %v", err)
|
|
}
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("GET /metrics: %v", err)
|
|
}
|
|
out, err := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("read /metrics: %v", err)
|
|
}
|
|
if !bytes.Contains(out, []byte("negative_cache_hits 0\n")) {
|
|
t.Errorf("/metrics missing negative_cache_hits 0:\n%s", out)
|
|
}
|
|
if ct := resp.Header.Get("Content-Type"); ct != "text/plain; version=0.0.4; charset=utf-8" {
|
|
t.Errorf("/metrics Content-Type=%q, want Prometheus text 0.0.4", ct)
|
|
}
|
|
if !bytes.Contains(out, []byte("# HELP negative_cache_hits ")) {
|
|
t.Errorf("/metrics missing # HELP negative_cache_hits:\n%s", out)
|
|
}
|
|
if !bytes.Contains(out, []byte("# TYPE negative_cache_hits counter")) {
|
|
t.Errorf("/metrics missing # TYPE negative_cache_hits counter:\n%s", out)
|
|
}
|
|
}
|