import assert from 'node:assert/strict' import test from 'node:test' import Fastify from 'fastify' import routes from '../src/routes/notifications' import { NotificationService } from '../src/modules/notification.service' import { notificationsEventTypes, notificationsPreferences } from '../db/schema' function fixture(prefs: any[] = [], defaults: any[] = []) { const writes: any[] = [] const event = { eventKey: 'communication.message.direct', displayName: 'Direktnachrichten', category: 'communication', allowedChannels: ['inapp', 'email', 'push'], isActive: true } const db: any = { select: () => ({ from: (table: any) => ({ where: async () => table === notificationsEventTypes ? [event] : table === notificationsPreferences ? prefs : defaults }) }), transaction: async (callback: any) => callback(db), insert: () => ({ values: (value: any) => ({ onConflictDoUpdate: async () => { writes.push(value) } }) }), } return { service: new NotificationService({ db } as any, async () => null), db, writes } } test('defaults are quiet for email, user overrides win over tenant defaults', async () => { assert.deepEqual((await fixture().service.preferences(1, 'user')).events[0].channels, { email: false, push: true }) const { service } = fixture([{ channel: 'push', enabled: false }], [{ channel: 'push', enabled: true }, { channel: 'email', enabled: true }]) assert.deepEqual((await service.preferences(1, 'user')).events[0].channels, { email: true, push: false }) }) test('invalid batches cause no writes, including duplicates and unsupported channels', async () => { for (const bad of [null, {}, [{ eventType: 'unknown', channel: 'push', enabled: true }], [{ eventType: 'communication.message.direct', channel: 'sms', enabled: true }], [{ eventType: 'communication.message.direct', channel: 'push', enabled: 'false' }], Array(2).fill({ eventType: 'communication.message.direct', channel: 'push', enabled: true })]) { const { service, writes } = fixture() await assert.rejects(service.savePreferences(1, 'user', bad), { statusCode: 400 }) assert.equal(writes.length, 0) } }) test('API ignores supplied tenant/user identifiers and saves in the authenticated context', async () => { const { db, writes } = fixture() const app = Fastify() app.decorate('db', db) app.addHook('preHandler', async (req: any) => { req.user = { tenant_id: 7, user_id: 'current-user' } }) await app.register(routes) try { const response = await app.inject({ method: 'PUT', url: '/notifications/preferences', payload: [ { tenantId: 999, userId: 'other', eventType: 'communication.message.direct', channel: 'push', enabled: false }, ] }) assert.equal(response.statusCode, 200, response.body) assert.deepEqual(writes, [{ tenantId: 7, userId: 'current-user', eventType: 'communication.message.direct', channel: 'push', enabled: false }]) const invalid = await app.inject({ method: 'PUT', url: '/notifications/preferences', payload: [{ eventType: 'system.test_push', channel: 'push', enabled: false }] }) assert.equal(invalid.statusCode, 400) } finally { await app.close() } }) test('room notifications stay opt-in while direct messages remain enabled', async () => { const { service } = fixture() const resolve = (eventType: string) => (service as any).resolveEnabledChannels({ tenantId: 1, userId: 'user', eventType, channels: ['email', 'push'] }) assert.deepEqual(await resolve('communication.message.room'), []) assert.deepEqual(await resolve('communication.room.invited'), []) assert.deepEqual(await resolve('communication.message.direct'), ['push']) })