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