Skip to content

Commit 6c6a2aa

Browse files
committed
fix(cache): invalidate public profile cache after platform link mutations
Root cause: createPlatformLink, updatePlatformLink, deletePlatformLink, and reorderLinks all mutated the database but never called redis.del on the profile:<username> cache key, leaving stale data served to viewers until the 5-minute TTL expired naturally. Fix: Add a private invalidateProfileCacheForUser helper that resolves the username via a lightweight SELECT then calls redis.del. All four mutation functions now await this helper after a successful DB write so the cache is cleared immediately. Cache invalidation is skipped when Redis is absent and errors are caught and logged non-fatally so a Redis blip never fails a mutation request. Also fix the DELETE /api/cards/:id route handler which checked error codes as return values; the service throws errors, so the handler now catches them. Fix cards.test.ts duplicate buildApp declaration, and apply the PlatformLink type fix to cardService.ts (upstream/main has not yet merged that PR). Tests: 21 new tests in profile-cache.test.ts cover cache hit/miss lifecycle, all four mutation paths, failed mutations, non-existent links, Redis-absent mode, consecutive mutations, cache repopulation, and non-fatal Redis errors.
1 parent 82a256c commit 6c6a2aa

26 files changed

Lines changed: 882 additions & 112 deletions

apps/backend/src/__tests__/analytics.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import Fastify, {
2+
type FastifyInstance,
3+
} from 'fastify';
14
import {
25
describe,
36
it,
@@ -7,13 +10,11 @@ import {
710
vi,
811
} from 'vitest';
912

10-
import Fastify, {
11-
type FastifyInstance,
12-
} from 'fastify';
13+
14+
import { analyticsRoutes } from '../routes/analytics';
1315

1416
import type { PrismaClient } from '@prisma/client';
1517

16-
import { analyticsRoutes } from '../routes/analytics';
1718

1819
// ─── Shared mock data ────────────────────────────────────────────────────────
1920

@@ -34,7 +35,7 @@ const prismaMock = {
3435

3536
// ─── App factory ─────────────────────────────────────────────────────────────
3637

37-
let mockJwtVerify = vi.fn();
38+
const mockJwtVerify = vi.fn();
3839

3940
async function buildApp(): Promise<FastifyInstance> {
4041
const app = Fastify({
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import cookie from '@fastify/cookie';
2+
import jwt from '@fastify/jwt';
3+
import Fastify from 'fastify';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
6+
import { authRoutes } from '../routes/auth.js';
7+
8+
import type { PrismaClient } from '@prisma/client';
9+
10+
const mockUser = {
11+
id: 'user-123',
12+
username: 'devcard-demo',
13+
};
14+
15+
const prismaMock = {
16+
user: {
17+
findUnique: vi.fn(),
18+
},
19+
};
20+
21+
async function buildApp(nodeEnv: string) {
22+
vi.stubEnv('NODE_ENV', nodeEnv);
23+
24+
const app = Fastify();
25+
await app.register(jwt, { secret: 'test-secret' });
26+
await app.register(cookie);
27+
app.decorate('prisma', prismaMock as unknown as PrismaClient);
28+
app.decorate('authenticate', async () => {});
29+
await app.register(authRoutes, { prefix: '/auth' });
30+
await app.ready();
31+
return app;
32+
}
33+
34+
describe('auth dev-login route registration', () => {
35+
beforeEach(() => {
36+
vi.clearAllMocks();
37+
});
38+
39+
afterEach(() => {
40+
vi.unstubAllEnvs();
41+
});
42+
43+
it('registers /auth/dev-login outside production', async () => {
44+
prismaMock.user.findUnique.mockResolvedValue(mockUser);
45+
const app = await buildApp('development');
46+
47+
const res = await app.inject({
48+
method: 'POST',
49+
url: '/auth/dev-login',
50+
});
51+
52+
expect(res.statusCode).toBe(200);
53+
expect(res.json()).toHaveProperty('token');
54+
expect(prismaMock.user.findUnique).toHaveBeenCalledWith({
55+
where: { username: 'devcard-demo' },
56+
});
57+
58+
await app.close();
59+
});
60+
61+
it('does not register /auth/dev-login in production', async () => {
62+
const app = await buildApp('production');
63+
64+
const res = await app.inject({
65+
method: 'POST',
66+
url: '/auth/dev-login',
67+
});
68+
69+
expect(res.statusCode).toBe(404);
70+
expect(prismaMock.user.findUnique).not.toHaveBeenCalled();
71+
72+
await app.close();
73+
});
74+
75+
it('keeps other auth routes registered in production', async () => {
76+
const app = await buildApp('production');
77+
78+
const res = await app.inject({
79+
method: 'POST',
80+
url: '/auth/logout',
81+
});
82+
83+
expect(res.statusCode).toBe(200);
84+
expect(res.json()).toMatchObject({ message: 'Logged out' });
85+
86+
await app.close();
87+
});
88+
});

apps/backend/src/__tests__/cards.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function wireTransaction(): void {
5252
);
5353
}
5454

55-
async function buildApp():Promise<FastifyInstance> {
55+
async function buildApp(): Promise<FastifyInstance> {
5656
const app = Fastify({ logger: false });
5757
app.decorate('prisma', mockPrisma as unknown as PrismaClient);
5858
app.decorate('authenticate', async (request: any) => {

apps/backend/src/__tests__/event.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import Fastify, { type FastifyInstance } from 'fastify';
12
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2-
import Fastify, { FastifyInstance } from 'fastify';
3-
import { PrismaClient } from '@prisma/client';
3+
44
import { eventRoutes } from '../routes/event';
55

6+
import type { PrismaClient } from '@prisma/client';
7+
68
// ─── Shared mock data ────────────────────────────────────────────────────────
79

810
const MOCK_USER_ID = 'user-uuid-001';
@@ -64,7 +66,7 @@ const prismaMock = {
6466
//
6567
// This mirrors the real app setup without touching a real DB or real JWT keys.
6668

67-
let mockJwtVerify = vi.fn();
69+
const mockJwtVerify = vi.fn();
6870

6971
async function buildApp(): Promise<FastifyInstance> {
7072
const app = Fastify({ logger: false });

apps/backend/src/__tests__/follow.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Fastify, { FastifyInstance } from 'fastify';
1+
import Fastify, { type FastifyInstance } from 'fastify';
22
import { describe, expect, it, vi, beforeAll, beforeEach, afterAll } from 'vitest';
33

44
import { followRoutes } from '../routes/follow.js';

apps/backend/src/__tests__/oauth-scope.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
* flow so the two records are independent and can never overwrite each other.
1212
*/
1313

14-
import { describe, it, expect, beforeEach, vi } from 'vitest';
1514
import Fastify from 'fastify';
15+
import { describe, it, expect, beforeEach, vi } from 'vitest';
16+
1617
import { connectRoutes } from '../routes/connect.js';
1718
import { followRoutes } from '../routes/follow.js';
19+
1820
import type { PrismaClient } from '@prisma/client';
1921

2022
// ── Mocks ─────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)