-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathmail.controller.test.js
More file actions
116 lines (95 loc) · 3.52 KB
/
Copy pathmail.controller.test.js
File metadata and controls
116 lines (95 loc) · 3.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
'use strict';
process.env.REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379/0";
process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || "0123456789012345678901234567890a";
jest.mock('resend', () => {
const sendMock = jest.fn(() => Promise.resolve({ data: { id: 'mail-123' }, error: null }));
return {
Resend: jest.fn(() => ({
emails: { send: sendMock },
})),
__sendMock: sendMock,
};
});
jest.mock('@urbackend/common', () => {
const { sendMailSchema } = require('../../../../packages/common/src/utils/input.validation');
const redisMock = {
status: 'ready',
incr: jest.fn(),
expire: jest.fn(),
decr: jest.fn(),
};
return {
sendMailSchema,
Project: { findById: jest.fn() },
decrypt: jest.fn(),
redis: redisMock,
};
});
const { Resend } = require('resend');
const { Project, decrypt, redis } = require('@urbackend/common');
const mailController = require('../controllers/mail.controller');
const makeReq = () => ({
keyRole: 'secret',
project: { _id: 'proj_1' },
body: { to: 'user@example.com', subject: 'Hello', text: 'This is a message.' },
});
const makeRes = () => {
const res = { status: jest.fn(), json: jest.fn() };
res.status.mockReturnValue(res);
res.json.mockReturnValue(res);
return res;
};
const mockProjectConfig = (payload) => {
Project.findById.mockReturnValue({
select: jest.fn(() => ({
lean: jest.fn(() => Promise.resolve(payload)),
})),
});
};
describe('mail.controller', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env.RESEND_API_KEY = 'default-key';
process.env.EMAIL_FROM = 'mail@urbackend.app';
});
test('sends mail using BYOK key when configured', async () => {
const req = makeReq();
const res = makeRes();
mockProjectConfig({ _id: 'proj_1', resendApiKey: {} });
decrypt.mockReturnValue('byok-key');
redis.incr.mockResolvedValue(1);
await mailController.sendMail(req, res);
expect(redis.incr).toHaveBeenCalledTimes(1);
expect(redis.expire).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
success: true,
data: expect.objectContaining({ provider: 'byok', monthlyUsage: 1 }),
}));
});
test('falls back to default key when BYOK missing', async () => {
const req = makeReq();
const res = makeRes();
mockProjectConfig({ _id: 'proj_1', resendApiKey: null });
decrypt.mockReturnValue(null);
redis.incr.mockResolvedValue(2);
await mailController.sendMail(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ provider: 'default', monthlyUsage: 2 }),
}));
});
test('enforces monthly limit', async () => {
const req = makeReq();
const res = makeRes();
mockProjectConfig({ _id: 'proj_1', resendApiKey: null });
decrypt.mockReturnValue(null);
redis.incr.mockResolvedValue(101);
await mailController.sendMail(req, res);
expect(redis.decr).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
error: 'Monthly mail limit exceeded.',
}));
});
});