Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
2
3import makeSendEmail, {
4 CoopEmailAddress,
5 makeSendEmailViaSendGrid,
6 type Message,
7} from './sendEmailService.js';
8
9function makeMockClient() {
10 const mockSend = jest.fn().mockResolvedValue({ MessageId: 'test-message-id' });
11 const mockClient = { send: mockSend } as unknown as SESClient;
12 return { mockSend, mockClient };
13}
14
15describe('sendEmailService', () => {
16 describe('SES backend (default)', () => {
17 it('should create a function', () => {
18 const { mockClient } = makeMockClient();
19 const sendEmail = makeSendEmail(mockClient);
20 expect(typeof sendEmail).toBe('function');
21 });
22
23 it('should send email with correct SES parameters', async () => {
24 const { mockSend, mockClient } = makeMockClient();
25 const sendEmail = makeSendEmail(mockClient);
26
27 const msg: Message = {
28 to: 'recipient@example.com',
29 from: CoopEmailAddress.NoReply,
30 subject: 'Test Subject',
31 html: '<p>Hello</p>',
32 };
33
34 await sendEmail(msg);
35
36 expect(mockSend).toHaveBeenCalledTimes(1);
37 const command = mockSend.mock.calls[0][0];
38 expect(command).toBeInstanceOf(SendEmailCommand);
39 expect(command.input).toEqual({
40 Source: CoopEmailAddress.NoReply,
41 Destination: {
42 ToAddresses: ['recipient@example.com'],
43 },
44 Message: {
45 Subject: { Charset: 'UTF-8', Data: 'Test Subject' },
46 Body: {
47 Html: { Charset: 'UTF-8', Data: '<p>Hello</p>' },
48 },
49 },
50 });
51 });
52
53 it('should format Source as "Name <email>" for object from address', async () => {
54 const { mockSend, mockClient } = makeMockClient();
55 const sendEmail = makeSendEmail(mockClient);
56
57 const msg: Message = {
58 to: 'recipient@example.com',
59 from: { name: 'Coop', email: CoopEmailAddress.NoReply },
60 subject: 'Test Subject',
61 html: '<p>Hello</p>',
62 };
63
64 await sendEmail(msg);
65
66 const command = mockSend.mock.calls[0][0];
67 expect(command.input.Source).toBe(
68 `Coop <${CoopEmailAddress.NoReply}>`,
69 );
70 });
71
72 it('should use Body.Text instead of Body.Html for text content', async () => {
73 const { mockSend, mockClient } = makeMockClient();
74 const sendEmail = makeSendEmail(mockClient);
75
76 const msg: Message = {
77 to: 'recipient@example.com',
78 from: CoopEmailAddress.NoReply,
79 subject: 'Test Subject',
80 text: 'Hello plain text',
81 };
82
83 await sendEmail(msg);
84
85 const command = mockSend.mock.calls[0][0];
86 expect(command.input.Message.Body).toEqual({
87 Text: { Charset: 'UTF-8', Data: 'Hello plain text' },
88 });
89 });
90
91 it('should pass array directly to ToAddresses', async () => {
92 const { mockSend, mockClient } = makeMockClient();
93 const sendEmail = makeSendEmail(mockClient);
94
95 const msg: Message = {
96 to: ['a@example.com', 'b@example.com'],
97 from: CoopEmailAddress.NoReply,
98 subject: 'Test',
99 text: 'Hello',
100 };
101
102 await sendEmail(msg);
103
104 const command = mockSend.mock.calls[0][0];
105 expect(command.input.Destination.ToAddresses).toEqual([
106 'a@example.com',
107 'b@example.com',
108 ]);
109 });
110
111 it('should not throw when SES returns an error', async () => {
112 const { mockSend, mockClient } = makeMockClient();
113 mockSend.mockRejectedValueOnce(new Error('SES error'));
114 const sendEmail = makeSendEmail(mockClient);
115
116 const msg: Message = {
117 to: 'recipient@example.com',
118 from: CoopEmailAddress.NoReply,
119 subject: 'Test',
120 text: 'Hello',
121 };
122
123 await expect(sendEmail(msg)).resolves.not.toThrow();
124 });
125
126 it('should log the error when SES fails', async () => {
127 const consoleSpy = jest
128 .spyOn(console, 'error')
129 .mockImplementation(() => {});
130 const { mockSend, mockClient } = makeMockClient();
131 mockSend.mockRejectedValueOnce(new Error('MessageRejected'));
132 const sendEmail = makeSendEmail(mockClient);
133
134 const msg: Message = {
135 to: 'recipient@example.com',
136 from: CoopEmailAddress.NoReply,
137 subject: 'Test',
138 text: 'Hello',
139 };
140
141 await sendEmail(msg);
142
143 expect(consoleSpy).toHaveBeenCalledWith(
144 'Failed to send email:',
145 'MessageRejected',
146 );
147 consoleSpy.mockRestore();
148 });
149 });
150
151 describe('SendGrid backend', () => {
152 it('should create a function when given an API key', () => {
153 const sendEmail = makeSendEmailViaSendGrid('SG.test-key');
154 expect(typeof sendEmail).toBe('function');
155 });
156 });
157
158 describe('CoopEmailAddress defaults', () => {
159 it('should have default email addresses', () => {
160 expect(CoopEmailAddress.NoReply).toBeDefined();
161 expect(CoopEmailAddress.Support).toBeDefined();
162 expect(CoopEmailAddress.Team).toBeDefined();
163 });
164 });
165});