diff --git a/integration/database.test.ts b/integration/database.test.ts index b1d0401b..2079b9b4 100644 --- a/integration/database.test.ts +++ b/integration/database.test.ts @@ -156,7 +156,14 @@ beforeAll(async () => { process.env.REDIS_URL = `redis://:${redisPassword}@${redisContainer.getHost()}:${redisContainer.getMappedPort(6379)}/0`; delete process.env.SKIP_ENV_VALIDATION; delete process.env.OPENAI_API_KEY; - Object.assign(process.env, { NODE_ENV: "test" }); + // Deliberately NOT "test": cache.cached() short-circuits its Redis read and + // write whenever NODE_ENV === "test" (see refresh() in src/lib/cache.ts). + // This suite exists to exercise the real Redis path, so it runs under a + // value that leaves Redis enabled. "development" is used because it is the + // only non-production value src/env.ts accepts. Vitest's own environment is + // still configured via vitest.integration.config.ts. Object.assign is used + // because process.env.NODE_ENV is typed read-only. + Object.assign(process.env, { NODE_ENV: "development" }); process.env.HOTEL_NAME = "Integration"; await connection.query( @@ -380,9 +387,10 @@ describe("Redis application cache", () => { let fetches = 0; const fetch = async () => ({ revision: ++fetches }); expect(await cache.cached(key, 60_000, fetch)).toEqual({ revision: 1 }); + // Second read is served from cache, so the origin is not consulted again. expect( - await cache.cached(key, 60000, async () => ({ revision: 1 })), - ).resolves.toMatchObject({ revision: 1 }); + await cache.cached(key, 60_000, fetch), + ).toEqual({ revision: 1 }); expect(await appRedis?.ttl(key)).toBeGreaterThan(0); cache.invalidateMemory(key); expect(await cache.cached(key, 60_000, fetch)).toEqual({ revision: 1 }); @@ -403,6 +411,9 @@ describe("Redis application cache", () => { expect(await cache.cached(first, 60_000, async () => "updated")).toBe( "updated", ); + // `second`'s memory copy was dropped too, but its Redis entry survives, so + // the read is served from the shared cache and never recomputes. This is + // what makes the two entries independent. expect(await cache.cached(second, 60_000, async () => "wrong")).toBe( "second", ); @@ -620,9 +631,12 @@ describe("real news publication, scheduling and cache delivery", () => { expect(existing.status).toBe("draft"); expect(existing.publishedAt).toBeNull(); expect(await publicNews.getPublishedArticle(existing.slug)).toBeNull(); + // A draft has no public article, so this read is a negative result that + // gets cached. Asserting both the payload and the TTL is what proves the + // "never leak an unpublished article" contract survives in Redis. const negativeRevision = await appRedis?.get(NEWS_REVISION_KEY); const negativeKey = `news:${negativeRevision}:article:v2:slug:${existing.slug}`; - expect(await appRedis?.get(negativeKey)).toBeNull(); + expect(await appRedis?.get(negativeKey)).toBe("null"); expect(await appRedis?.ttl(negativeKey)).toBeGreaterThan(0); const body = `
${"Contenuto completo è 📰 ".repeat(4000)}
`; @@ -839,7 +853,8 @@ describe("real news publication, scheduling and cache delivery", () => { expect(await publicNews.getPublishedArticle(existing.slug)).toBeNull(); const negativeRevision = await redis.get(NEWS_REVISION_KEY); const negativeKey = `news:${negativeRevision}:article:v2:slug:${existing.slug}`; - expect(await redis.get(negativeKey)).toBeNull(); + // Cached negative results are stored as the JSON encoding of null. + expect(await redis.get(negativeKey)).toBe("null"); const publish = articleForm({ id: String(existing.id), baseToken: articleEditToken(existing), diff --git a/public/swf/gordon/shirt_M_drsports.swf b/public/swf/gordon/shirt_M_drsports.swf index 3726d922..2f8bdb56 100644 Binary files a/public/swf/gordon/shirt_M_drsports.swf and b/public/swf/gordon/shirt_M_drsports.swf differ diff --git a/src/app/admin/devops/health-check-client.tsx b/src/app/admin/devops/health-check-client.tsx index 455c3c32..dc8af623 100644 --- a/src/app/admin/devops/health-check-client.tsx +++ b/src/app/admin/devops/health-check-client.tsx @@ -29,10 +29,9 @@ export function HealthCheckClient() { } } - // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => { checkEmulator(); - }, []); + }, [checkEmulator]); return (