import { beforeEach, describe, expect, it, vi } from 'vitest'; import { HttpError } from '@/lib/security/http'; vi.mock('server-only', () => ({})); const mocks = vi.hoisted(() => ({ user: vi.fn(), list: vi.fn(), read: vi.fn(), limit: vi.fn() })); vi.mock('@/lib/commission/server', () => ({ requireCommissionUser: mocks.user })); vi.mock('@/lib/notifications/repository', () => ({ listNotifications: mocks.list, readNotifications: mocks.read })); vi.mock('@/lib/security/rate-limit', () => ({ limitRequest: mocks.limit })); const { GET, POST } = await import('@/app/api/notifications/route'); beforeEach(() => { vi.clearAllMocks(); process.env.BETTER_AUTH_URL = 'https://guide.example.test'; mocks.user.mockResolvedValue({ id: 'owner', role: 'user', emailVerified: true }); mocks.list.mockResolvedValue({ items: [], unread: 0, nextCursor: null }); }); describe('notification endpoints', () => { it('requires an authenticated account and disables caching', async () => { const request = new Request('https://guide.example.test/api/notifications'); expect((await GET(request)).headers.get('cache-control')).toBe('private, no-store'); mocks.user.mockRejectedValueOnce(new HttpError(401, 'unauthorized')); expect((await GET(request)).status).toBe(401); }); it('validates cursors and uses the current account for mark-read', async () => { expect((await GET(new Request('https://guide.example.test/api/notifications?before=bad'))).status).toBe(400); const id = '00000000-0000-4000-8000-000000000001'; const request = (origin: string) => new Request('https://guide.example.test/api/notifications', { method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json' }, body: JSON.stringify({ id, userId: 'victim' }) }); expect((await POST(request('https://evil.example'))).status).toBe(403); expect(mocks.read).not.toHaveBeenCalled(); expect((await POST(request('https://guide.example.test'))).status).toBe(204); expect(mocks.read).toHaveBeenCalledWith('owner', id); }); });