package imgcache import ( "context" "errors" "testing" "time" ) func TestNegativeCache_StoreAndCheck(t *testing.T) { t.Parallel() db := setupTestDB(t) dir := t.TempDir() cache, err := NewCache(db, CacheConfig{ StateDir: dir, NegativeTTL: 5 * time.Minute, }) if err != nil { t.Fatal(err) } ctx := context.Background() req := &ImageRequest{ SourceHost: testHostExample, SourcePath: "/missing.jpg", } // Initially should not be in negative cache hit, err := cache.checkNegativeCache(ctx, req) if err != nil { t.Fatal(err) } if hit { t.Error("expected no negative cache hit initially") } // Store a negative entry err = cache.StoreNegative(ctx, req, 404, "not found") if err != nil { t.Fatal(err) } // Now should be in negative cache hit, err = cache.checkNegativeCache(ctx, req) if err != nil { t.Fatal(err) } if !hit { t.Error("expected negative cache hit after storing") } } func TestNegativeCache_Expired(t *testing.T) { t.Parallel() db := setupTestDB(t) dir := t.TempDir() cache, err := NewCache(db, CacheConfig{ StateDir: dir, NegativeTTL: 1 * time.Millisecond, // very short TTL }) if err != nil { t.Fatal(err) } ctx := context.Background() req := &ImageRequest{ SourceHost: testHostExample, SourcePath: "/expired.jpg", } // Store a negative entry with very short TTL err = cache.StoreNegative(ctx, req, 500, "server error") if err != nil { t.Fatal(err) } // Wait for expiry time.Sleep(10 * time.Millisecond) // Should no longer be in negative cache hit, err := cache.checkNegativeCache(ctx, req) if err != nil { t.Fatal(err) } if hit { t.Error("expected expired negative cache entry to be a miss") } } // TestService_Get_ServesCachedVariantOfNegativeCachedURL caches a variant of // the test photo, then keeps a failed fetch of the photo's URL in the negative // cache, as a failed fetch for another variant does once the source has been // evicted. The cached variant is still served; a variant not in the cache is // still refused. func TestService_Get_ServesCachedVariantOfNegativeCachedURL(t *testing.T) { t.Parallel() svc, fixtures := SetupTestService(t) resp, err := svc.Get(t.Context(), photoVariant(fixtures, 85, FitCover)) if err != nil { t.Fatalf("Get() first request error = %v", err) } _ = resp.Content.Close() err = svc.cache.StoreNegative(t.Context(), photoVariant(fixtures, 85, FitCover), httpStatusBadGateway, "upstream answered 503") if err != nil { t.Fatalf("StoreNegative() error = %v", err) } resp, err = svc.Get(t.Context(), photoVariant(fixtures, 85, FitCover)) if err != nil { t.Fatalf("Get() of the cached variant error = %v", err) } _ = resp.Content.Close() if resp.CacheStatus != CacheHit { t.Errorf("cached variant CacheStatus = %q, want %q", resp.CacheStatus, CacheHit) } _, err = svc.Get(t.Context(), photoVariant(fixtures, 40, FitCover)) t.Logf("Get() of a variant not in the cache: error = %v", err) if !errors.Is(err, ErrNegativeCached) { t.Errorf("Get() of a variant not in the cache error = %v, want %v", err, ErrNegativeCached) } } func TestService_Get_ReturnsErrorForNegativeCachedURL(t *testing.T) { t.Parallel() // This test verifies that Service.Get() checks the negative cache // We can't easily test the full pipeline without vips, but we can // verify the error type err := ErrNegativeCached if !errors.Is(err, ErrNegativeCached) { t.Error("ErrNegativeCached should be identifiable with errors.Is") } }