1
1
package api
2
2
3
3
import (
4
+ "encoding/json"
4
5
"net/http"
6
+ "net/url"
5
7
"testing"
6
8
7
9
"net/http/httptest"
@@ -13,6 +15,7 @@ import (
13
15
"github.com/supabase/auth/internal/conf"
14
16
"github.com/supabase/auth/internal/hooks/hookserrors"
15
17
"github.com/supabase/auth/internal/hooks/v0hooks"
18
+ mail "github.com/supabase/auth/internal/mailer"
16
19
"github.com/supabase/auth/internal/models"
17
20
"github.com/supabase/auth/internal/storage"
18
21
@@ -294,3 +297,173 @@ func (ts *HooksTestSuite) TestInvokeHookIntegration() {
294
297
// Ensure that all expected HTTP interactions (mocks) have been called
295
298
require .True (ts .T (), gock .IsDone (), "Expected all mocks to have been called including retry" )
296
299
}
300
+
301
+ func (ts * HooksTestSuite ) TestAccountChangesNotificationsHookPayload () {
302
+ // Setup hook config for send_email hook
303
+ defer gock .OffAll ()
304
+
305
+ testURL := "http://localhost:8888/functions/v1/send-email"
306
+ ts .Config .Hook .SendEmail .URI = testURL
307
+ ts .Config .Hook .SendEmail .Enabled = true
308
+
309
+ // Mock the hook endpoint to capture the payload
310
+ var capturedPayload * v0hooks.SendEmailInput
311
+
312
+ gock .New (testURL ).
313
+ Post ("/" ).
314
+ MatchType ("json" ).
315
+ SetMatcher (gock .NewMatcher ()).
316
+ AddMatcher (func (req * http.Request , greq * gock.Request ) (bool , error ) {
317
+ // Capture the payload
318
+ payload := & v0hooks.SendEmailInput {}
319
+ if err := json .NewDecoder (req .Body ).Decode (payload ); err != nil {
320
+ return false , err
321
+ }
322
+ capturedPayload = payload
323
+ return true , nil
324
+ }).
325
+ Persist ().
326
+ Reply (http .StatusOK ).
327
+ JSON (v0hooks.SendEmailOutput {})
328
+
329
+ testCases := []struct {
330
+ description string
331
+ expectedActionType string
332
+ expectedProvider string
333
+ expectedOldEmail string
334
+ expectedOldPhone string
335
+ expectedFactorType string
336
+ setupFunc func () error
337
+ enableNotification func ()
338
+ }{
339
+ {
340
+ description : "IdentityLinkedNotification contains provider" ,
341
+ expectedActionType : mail .IdentityLinkedNotification ,
342
+ expectedProvider : "google" ,
343
+ enableNotification : func () {
344
+ ts .Config .Mailer .Notifications .IdentityLinkedEnabled = true
345
+ },
346
+ setupFunc : func () error {
347
+ req := httptest .NewRequest ("POST" , "/identities" , nil )
348
+ externalHost , err := url .Parse ("http://example.com" )
349
+ require .NoError (ts .T (), err )
350
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
351
+ return ts .API .sendIdentityLinkedNotification (req , ts .API .db , ts .TestUser , "google" )
352
+ },
353
+ },
354
+ {
355
+ description : "IdentityUnlinkedNotification contains provider" ,
356
+ expectedActionType : mail .IdentityUnlinkedNotification ,
357
+ expectedProvider : "github" ,
358
+ enableNotification : func () {
359
+ ts .Config .Mailer .Notifications .IdentityUnlinkedEnabled = true
360
+ },
361
+ setupFunc : func () error {
362
+ req := httptest .NewRequest ("DELETE" , "/identities/123" , nil )
363
+ externalHost , err := url .Parse ("http://example.com" )
364
+ require .NoError (ts .T (), err )
365
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
366
+ return ts .API .sendIdentityUnlinkedNotification (req , ts .API .db , ts .TestUser , "github" )
367
+ },
368
+ },
369
+ {
370
+ description : "EmailChangedNotification contains old_email" ,
371
+ expectedActionType : mail .EmailChangedNotification ,
372
+ expectedOldEmail :
"[email protected] " ,
373
+ enableNotification : func () {
374
+ ts .Config .Mailer .Notifications .EmailChangedEnabled = true
375
+ },
376
+ setupFunc : func () error {
377
+ req := httptest .NewRequest ("PUT" , "/user" , nil )
378
+ externalHost , err := url .Parse ("http://example.com" )
379
+ require .NoError (ts .T (), err )
380
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
381
+ return ts .
API .
sendEmailChangedNotification (
req ,
ts .
API .
db ,
ts .
TestUser ,
"[email protected] " )
382
+ },
383
+ },
384
+ {
385
+ description : "PhoneChangedNotification contains old_phone" ,
386
+ expectedActionType : mail .PhoneChangedNotification ,
387
+ expectedOldPhone : "+15551234567" ,
388
+ enableNotification : func () {
389
+ ts .Config .Mailer .Notifications .PhoneChangedEnabled = true
390
+ },
391
+ setupFunc : func () error {
392
+ req := httptest .NewRequest ("PUT" , "/user" , nil )
393
+ externalHost , err := url .Parse ("http://example.com" )
394
+ require .NoError (ts .T (), err )
395
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
396
+ return ts .API .sendPhoneChangedNotification (req , ts .API .db , ts .TestUser , "+15551234567" )
397
+ },
398
+ },
399
+ {
400
+ description : "MFAFactorEnrolledNotification contains factor_type" ,
401
+ expectedActionType : mail .MFAFactorEnrolledNotification ,
402
+ expectedFactorType : "totp" ,
403
+ enableNotification : func () {
404
+ ts .Config .Mailer .Notifications .MFAFactorEnrolledEnabled = true
405
+ },
406
+ setupFunc : func () error {
407
+ req := httptest .NewRequest ("POST" , "/factors" , nil )
408
+ externalHost , err := url .Parse ("http://example.com" )
409
+ require .NoError (ts .T (), err )
410
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
411
+ return ts .API .sendMFAFactorEnrolledNotification (req , ts .API .db , ts .TestUser , "totp" )
412
+ },
413
+ },
414
+ {
415
+ description : "MFAFactorUnenrolledNotification contains factor_type" ,
416
+ expectedActionType : mail .MFAFactorUnenrolledNotification ,
417
+ expectedFactorType : "phone" ,
418
+ enableNotification : func () {
419
+ ts .Config .Mailer .Notifications .MFAFactorUnenrolledEnabled = true
420
+ },
421
+ setupFunc : func () error {
422
+ req := httptest .NewRequest ("DELETE" , "/factors/123" , nil )
423
+ externalHost , err := url .Parse ("http://example.com" )
424
+ require .NoError (ts .T (), err )
425
+ req = req .WithContext (withExternalHost (req .Context (), externalHost ))
426
+ return ts .API .sendMFAFactorUnenrolledNotification (req , ts .API .db , ts .TestUser , "phone" )
427
+ },
428
+ },
429
+ }
430
+
431
+ for _ , tc := range testCases {
432
+ ts .Run (tc .description , func () {
433
+ // Reset captured payload
434
+ capturedPayload = nil
435
+
436
+ // Enable the notification
437
+ tc .enableNotification ()
438
+
439
+ // Execute the setup function that triggers the notification
440
+ err := tc .setupFunc ()
441
+ require .NoError (ts .T (), err )
442
+
443
+ // Verify the payload was captured
444
+ require .NotNil (ts .T (), capturedPayload , "Hook should have been called" )
445
+
446
+ // Verify email action type
447
+ require .Equal (ts .T (), tc .expectedActionType , capturedPayload .EmailData .EmailActionType )
448
+
449
+ // Verify notification-specific fields
450
+ if tc .expectedProvider != "" {
451
+ require .Equal (ts .T (), tc .expectedProvider , capturedPayload .EmailData .Provider , "Provider should be set in EmailData" )
452
+ }
453
+ if tc .expectedOldEmail != "" {
454
+ require .Equal (ts .T (), tc .expectedOldEmail , capturedPayload .EmailData .OldEmail , "OldEmail should be set in EmailData" )
455
+ }
456
+ if tc .expectedOldPhone != "" {
457
+ require .Equal (ts .T (), tc .expectedOldPhone , capturedPayload .EmailData .OldPhone , "OldPhone should be set in EmailData" )
458
+ }
459
+ if tc .expectedFactorType != "" {
460
+ require .Equal (ts .T (), tc .expectedFactorType , capturedPayload .EmailData .FactorType , "FactorType should be set in EmailData" )
461
+ }
462
+
463
+ // Verify common fields
464
+ require .Equal (ts .T (), ts .TestUser .ID , capturedPayload .User .ID , "User ID should match" )
465
+ require .NotEmpty (ts .T (), capturedPayload .EmailData .SiteURL , "SiteURL should be set" )
466
+ require .NotEmpty (ts .T (), capturedPayload .EmailData .RedirectTo , "RedirectTo should be set" )
467
+ })
468
+ }
469
+ }
0 commit comments