import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PgDialect } from 'drizzle-orm/pg-core'; import type { SQL } from 'drizzle-orm'; const state = vi.hoisted(() => ({ rows: [] as unknown[][], filters: [] as unknown[], joins: [] as unknown[], select: vi.fn() })); vi.mock('server-only', () => ({})); vi.mock('./events', () => ({ notifyNotificationChange: vi.fn() })); vi.mock('@/lib/media/storage', () => ({ publicMediaUrl: (key: string) => `https://cdn.example/${key}` })); vi.mock('@/db', () => ({ getDb: () => ({ select: state.select }) })); import { listNotifications } from './repository'; const user = { id: 'reader', role: 'user', emailVerified: true }; const guideId = '00000000-0000-4000-8000-000000000001'; const otherGuideId = '00000000-0000-4000-8000-000000000002'; function row(kind: string, eventKey: string) { return { notification: { id: eventKey, kind, eventKey }, cursorTime: '2026-10-12T00:00:00Z', authorName: null, authorImage: null }; } beforeEach(() => { state.rows = []; state.filters = []; state.joins = []; state.select.mockReset().mockImplementation(() => { const rows = state.rows.shift() ?? []; const query = { from: () => query, leftJoin: (_table: unknown, condition: unknown) => { state.joins.push(condition); return query; }, innerJoin: () => query, where: (filter: unknown) => { state.filters.push(filter); return query; }, orderBy: () => query, limit: () => query, then: (resolve: (value: unknown[]) => unknown) => Promise.resolve(rows).then(resolve), }; return query; }); }); describe('guide overview images in notification feeds', () => { it('loads multiple guide covers in one bounded query while preserving comment authors and unread counts', async () => { state.rows = [[row('guide_published', `guide:${guideId}:published`), row('guide_published', `guide:${otherGuideId}:published`), { ...row('comment_new', 'comment:1'), authorName: 'Reader', authorImage: 'https://profile.example/reader.png' }], [{ count: 3 }], [{ id: guideId, objectKey: 'guide/cover.png' }, { id: otherGuideId, objectKey: 'guide/other.png' }]]; const result = await listNotifications(user); expect(result.unread).toBe(3); expect(result.nextCursor).toBeNull(); expect(result.items.map(item => item.imageUrl)).toEqual(['https://cdn.example/guide/cover.png', 'https://cdn.example/guide/other.png', null]); expect(result.items[2]).toMatchObject({ authorName: 'Reader', authorImage: 'https://profile.example/reader.png' }); expect(state.select).toHaveBeenCalledTimes(3); const filter = new PgDialect().sqlToQuery(state.filters[2] as SQL); expect(filter.params).toContain(guideId); expect(filter.params).toContain(otherGuideId); expect(filter.params).toContain(true); expect(filter.sql).toContain('"is_public"'); expect(filter.sql).toContain('"trashed_at" is null'); }); it('avoids cover queries for other notification kinds or malformed publication keys', async () => { state.rows = [[row('commission', `guide:${guideId}:published`), row('guide_published', 'guide:invalid:published')], [{ count: 2 }]]; const result = await listNotifications(user); expect(result.items.map(item => item.imageUrl)).toEqual([null, null]); expect(state.select).toHaveBeenCalledTimes(2); }); it('keeps the notification usable without an image when its guide is unavailable', async () => { state.rows = [[row('guide_published', `guide:${guideId}:published`)], [{ count: 1 }], []]; expect((await listNotifications(user)).items[0].imageUrl).toBeNull(); }); it('uses the heart giver for heart notifications and the author for new comments and replies', async () => { state.rows = [[{ ...row('comment_heart', 'comment:1:heart'), authorName: 'Admin', authorImage: 'https://profile.example/admin.png' }], [{ count: 1 }]]; const result = await listNotifications(user); expect(result.items[0]).toMatchObject({ authorName: 'Admin', authorImage: 'https://profile.example/admin.png', imageUrl: null }); const joins = state.joins.map(condition => new PgDialect().sqlToQuery(condition as SQL)); const profileJoin = joins.find(join => join.sql.includes('case when'))!; expect(profileJoin.sql).toContain("= 'comment_heart' then"); expect(profileJoin.sql).toContain('"hearted_by_id" else'); expect(profileJoin.sql).toContain('"author_id" end'); expect(profileJoin.params).toContain('comment_heart'); expect(state.select).toHaveBeenCalledTimes(2); }); });