fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('resend', () => ({ Resend: vi.fn() }))
|
||||
vi.mock('twilio', () => ({ default: vi.fn(() => ({ messages: { create: vi.fn() } })) }))
|
||||
vi.mock('firebase-admin', () => ({
|
||||
default: {
|
||||
apps: [],
|
||||
initializeApp: vi.fn(),
|
||||
credential: { cert: vi.fn() },
|
||||
messaging: vi.fn(() => ({ send: vi.fn() })),
|
||||
},
|
||||
}))
|
||||
vi.mock('../lib/prisma', () => ({
|
||||
prisma: {
|
||||
notification: {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('../lib/redis', () => ({
|
||||
redis: { publish: vi.fn(), on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
|
||||
}))
|
||||
vi.mock('./notificationLocalizationService', async () => {
|
||||
const actual = await vi.importActual<typeof import('./notificationLocalizationService')>('./notificationLocalizationService')
|
||||
return {
|
||||
...actual,
|
||||
resolveNotificationLocale: vi.fn().mockResolvedValue('en'),
|
||||
resolveNotificationTemplate: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { redis } from '../lib/redis'
|
||||
import { sendNotification } from './notificationService'
|
||||
import { resolveNotificationLocale, resolveNotificationTemplate } from './notificationLocalizationService'
|
||||
|
||||
describe('notificationService delivery boundaries', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(prisma.notification.create).mockResolvedValue({ id: 'notification_1', title: 'Hello', body: 'Body' } as never)
|
||||
vi.mocked(prisma.notification.update).mockResolvedValue({ id: 'notification_1' } as never)
|
||||
vi.mocked(resolveNotificationLocale).mockResolvedValue('en')
|
||||
vi.mocked(resolveNotificationTemplate).mockResolvedValue(null as never)
|
||||
})
|
||||
|
||||
it('persists and publishes in-app notifications for employee recipients', async () => {
|
||||
const result = await sendNotification({
|
||||
type: 'SYSTEM_ALERT' as never,
|
||||
title: 'Maintenance window',
|
||||
body: 'The dashboard will be unavailable.',
|
||||
companyId: 'company_1',
|
||||
employeeId: 'employee_1',
|
||||
channels: ['IN_APP' as never],
|
||||
data: { severity: 'low' },
|
||||
})
|
||||
|
||||
expect(result).toEqual([{ channel: 'IN_APP', success: true }])
|
||||
expect(resolveNotificationLocale).toHaveBeenCalledWith(expect.objectContaining({
|
||||
companyId: 'company_1',
|
||||
employeeId: 'employee_1',
|
||||
}))
|
||||
expect(prisma.notification.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
type: 'SYSTEM_ALERT',
|
||||
title: 'Maintenance window',
|
||||
body: 'The dashboard will be unavailable.',
|
||||
channel: 'IN_APP',
|
||||
status: 'PENDING',
|
||||
companyId: 'company_1',
|
||||
employeeId: 'employee_1',
|
||||
renterId: null,
|
||||
}),
|
||||
})
|
||||
expect(redis.publish).toHaveBeenCalledWith(
|
||||
'notifications:employee_1',
|
||||
expect.stringContaining('"status":"DELIVERED"'),
|
||||
)
|
||||
expect(prisma.notification.update).toHaveBeenCalledWith({
|
||||
where: { id: 'notification_1' },
|
||||
data: expect.objectContaining({ status: 'SENT', providerMessageId: null }),
|
||||
})
|
||||
})
|
||||
|
||||
it('uses resolved templates and records failed channels without throwing', async () => {
|
||||
vi.mocked(resolveNotificationTemplate).mockResolvedValue({
|
||||
templateKey: 'reservation.confirmed',
|
||||
locale: 'fr',
|
||||
subject: 'Réservation confirmée',
|
||||
body: 'Votre réservation est confirmée.',
|
||||
usedFallback: false,
|
||||
version: 2,
|
||||
} as never)
|
||||
|
||||
const result = await sendNotification({
|
||||
type: 'RESERVATION_UPDATE' as never,
|
||||
templateKey: 'reservation.confirmed',
|
||||
templateVariables: { firstName: 'Aya' },
|
||||
companyId: 'company_1',
|
||||
channels: ['EMAIL' as never],
|
||||
email: 'aya@example.test',
|
||||
})
|
||||
|
||||
expect(result).toEqual([{ channel: 'EMAIL', success: false, error: 'No email provider is configured' }])
|
||||
expect(resolveNotificationTemplate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
templateKey: 'reservation.confirmed',
|
||||
channel: 'EMAIL',
|
||||
locale: 'en',
|
||||
variables: { firstName: 'Aya' },
|
||||
}))
|
||||
expect(prisma.notification.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
title: 'Réservation confirmée',
|
||||
body: 'Votre réservation est confirmée.',
|
||||
templateKey: 'reservation.confirmed',
|
||||
locale: 'fr',
|
||||
}),
|
||||
})
|
||||
expect(prisma.notification.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails incomplete notification content before persistence', async () => {
|
||||
const result = await sendNotification({
|
||||
type: 'SYSTEM_ALERT' as never,
|
||||
title: 'Missing body',
|
||||
channels: ['IN_APP' as never],
|
||||
employeeId: 'employee_1',
|
||||
})
|
||||
|
||||
expect(result).toEqual([{ channel: 'IN_APP', success: false, error: 'Notification content is incomplete for channel IN_APP' }])
|
||||
expect(prisma.notification.create).not.toHaveBeenCalled()
|
||||
expect(redis.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user