Files
pixa/internal/imgcache/max_age_internal_test.go
T
clawbot 8feed69143
check / check (push) Canceled after 0s
Check cached sources with upstream after the cache TTL (closes #69, closes #83)
A cached source and its variants were served until eviction, so a
changed image at the same URL was never picked up.

Once a source was last checked 24 hours ago, the next request for one
of its images asks upstream with If-None-Match and If-Modified-Since,
from the ETag and Last-Modified it stored. A 304 renews the source and
its variant without a download or a conversion. New bytes replace the
source, and each variant made before them is converted again. A failed
check serves the cached image and puts the URL in the negative cache.
Sources and variants not checked successfully for 30 days are evicted.

A variant file's modification time is its source's check time, so a
hit still makes no database call.

Existing tests change only in call signatures. The test that asserted
two-day-old entries are served without asking upstream is replaced by
the new ones.

Model: opus-5-5
2026-11-11 00:24:06 +00:00

89 lines
2.7 KiB
Go

package imgcache
import (
"bytes"
"os"
"slices"
"testing"
"time"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// TestEvictPastMaxAge stores a source and a variant last checked with
// upstream 31 days ago and a source and a variant checked 29 days ago.
// EvictPastMaxAge must remove the rows and files of the first two and keep
// the other two.
func TestEvictPastMaxAge(t *testing.T) {
t.Parallel()
cache, _ := newEvictionTestCache(t, 1<<30)
old := time.Now().Add(-31 * 24 * time.Hour)
recent := time.Now().Add(-29 * 24 * time.Hour)
oldSource := imagerequest.Source{Host: testHostCDN, Path: "/old.jpg"}
recentSource := imagerequest.Source{Host: testHostCDN, Path: "/recent.jpg"}
for src, checkedAt := range map[imagerequest.Source]time.Time{
oldSource: old, recentSource: recent,
} {
_, err := cache.PutSource(t.Context(), src, bytes.NewReader([]byte("source")),
SourceInfo{FetchedAt: checkedAt, CheckedAt: checkedAt})
if err != nil {
t.Fatalf("PutSource(%s) error = %v", src.Path, err)
}
}
oldVariant := imagerequest.VariantKey("aabbccdd0001")
recentVariant := imagerequest.VariantKey("aabbccdd0002")
for key, checkedAt := range map[imagerequest.VariantKey]time.Time{
oldVariant: old, recentVariant: recent,
} {
err := cache.PutVariant(t.Context(), key, checkedAt,
bytes.NewReader([]byte("variant")))
if err != nil {
t.Fatalf("PutVariant(%s) error = %v", key, err)
}
}
err := cache.EvictPastMaxAge(t.Context())
if err != nil {
t.Fatalf("EvictPastMaxAge() error = %v", err)
}
sourceRows, err := queryStringColumn[imagerequest.SourceKey](t.Context(),
cache.db, `SELECT source_key FROM source_content`, "sources", "source")
if err != nil {
t.Fatal(err)
}
variantRows, err := queryStringColumn[imagerequest.VariantKey](t.Context(),
cache.db, `SELECT cache_key FROM variant_content`, "variants", "variant")
if err != nil {
t.Fatal(err)
}
t.Logf("source rows left = %v, variant rows left = %v", sourceRows, variantRows)
if !slices.Equal(sourceRows, []imagerequest.SourceKey{recentSource.Key()}) {
t.Errorf("source rows left = %v, want only %s", sourceRows, recentSource.Key())
}
if !slices.Equal(variantRows, []imagerequest.VariantKey{recentVariant}) {
t.Errorf("variant rows left = %v, want only %s", variantRows, recentVariant)
}
for path, wantKept := range map[string]bool{
cache.srcContent.keyToPath(string(oldSource.Key())): false,
cache.srcContent.keyToPath(string(recentSource.Key())): true,
cache.variants.keyToPath(string(oldVariant)): false,
cache.variants.keyToPath(string(recentVariant)): true,
} {
_, statErr := os.Stat(path)
if kept := statErr == nil; kept != wantKept {
t.Errorf("file %s kept = %t, want %t (%v)", path, kept, wantKept, statErr)
}
}
}