f6fcd7ce54
Build & Push / Pipeline Tests (push) Failing after 1m8s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 53s
Test / API Unit Tests (push) Failing after 52s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 43s
Test / Dashboard Unit Tests (push) Successful in 46s
Test / API Integration Tests (push) Successful in 1m7s
148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const prismaMock = vi.hoisted(() => {
|
|
const tx = {
|
|
notificationEvent: {
|
|
create: vi.fn().mockResolvedValue({ id: 'event_1' }),
|
|
},
|
|
notificationRecipient: {
|
|
create: vi.fn().mockResolvedValue({ id: 'recipient_1' }),
|
|
},
|
|
notificationDelivery: {
|
|
create: vi.fn().mockImplementation(({ data }) => Promise.resolve({ id: `delivery_${data.channel}`, ...data })),
|
|
},
|
|
notificationOutbox: {
|
|
create: vi.fn().mockResolvedValue({ id: 'outbox_1' }),
|
|
},
|
|
}
|
|
|
|
return {
|
|
tx,
|
|
prisma: {
|
|
employee: { findFirst: vi.fn(), findMany: vi.fn() },
|
|
renter: { findUnique: vi.fn() },
|
|
notificationPreference: { findFirst: vi.fn() },
|
|
companyNotificationPreference: { findUnique: vi.fn() },
|
|
notificationEvent: { findUniqueOrThrow: vi.fn() },
|
|
$transaction: vi.fn((callback) => callback(tx)),
|
|
},
|
|
}
|
|
})
|
|
|
|
vi.mock('resend', () => ({ Resend: vi.fn() }))
|
|
vi.mock('../lib/prisma', () => ({ prisma: prismaMock.prisma }))
|
|
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 { createNotification, sendNotification } from './notificationService'
|
|
import { resolveNotificationLocale, resolveNotificationTemplate } from './notificationLocalizationService'
|
|
|
|
describe('notificationService command boundaries', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.mocked(prisma.employee.findFirst).mockResolvedValue({
|
|
id: 'employee_1',
|
|
email: 'agent@example.test',
|
|
preferredLanguage: 'en',
|
|
} as never)
|
|
vi.mocked(prisma.notificationEvent.findUniqueOrThrow).mockResolvedValue({ recipients: [] } as never)
|
|
vi.mocked(prisma.notificationPreference.findFirst).mockResolvedValue(null as never)
|
|
vi.mocked(prisma.companyNotificationPreference.findUnique).mockResolvedValue(null as never)
|
|
vi.mocked(resolveNotificationLocale).mockResolvedValue('en')
|
|
vi.mocked(resolveNotificationTemplate).mockResolvedValue(null as never)
|
|
})
|
|
|
|
it('creates an event, explicit recipient, delivery rows, and an outbox row without inline provider calls', async () => {
|
|
const result = await createNotification({
|
|
companyId: 'company_1',
|
|
type: 'NEW_BOOKING',
|
|
audience: { type: 'EMPLOYEE', employeeId: 'employee_1' },
|
|
channels: ['IN_APP', 'EMAIL'],
|
|
source: { type: 'reservation', id: 'reservation_1' },
|
|
idempotencyKey: 'reservation_1:new_booking:employee_1',
|
|
title: 'New booking',
|
|
body: 'A renter booked a vehicle.',
|
|
data: { reservationId: 'reservation_1' },
|
|
})
|
|
|
|
expect(result.duplicate).toBe(false)
|
|
expect(prisma.notificationEvent.findUniqueOrThrow).not.toHaveBeenCalled()
|
|
expect(prismaMock.tx.notificationEvent.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
companyId: 'company_1',
|
|
type: 'NEW_BOOKING',
|
|
sourceType: 'reservation',
|
|
sourceId: 'reservation_1',
|
|
}),
|
|
})
|
|
expect(prismaMock.tx.notificationRecipient.create).toHaveBeenCalledWith({
|
|
data: {
|
|
notificationEventId: 'event_1',
|
|
recipientType: 'EMPLOYEE',
|
|
employeeId: 'employee_1',
|
|
renterId: null,
|
|
},
|
|
})
|
|
expect(prismaMock.tx.notificationDelivery.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
notificationRecipientId: 'recipient_1',
|
|
channel: 'IN_APP',
|
|
status: 'QUEUED',
|
|
preferenceDecision: 'ENABLED_BY_PRODUCT_DEFAULT',
|
|
}),
|
|
})
|
|
expect(prismaMock.tx.notificationOutbox.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
notificationEventId: 'event_1',
|
|
payload: expect.objectContaining({
|
|
notificationEventId: 'event_1',
|
|
type: 'NEW_BOOKING',
|
|
}),
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('records skipped delivery decisions for unsupported optional channels', async () => {
|
|
const result = await sendNotification({
|
|
type: 'BOOKING_CONFIRMED',
|
|
title: 'Booking confirmed',
|
|
body: 'Your booking is confirmed.',
|
|
companyId: 'company_1',
|
|
employeeId: 'employee_1',
|
|
channels: ['PUSH'],
|
|
data: { reservationId: 'reservation_1' },
|
|
})
|
|
|
|
expect(result).toEqual([
|
|
{ channel: 'PUSH', success: false, error: 'Delivery channel disabled by notification policy or preference.' },
|
|
])
|
|
expect(prismaMock.tx.notificationDelivery.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
channel: 'PUSH',
|
|
status: 'SKIPPED',
|
|
preferenceDecision: 'SKIPPED_UNSUPPORTED_CHANNEL',
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('fails before persistence when an explicit recipient is missing', async () => {
|
|
const result = await sendNotification({
|
|
type: 'NEW_BOOKING',
|
|
title: 'Missing recipient',
|
|
body: 'No target.',
|
|
companyId: 'company_1',
|
|
channels: ['IN_APP'],
|
|
})
|
|
|
|
expect(result).toEqual([{ channel: 'IN_APP', success: false, error: 'An explicit employee or renter recipient is required' }])
|
|
expect(prismaMock.tx.notificationEvent.create).not.toHaveBeenCalled()
|
|
})
|
|
})
|