From e87af49dff743a1be7d363d4d5265105ec9c23f7 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 21 Jun 2026 17:18:11 +0200 Subject: [PATCH] fix: ignore expired anime in random pool cache --- integrations/jikan/client_test.go | 47 +++++++++++++++++++++++++++++++ internal/db/queries.sql | 2 +- internal/db/queries.sql.go | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/integrations/jikan/client_test.go b/integrations/jikan/client_test.go index 5f7e4df..7f8de79 100644 --- a/integrations/jikan/client_test.go +++ b/integrations/jikan/client_test.go @@ -86,6 +86,32 @@ func TestGetWithCacheAllowsEmptySearchResults(t *testing.T) { } } +func TestLoadCachedRandomPoolIgnoresExpiredAnimeCache(t *testing.T) { + sqlDB := newTestCacheDB(t) + defer func() { + if err := sqlDB.Close(); err != nil { + t.Errorf("close sqlite: %v", err) + } + }() + + queries := db.New(sqlDB) + client := NewClient(config.Config{}, queries, observability.NewMetrics()) + insertCachedAnime(t, sqlDB, "anime:1", Anime{MalID: 1, Title: "fresh"}, time.Now().Add(time.Hour)) + insertCachedAnime(t, sqlDB, "anime:2", Anime{MalID: 2, Title: "expired"}, time.Now().Add(-time.Hour)) + + client.loadCachedRandomPool(context.Background()) + + client.poolMu.RLock() + defer client.poolMu.RUnlock() + + if len(client.randomPool) != 1 { + t.Fatalf("randomPool length = %d, want 1", len(client.randomPool)) + } + if client.randomPool[0].MalID != 1 || client.randomPool[0].Title != "fresh" { + t.Fatalf("randomPool[0] = %+v, want fresh anime", client.randomPool[0]) + } +} + func newTestCacheDB(t *testing.T) *sql.DB { t.Helper() ctx := context.Background() @@ -135,6 +161,27 @@ func insertCachedResponse(t *testing.T, sqlDB *sql.DB, key string, value TopAnim } } +func insertCachedAnime(t *testing.T, sqlDB *sql.DB, key string, value Anime, expiresAt time.Time) { + t.Helper() + ctx := context.Background() + + encoded, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal cached anime: %v", err) + } + + _, err = sqlDB.ExecContext( + ctx, + `INSERT INTO jikan_cache (key, data, expires_at) VALUES (?, ?, ?)`, + key, + string(encoded), + expiresAt, + ) + if err != nil { + t.Fatalf("insert cached anime: %v", err) + } +} + func waitForFreshCache(t *testing.T, sqlDB *sql.DB, client *Client, key string) { t.Helper() diff --git a/internal/db/queries.sql b/internal/db/queries.sql index 03ccc29..2fdc212 100644 --- a/internal/db/queries.sql +++ b/internal/db/queries.sql @@ -362,4 +362,4 @@ LIMIT ?; -- name: GetAllCachedAnime :many SELECT data FROM jikan_cache -WHERE key LIKE 'anime:%' LIMIT 1000; +WHERE key LIKE 'anime:%' AND datetime(expires_at) > CURRENT_TIMESTAMP LIMIT 1000; diff --git a/internal/db/queries.sql.go b/internal/db/queries.sql.go index 025f1d3..ecfad90 100644 --- a/internal/db/queries.sql.go +++ b/internal/db/queries.sql.go @@ -238,7 +238,7 @@ func (q *Queries) GetAPITokenByHash(ctx context.Context, tokenHash string) (ApiT const getAllCachedAnime = `-- name: GetAllCachedAnime :many SELECT data FROM jikan_cache -WHERE key LIKE 'anime:%' LIMIT 1000 +WHERE key LIKE 'anime:%' AND datetime(expires_at) > CURRENT_TIMESTAMP LIMIT 1000 ` func (q *Queries) GetAllCachedAnime(ctx context.Context) ([]string, error) {