// tickets.confirmation-email builds and sends the redemption confirmation // email, shared by the public redeem path and the admin resend action. jest.mock('../../src/models/calendar/events/events.service', () => ({ getEventById: jest.fn() })); jest.mock('../../src/models/calendar/events/icalgenerator.service', () => ({ convertToIcal: jest.fn() })); jest.mock('../../src/common/common.mail', () => ({ MailService: {sendMail: jest.fn()} })); jest.mock('../../src/models/tickets/Tickets.db', () => ({ NachklangTicketsDB: {getConnection: jest.fn()} })); jest.mock('../../src/middleware/logger', () => ({ __esModule: true, default: {info: jest.fn(), warn: jest.fn(), error: jest.fn()} })); import * as EventsService from '../../src/models/calendar/events/events.service'; import * as IcalService from '../../src/models/calendar/events/icalgenerator.service'; import {MailService} from '../../src/common/common.mail'; import logger from '../../src/middleware/logger'; import {NachklangTicketsDB} from '../../src/models/tickets/Tickets.db'; import {recordConfirmationEmailResult, sendRedemptionConfirmation} from '../../src/models/tickets/tickets.confirmation-email'; const mockGetEvent = EventsService.getEventById as jest.Mock; const mockToIcal = IcalService.convertToIcal as jest.Mock; const mockSendMail = MailService.sendMail as jest.Mock; const mockLogger = logger as unknown as {info: jest.Mock; warn: jest.Mock; error: jest.Mock}; const mockGetConnection = NachklangTicketsDB.getConnection as jest.Mock; const EVENT = { eventId: 42, name: 'Sommerkonzert 2026', startDateTime: new Date('2026-07-01T19:00:00Z'), location: 'Christuskirche', status: 'PUBLISHED' }; const RECIPIENT = { eventId: 42, contactName: 'Erika Mustermann', contactEmail: 'erika@example.com', guestNames: ['Erika Mustermann', 'Hans Mustermann'] }; beforeEach(() => { jest.clearAllMocks(); mockGetEvent.mockResolvedValue(EVENT); mockToIcal.mockResolvedValue('BEGIN:VCALENDAR\nEND:VCALENDAR'); mockSendMail.mockResolvedValue(true); }); describe('sendRedemptionConfirmation', () => { it('sends the German confirmation with the event details, guest list and .ics attachment', async () => { const result = await sendRedemptionConfirmation(RECIPIENT); expect(result).toBe(true); expect(mockSendMail).toHaveBeenCalledTimes(1); const [to, subject, body, options] = mockSendMail.mock.calls[0]; expect(to).toBe('erika@example.com'); expect(subject).toBe('Bestätigung: Sommerkonzert 2026'); expect(body).toContain('Hallo Erika Mustermann,'); expect(body).toContain('"Sommerkonzert 2026"'); expect(body).toContain('- Hans Mustermann'); expect(options.attachments).toEqual([ {filename: 'konzert.ics', content: 'BEGIN:VCALENDAR\nEND:VCALENDAR', contentType: 'text/calendar'} ]); }); it('still sends (without an attachment) and warns when the .ics build fails', async () => { mockToIcal.mockRejectedValue(new Error('ical boom')); await sendRedemptionConfirmation(RECIPIENT); const options = mockSendMail.mock.calls[0][3]; expect(options.attachments).toBeUndefined(); expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('without .ics attachment')); }); it('returns false and does not send when the event no longer exists', async () => { mockGetEvent.mockResolvedValue(null); const result = await sendRedemptionConfirmation(RECIPIENT); expect(result).toBe(false); expect(mockSendMail).not.toHaveBeenCalled(); }); it('propagates the relay result', async () => { mockSendMail.mockResolvedValue(false); expect(await sendRedemptionConfirmation(RECIPIENT)).toBe(false); }); }); describe('recordConfirmationEmailResult', () => { const makeConn = () => ({query: jest.fn().mockResolvedValue(undefined), end: jest.fn().mockResolvedValue(undefined)}); it('writes SENT when the mail was accepted', async () => { const conn = makeConn(); mockGetConnection.mockResolvedValue(conn); await recordConfirmationEmailResult(7, true); expect(conn.query).toHaveBeenCalledWith( 'UPDATE redemptions SET confirmation_email_status = ? WHERE redemption_id = ?', ['SENT', 7] ); expect(conn.end).toHaveBeenCalled(); }); it('writes FAILED when the mail was not accepted', async () => { const conn = makeConn(); mockGetConnection.mockResolvedValue(conn); await recordConfirmationEmailResult(7, false); expect(conn.query).toHaveBeenCalledWith(expect.any(String), ['FAILED', 7]); }); it('swallows a DB error rather than throwing', async () => { const conn = {query: jest.fn().mockRejectedValue(new Error('db down')), end: jest.fn().mockResolvedValue(undefined)}; mockGetConnection.mockResolvedValue(conn); await expect(recordConfirmationEmailResult(7, true)).resolves.toBeUndefined(); expect(conn.end).toHaveBeenCalled(); }); });