11package api
22
33import (
4+ "encoding/json"
45 "net/http"
6+ "net/url"
57 "testing"
68
79 "net/http/httptest"
@@ -13,6 +15,7 @@ import (
1315 "github.com/supabase/auth/internal/conf"
1416 "github.com/supabase/auth/internal/hooks/hookserrors"
1517 "github.com/supabase/auth/internal/hooks/v0hooks"
18+ mail "github.com/supabase/auth/internal/mailer"
1619 "github.com/supabase/auth/internal/models"
1720 "github.com/supabase/auth/internal/storage"
1821
@@ -294,3 +297,173 @@ func (ts *HooksTestSuite) TestInvokeHookIntegration() {
294297 // Ensure that all expected HTTP interactions (mocks) have been called
295298 require .True (ts .T (), gock .IsDone (), "Expected all mocks to have been called including retry" )
296299}
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