diff --git a/internal/imgcache/stats_internal_test.go b/internal/imgcache/stats_internal_test.go index 4cecd4c..5076179 100644 --- a/internal/imgcache/stats_internal_test.go +++ b/internal/imgcache/stats_internal_test.go @@ -1,9 +1,12 @@ package imgcache import ( + "bytes" "context" "database/sql" + "log/slog" "math" + "strings" "testing" "time" @@ -101,3 +104,82 @@ func TestStats_ZeroCounts(t *testing.T) { t.Errorf("HitRate = %f, want 0.0 for zero counts", stats.HitRate) } } + +// TestStats_LogsFailedCountQueries verifies that a failed item count query +// and a failed size query are each logged at warn and Stats still succeeds. +func TestStats_LogsFailedCountQueries(t *testing.T) { + t.Parallel() + + db := setupStatsTestDB(t) + + var logBuf bytes.Buffer + + cache, err := NewCache(db, CacheConfig{ + StateDir: t.TempDir(), + CacheTTL: time.Hour, + NegativeTTL: 5 * time.Minute, + Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)), + }) + if err != nil { + t.Fatal(err) + } + + _, err = db.ExecContext(t.Context(), + `DROP TABLE request_cache; DROP TABLE output_content`) + if err != nil { + t.Fatal(err) + } + + _, err = cache.Stats(t.Context()) + if err != nil { + t.Fatalf("Stats() error = %v, want nil", err) + } + + for _, msg := range []string{ + "failed to count cache items for stats", + "failed to sum cache size for stats", + } { + want := `"level":"WARN","msg":"` + msg + `"` + if !strings.Contains(logBuf.String(), want) { + t.Errorf("log missing %s; got %q", want, logBuf.String()) + } + } +} + +// TestIncrementStats_LogsFailedUpdates verifies that a failed hit or miss +// count update and a failed upstream fetch count update are each logged at +// warn. +func TestIncrementStats_LogsFailedUpdates(t *testing.T) { + t.Parallel() + + db := setupStatsTestDB(t) + + var logBuf bytes.Buffer + + cache, err := NewCache(db, CacheConfig{ + StateDir: t.TempDir(), + CacheTTL: time.Hour, + NegativeTTL: 5 * time.Minute, + Logger: slog.New(slog.NewJSONHandler(&logBuf, nil)), + }) + if err != nil { + t.Fatal(err) + } + + _, err = db.ExecContext(t.Context(), `DROP TABLE cache_stats`) + if err != nil { + t.Fatal(err) + } + + cache.IncrementStats(t.Context(), false, 1024) + + for _, msg := range []string{ + "failed to count cache hit or miss", + "failed to count upstream fetch", + } { + want := `"level":"WARN","msg":"` + msg + `"` + if !strings.Contains(logBuf.String(), want) { + t.Errorf("log missing %s; got %q", want, logBuf.String()) + } + } +} diff --git a/internal/imgcache/storage_internal_test.go b/internal/imgcache/storage_internal_test.go index a72ec94..0465bb1 100644 --- a/internal/imgcache/storage_internal_test.go +++ b/internal/imgcache/storage_internal_test.go @@ -4,8 +4,10 @@ import ( "bytes" "errors" "io" + "log/slog" "os" "path/filepath" + "strings" "testing" ) @@ -404,3 +406,35 @@ func TestCacheKey(t *testing.T) { t.Error("CacheKey() produced same key for different quality") } } + +// TestVariantStorage_StoreLogsFailedMetaWrite verifies that a .meta write +// that fails is logged at warn and the store still succeeds. +func TestVariantStorage_StoreLogsFailedMetaWrite(t *testing.T) { + t.Parallel() + + var logBuf bytes.Buffer + + storage, err := NewVariantStorage( + t.TempDir(), slog.New(slog.NewJSONHandler(&logBuf, nil))) + if err != nil { + t.Fatalf("NewVariantStorage() error = %v", err) + } + + key := CacheKey(&ImageRequest{SourceHost: testHostCDN, SourcePath: testPathCat}) + + // A directory where the .meta file goes makes the .meta write fail. + err = os.MkdirAll(storage.keyToPath(key)+".meta", StorageDirPerm) + if err != nil { + t.Fatalf("failed to create directory: %v", err) + } + + _, err = storage.Store(key, bytes.NewReader([]byte("variant data")), "image/webp") + if err != nil { + t.Fatalf("Store() error = %v, want nil", err) + } + + want := `"level":"WARN","msg":"failed to write variant metadata sidecar"` + if !strings.Contains(logBuf.String(), want) { + t.Errorf("log missing %s; got %q", want, logBuf.String()) + } +}