Skip to content

Commit 46f1a7b

Browse files
committed
Add unit tests to pages/api/_utils files
1 parent 89b3402 commit 46f1a7b

3 files changed

Lines changed: 474 additions & 1 deletion

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
2+
import { sendDSARVerificationEmail } from '@pages/api/_utils/gdpr-email'
3+
4+
// Mock the Resend module
5+
vi.mock('resend', () => {
6+
const mockSend = vi.fn()
7+
return {
8+
Resend: vi.fn().mockImplementation(() => ({
9+
emails: {
10+
send: mockSend,
11+
},
12+
})),
13+
}
14+
})
15+
16+
describe('GDPR Email Utils', () => {
17+
let mockResendSend: ReturnType<typeof vi.fn>
18+
let originalEnv: Record<string, string | undefined>
19+
let consoleLogSpy: ReturnType<typeof vi.fn>
20+
let consoleErrorSpy: ReturnType<typeof vi.fn>
21+
22+
beforeEach(async () => {
23+
// Store original env
24+
originalEnv = { ...process.env }
25+
26+
// Mock console methods
27+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
28+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
29+
30+
// Get the mock function from the mocked Resend class
31+
const { Resend } = await import('resend')
32+
const resendInstance = new Resend('test-key')
33+
mockResendSend = resendInstance.emails.send as ReturnType<typeof vi.fn>
34+
35+
// Reset all mocks
36+
vi.clearAllMocks()
37+
})
38+
39+
afterEach(() => {
40+
// Restore original environment
41+
process.env = originalEnv
42+
consoleLogSpy.mockRestore()
43+
consoleErrorSpy.mockRestore()
44+
})
45+
46+
describe('sendDSARVerificationEmail', () => {
47+
describe('in development/test environment', () => {
48+
it('should log email details instead of sending in development mode', async () => {
49+
process.env['NODE_ENV'] = 'development'
50+
51+
await sendDSARVerificationEmail('test@example.com', 'test-token', 'ACCESS')
52+
53+
expect(consoleLogSpy).toHaveBeenCalledWith(
54+
'[DEV/TEST MODE] DSAR verification email would be sent:',
55+
{
56+
email: 'test@example.com',
57+
token: 'test-token',
58+
requestType: 'ACCESS',
59+
}
60+
)
61+
expect(mockResendSend).not.toHaveBeenCalled()
62+
})
63+
64+
it('should log email details instead of sending in test mode', async () => {
65+
process.env['NODE_ENV'] = 'test'
66+
67+
await sendDSARVerificationEmail('test@example.com', 'test-token', 'DELETE')
68+
69+
expect(consoleLogSpy).toHaveBeenCalledWith(
70+
'[DEV/TEST MODE] DSAR verification email would be sent:',
71+
{
72+
email: 'test@example.com',
73+
token: 'test-token',
74+
requestType: 'DELETE',
75+
}
76+
)
77+
expect(mockResendSend).not.toHaveBeenCalled()
78+
})
79+
80+
it('should log email details instead of sending in CI mode', async () => {
81+
process.env['NODE_ENV'] = 'production'
82+
process.env['CI'] = 'true'
83+
84+
await sendDSARVerificationEmail('test@example.com', 'test-token', 'ACCESS')
85+
86+
expect(consoleLogSpy).toHaveBeenCalledWith(
87+
'[DEV/TEST MODE] DSAR verification email would be sent:',
88+
{
89+
email: 'test@example.com',
90+
token: 'test-token',
91+
requestType: 'ACCESS',
92+
}
93+
)
94+
expect(mockResendSend).not.toHaveBeenCalled()
95+
})
96+
})
97+
98+
describe('in production environment', () => {
99+
beforeEach(() => {
100+
process.env['NODE_ENV'] = 'production'
101+
process.env['CI'] = 'false'
102+
process.env['RESEND_API_KEY'] = 'test-resend-key'
103+
process.env['SITE_URL'] = 'https://webstackbuilders.com'
104+
})
105+
106+
it('should send ACCESS verification email successfully', async () => {
107+
mockResendSend.mockResolvedValue({
108+
data: { id: 'message-id-123' },
109+
error: null,
110+
})
111+
112+
await sendDSARVerificationEmail('user@example.com', 'verification-token-123', 'ACCESS')
113+
114+
expect(mockResendSend).toHaveBeenCalledTimes(1)
115+
const callArgs = mockResendSend.mock.calls[0]?.[0]
116+
expect(callArgs).toBeDefined()
117+
118+
expect(callArgs!.from).toBe('Webstack Builders <privacy@webstackbuilders.com>')
119+
expect(callArgs!.to).toBe('user@example.com')
120+
expect(callArgs!.subject).toBe('Verify Your Data Access Request - Webstack Builders')
121+
expect(callArgs!.html).toContain('Data Access Request')
122+
expect(callArgs!.html).toContain('access your data')
123+
expect(callArgs!.html).toContain('https://webstackbuilders.com/api/gdpr/verify?token=verification-token-123')
124+
expect(callArgs!.text).toContain('Data Access Request')
125+
expect(callArgs!.tags).toEqual([
126+
{ name: 'type', value: 'gdpr-verification' },
127+
{ name: 'request-type', value: 'access' },
128+
])
129+
130+
expect(consoleLogSpy).toHaveBeenCalledWith(
131+
'[DSAR Email] Verification sent successfully:',
132+
{
133+
email: 'user@example.com',
134+
requestType: 'ACCESS',
135+
messageId: 'message-id-123',
136+
}
137+
)
138+
})
139+
140+
it('should send DELETE verification email successfully with warning', async () => {
141+
mockResendSend.mockResolvedValue({
142+
data: { id: 'message-id-456' },
143+
error: null,
144+
})
145+
146+
await sendDSARVerificationEmail('user@example.com', 'delete-token-456', 'DELETE')
147+
148+
expect(mockResendSend).toHaveBeenCalledTimes(1)
149+
const callArgs = mockResendSend.mock.calls[0]?.[0]
150+
expect(callArgs).toBeDefined()
151+
152+
expect(callArgs!.subject).toBe('Verify Your Data Deletion Request - Webstack Builders')
153+
expect(callArgs!.html).toContain('Data Deletion Request')
154+
expect(callArgs!.html).toContain('delete your data')
155+
expect(callArgs!.html).toContain('⚠️ Important')
156+
expect(callArgs!.html).toContain('permanently delete all your data')
157+
expect(callArgs!.text).toContain('⚠️ IMPORTANT')
158+
expect(callArgs!.tags).toEqual([
159+
{ name: 'type', value: 'gdpr-verification' },
160+
{ name: 'request-type', value: 'delete' },
161+
])
162+
})
163+
164+
it('should use default localhost URL when SITE_URL is not set', async () => {
165+
delete process.env['SITE_URL']
166+
mockResendSend.mockResolvedValue({
167+
data: { id: 'message-id-789' },
168+
error: null,
169+
})
170+
171+
await sendDSARVerificationEmail('user@example.com', 'token-789', 'ACCESS')
172+
173+
const callArgs = mockResendSend.mock.calls[0]?.[0]
174+
expect(callArgs).toBeDefined()
175+
expect(callArgs!.html).toContain('http://localhost:4321/api/gdpr/verify?token=token-789')
176+
expect(callArgs!.text).toContain('http://localhost:4321/api/gdpr/verify?token=token-789')
177+
})
178+
179+
it('should throw error when RESEND_API_KEY is not set', async () => {
180+
delete process.env['RESEND_API_KEY']
181+
182+
await expect(
183+
sendDSARVerificationEmail('user@example.com', 'token-123', 'ACCESS')
184+
).rejects.toThrow('RESEND_API_KEY environment variable is not set')
185+
186+
expect(mockResendSend).not.toHaveBeenCalled()
187+
})
188+
189+
it('should handle Resend API error response', async () => {
190+
mockResendSend.mockResolvedValue({
191+
data: null,
192+
error: {
193+
message: 'Invalid API key',
194+
name: 'validation_error',
195+
},
196+
})
197+
198+
await expect(
199+
sendDSARVerificationEmail('user@example.com', 'token-123', 'ACCESS')
200+
).rejects.toThrow('Failed to send verification email: Invalid API key')
201+
202+
expect(consoleErrorSpy).toHaveBeenCalledWith(
203+
'[DSAR Email] Failed to send verification:',
204+
expect.objectContaining({
205+
message: 'Invalid API key',
206+
name: 'validation_error',
207+
})
208+
)
209+
})
210+
211+
it('should handle Resend API network error', async () => {
212+
const networkError = new Error('Network failure')
213+
mockResendSend.mockRejectedValue(networkError)
214+
215+
await expect(
216+
sendDSARVerificationEmail('user@example.com', 'token-123', 'ACCESS')
217+
).rejects.toThrow('Network failure')
218+
219+
expect(consoleErrorSpy).toHaveBeenCalledWith(
220+
'[DSAR Email] Error sending verification:',
221+
networkError
222+
)
223+
})
224+
225+
it('should include current year in email content', async () => {
226+
const currentYear = new Date().getFullYear()
227+
mockResendSend.mockResolvedValue({
228+
data: { id: 'message-id-year' },
229+
error: null,
230+
})
231+
232+
await sendDSARVerificationEmail('user@example.com', 'token-year', 'ACCESS')
233+
234+
const callArgs = mockResendSend.mock.calls[0]?.[0]
235+
expect(callArgs).toBeDefined()
236+
expect(callArgs!.html).toContain(`© ${currentYear} Webstack Builders`)
237+
expect(callArgs!.text).toContain(`© ${currentYear} Webstack Builders`)
238+
})
239+
240+
it('should generate proper verification URLs with tokens', async () => {
241+
process.env['SITE_URL'] = 'https://example.com'
242+
mockResendSend.mockResolvedValue({
243+
data: { id: 'message-id-url' },
244+
error: null,
245+
})
246+
247+
await sendDSARVerificationEmail('user@example.com', 'special-token-123', 'DELETE')
248+
249+
const callArgs = mockResendSend.mock.calls[0]?.[0]
250+
expect(callArgs).toBeDefined()
251+
const expectedUrl = 'https://example.com/api/gdpr/verify?token=special-token-123'
252+
253+
expect(callArgs!.html).toContain(`href="${expectedUrl}"`)
254+
expect(callArgs!.html).toContain(expectedUrl) // Also as plain text in email
255+
expect(callArgs!.text).toContain(expectedUrl)
256+
})
257+
})
258+
})
259+
})

0 commit comments

Comments
 (0)