@@ -16,6 +16,9 @@ import {
1616} from './recovery-phone.errors' ;
1717import { LOGGER_PROVIDER } from '@fxa/shared/log' ;
1818import { StatsDService } from '@fxa/shared/metrics/statsd' ;
19+ import { MessageStatus } from 'twilio/lib/rest/api/v2010/account/message' ;
20+ import { TwilioConfig } from './twilio.config' ;
21+ import { getExpectedTwilioSignature } from 'twilio/lib/webhooks/webhooks' ;
1922
2023describe ( 'RecoveryPhoneService' , ( ) => {
2124 const phoneNumber = '+15005551234' ;
@@ -25,14 +28,18 @@ describe('RecoveryPhoneService', () => {
2528 const mockLogger = {
2629 error : jest . fn ( ) ,
2730 } ;
31+
2832 const mockMetrics = {
2933 gauge : jest . fn ( ) ,
3034 increment : jest . fn ( ) ,
3135 } ;
36+
3237 const mockSmsManager = {
3338 sendSMS : jest . fn ( ) ,
3439 phoneNumberLookup : jest . fn ( ) ,
40+ messageStatus : jest . fn ( ) ,
3541 } ;
42+
3643 const mockRecoveryPhoneManager = {
3744 storeUnconfirmed : jest . fn ( ) ,
3845 getUnconfirmed : jest . fn ( ) ,
@@ -43,15 +50,27 @@ describe('RecoveryPhoneService', () => {
4350 hasRecoveryCodes : jest . fn ( ) ,
4451 removeCode : jest . fn ( ) ,
4552 } ;
46- const mockOtpManager = { generateCode : jest . fn ( ) } ;
47- const mockRecoveryPhoneConfig = {
53+
54+ const mockOtpManager = {
55+ generateCode : jest . fn ( ) ,
56+ } ;
57+
58+ const mockRecoveryPhoneConfig : RecoveryPhoneConfig = {
4859 enabled : true ,
4960 allowedRegions : [ 'US' ] ,
5061 sms : {
5162 validNumberPrefixes : [ '+1500' ] ,
5263 smsPumpingRiskThreshold : 75 ,
5364 } ,
54- } ;
65+ } satisfies RecoveryPhoneConfig ;
66+
67+ const mockTwilioConfig : TwilioConfig = {
68+ accountSid : 'AC00000000000000000000000000000000' ,
69+ authToken : '00000000000000000000000000000000' ,
70+ webhookUrl : 'http://accounts.firefox.com/recovery-phone/message-status' ,
71+ validateWebhookCalls : true ,
72+ } satisfies TwilioConfig ;
73+
5574 const mockError = new Error ( 'BOOM' ) ;
5675
5776 let service : RecoveryPhoneService ;
@@ -70,6 +89,10 @@ describe('RecoveryPhoneService', () => {
7089 provide : RecoveryPhoneConfig ,
7190 useValue : mockRecoveryPhoneConfig ,
7291 } ,
92+ {
93+ provide : TwilioConfig ,
94+ useValue : mockTwilioConfig ,
95+ } ,
7396 {
7497 provide : LOGGER_PROVIDER ,
7598 useValue : mockLogger ,
@@ -588,4 +611,63 @@ describe('RecoveryPhoneService', () => {
588611 expect ( service . stripPhoneNumber ( phoneNumber , 4 ) ) . toEqual ( '' ) ;
589612 } ) ;
590613 } ) ;
614+
615+ describe ( 'can handle message status update' , ( ) => {
616+ it ( 'can handle message status update' , async ( ) => {
617+ const messageUpdate = {
618+ AccountSid : 'AC123' ,
619+ From : '+123456789' ,
620+ MessageSid : 'MS123' ,
621+ MessageStatus : 'delivered' as MessageStatus ,
622+ } ;
623+ await service . onMessageStatusUpdate ( messageUpdate ) ;
624+ expect ( mockSmsManager . messageStatus ) . toBeCalledWith ( messageUpdate ) ;
625+ } ) ;
626+ } ) ;
627+
628+ describe ( 'verify twilio signature' , ( ) => {
629+ // This is how Twilio generates the signature, see following doc for more info:
630+ // https://www.twilio.com/docs/usage/security#test-the-validity-of-your-webhook-signature
631+ const signature = getExpectedTwilioSignature (
632+ mockTwilioConfig . authToken ,
633+ mockTwilioConfig . webhookUrl ,
634+ {
635+ foo : 'bar' ,
636+ }
637+ ) ;
638+
639+ afterEach ( ( ) => {
640+ mockTwilioConfig . validateWebhookCalls = true ;
641+ } ) ;
642+
643+ it ( 'can validate twilio signature' , ( ) => {
644+ const valid = service . validateTwilioSignature ( signature , {
645+ foo : 'bar' ,
646+ } ) ;
647+ expect ( valid ) . toBeTruthy ( ) ;
648+ } ) ;
649+
650+ it ( 'can invalidate twilio signature due to bad payload' , ( ) => {
651+ const valid = service . validateTwilioSignature ( signature , {
652+ foo : 'bar' ,
653+ bar : 'baz' ,
654+ } ) ;
655+ expect ( valid ) . toBeFalsy ( ) ;
656+ } ) ;
657+
658+ it ( 'can invalidate twilio signature due to bad signature' , ( ) => {
659+ const valid = service . validateTwilioSignature ( signature + '0' , {
660+ foo : 'bar' ,
661+ } ) ;
662+ expect ( valid ) . toBeFalsy ( ) ;
663+ } ) ;
664+
665+ it ( 'will always validate if validateWebhookCalls is false' , ( ) => {
666+ mockTwilioConfig . validateWebhookCalls = false ;
667+ const valid = service . validateTwilioSignature ( signature + '0' , {
668+ foo : 'bar' ,
669+ } ) ;
670+ expect ( valid ) . toBeTruthy ( ) ;
671+ } ) ;
672+ } ) ;
591673} ) ;
0 commit comments