import { describe, expect, it } from 'vitest'; import { TenderNotificationPrefService } from './tender-notification-pref.service'; /** * TenderNotificationPrefService.spec — RED-first (TDD) proof for NOTIFY-01 * (D-01/D-03) and the V4/IDOR access-control invariant (T-12-14): * * - getForUser() without an existing row returns a default * { digestInterval: 'daily' } (D-01) — no error, no implicit autowrite. * - setForUser() upserts on the @@unique userId (D-03); a second call with * a different value updates the SAME row rather than creating a new one. * * Uses the same hand-rolled prisma-shaped fake convention as * tender-saved-search.service.spec.ts / tender-triage.service.spec.ts * (in-memory Map, no live DB connection). */ function makeFakePrisma() { const rows = new Map(); return { tenderNotificationPref: { findUnique: async ({ where }: any) => rows.get(where.userId) ?? null, upsert: async ({ where, create, update }: any) => { const existing = rows.get(where.userId); const record = existing ? { ...existing, ...update, updatedAt: new Date() } : { id: `pref-${rows.size + 1}`, ...create, createdAt: new Date(), updatedAt: new Date() }; rows.set(where.userId, record); return record; }, }, }; } describe('TenderNotificationPrefService', () => { it('getForUser() returns a default digestInterval="daily" when no row exists (D-01)', async () => { const prisma = makeFakePrisma(); const service = new TenderNotificationPrefService(prisma as any); const result = await service.getForUser('u1'); expect(result.digestInterval).toBe('daily'); }); it('setForUser() upserts on userId, creating a row scoped to (userId, tenantId) (D-03)', async () => { const prisma = makeFakePrisma(); const service = new TenderNotificationPrefService(prisma as any); const result = await service.setForUser('u1', 'tenant1', 'weekly'); expect(result.userId).toBe('u1'); expect(result.tenantId).toBe('tenant1'); expect(result.digestInterval).toBe('weekly'); }); it('setForUser() called a second time updates the SAME row (@@unique userId), not a new one', async () => { const prisma = makeFakePrisma(); const service = new TenderNotificationPrefService(prisma as any); await service.setForUser('u1', 'tenant1', 'weekly'); const second = await service.setForUser('u1', 'tenant1', 'off'); expect(second.digestInterval).toBe('off'); expect(await service.getForUser('u1')).toMatchObject({ digestInterval: 'off' }); }); it('getForUser() is scoped strictly by userId — a foreign userId never sees another user\'s pref (V4 / IDOR)', async () => { const prisma = makeFakePrisma(); const service = new TenderNotificationPrefService(prisma as any); await service.setForUser('u1', 'tenant1', 'weekly'); const foreign = await service.getForUser('u2'); expect(foreign.digestInterval).toBe('daily'); // default, not u1's 'weekly' }); });