Skip to content

Commit fed0072

Browse files
authored
Merge pull request #18433 from mozilla/FXA-11072
task(recovery-phone, auth): Allow for fallback sms messages
2 parents 126ea5c + 44cf9af commit fed0072

12 files changed

Lines changed: 272 additions & 86 deletions

File tree

libs/accounts/recovery-phone/src/lib/recovery-phone.errors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,19 @@ export class RecoveryPhoneRegistrationLimitReached extends RecoveryPhoneError {
8484
);
8585
}
8686
}
87+
88+
export class MessageBodyTooLong extends RecoveryPhoneError {
89+
constructor(
90+
public readonly maxSegmentLength: number,
91+
public readonly segmentCount: number,
92+
public readonly encoding: string,
93+
public readonly body: string
94+
) {
95+
super('SMS body exceeds max segment length', {
96+
maxSegmentLength,
97+
segmentCount,
98+
body,
99+
encoding,
100+
});
101+
}
102+
}

libs/accounts/recovery-phone/src/lib/recovery-phone.service.spec.ts

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { StatsDService } from '@fxa/shared/metrics/statsd';
2020
import { MessageStatus } from 'twilio/lib/rest/api/v2010/account/message';
2121
import { TwilioConfig } from './twilio.config';
2222
import { getExpectedTwilioSignature } from 'twilio/lib/webhooks/webhooks';
23+
import { SegmentedMessage } from 'sms-segments-calculator';
2324

2425
describe('RecoveryPhoneService', () => {
2526
const phoneNumber = '+15005551234';
@@ -28,6 +29,7 @@ describe('RecoveryPhoneService', () => {
2829

2930
const mockLogger = {
3031
error: jest.fn(),
32+
warn: jest.fn(),
3133
};
3234

3335
const mockMetrics = {
@@ -39,6 +41,7 @@ describe('RecoveryPhoneService', () => {
3941
sendSMS: jest.fn(),
4042
phoneNumberLookup: jest.fn(),
4143
messageStatus: jest.fn(),
44+
checkMessageSegments: jest.fn(),
4245
};
4346

4447
const mockRecoveryPhoneManager = {
@@ -74,12 +77,27 @@ describe('RecoveryPhoneService', () => {
7477
validateWebhookCalls: true,
7578
} satisfies TwilioConfig;
7679

80+
const mockGetFormattedMessage = async (code: string) => {
81+
return {
82+
msg: `${code} is your Mozilla verification code. Expires in 5 minutes.`,
83+
shortMsg: `Mozilla verification code: ${code}`,
84+
failsafeMsg: `Mozilla: ${code}`,
85+
};
86+
};
87+
7788
const mockError = new Error('BOOM');
7889

7990
let service: RecoveryPhoneService;
8091

8192
beforeEach(async () => {
8293
mockSmsManager.sendSMS.mockReturnValue({ status: 'success' });
94+
mockSmsManager.checkMessageSegments.mockImplementation((msg: string) => {
95+
const sm = new SegmentedMessage(msg);
96+
return {
97+
overLimit: sm.segmentsCount > 1,
98+
segmentedMessage: sm,
99+
};
100+
});
83101
mockRecoveryPhoneManager.hasRecoveryCodes.mockResolvedValue(true);
84102
mockRecoveryPhoneManager.getAllUnconfirmed.mockResolvedValue([]);
85103

@@ -119,16 +137,20 @@ describe('RecoveryPhoneService', () => {
119137
expect(service).toBeInstanceOf(RecoveryPhoneService);
120138
});
121139

122-
it('Should set up a phone number', async () => {
140+
it('Should setup a phone number', async () => {
123141
mockOtpManager.generateCode.mockReturnValue(code);
124142

125-
const result = await service.setupPhoneNumber(uid, phoneNumber);
143+
const result = await service.setupPhoneNumber(
144+
uid,
145+
phoneNumber,
146+
mockGetFormattedMessage
147+
);
126148

127149
expect(result).toBeTruthy();
128150
expect(mockOtpManager.generateCode).toBeCalled();
129151
expect(mockSmsManager.sendSMS).toBeCalledWith({
130152
to: phoneNumber,
131-
body: code,
153+
body: (await mockGetFormattedMessage(code)).msg,
132154
});
133155
expect(mockRecoveryPhoneManager.storeUnconfirmed).toBeCalledWith(
134156
uid,
@@ -137,6 +159,7 @@ describe('RecoveryPhoneService', () => {
137159
true
138160
);
139161
expect(mockRecoveryPhoneManager.getAllUnconfirmed).toBeCalledWith(uid);
162+
expect(result).toBeTruthy();
140163
});
141164

142165
it('Should send new code to set up a phone number', async () => {
@@ -146,15 +169,19 @@ describe('RecoveryPhoneService', () => {
146169
'this:is:the:code456',
147170
]);
148171

149-
const result = await service.setupPhoneNumber(uid, phoneNumber);
172+
const result = await service.setupPhoneNumber(
173+
uid,
174+
phoneNumber,
175+
mockGetFormattedMessage
176+
);
150177

151178
expect(result).toBeTruthy();
152179
expect(mockRecoveryPhoneManager.removeCode).toBeCalledWith(uid, 'code123');
153180
expect(mockRecoveryPhoneManager.removeCode).toBeCalledWith(uid, 'code456');
154181
expect(mockOtpManager.generateCode).toBeCalled();
155182
expect(mockSmsManager.sendSMS).toBeCalledWith({
156183
to: phoneNumber,
157-
body: code,
184+
body: (await mockGetFormattedMessage(code)).msg,
158185
});
159186
expect(mockRecoveryPhoneManager.storeUnconfirmed).toBeCalledWith(
160187
uid,
@@ -167,19 +194,18 @@ describe('RecoveryPhoneService', () => {
167194

168195
it('handles message template when provided to set up phone number', async () => {
169196
mockOtpManager.generateCode.mockReturnValue(code);
170-
const getFormattedMessage = jest.fn().mockResolvedValue('message');
171197

172198
const result = await service.setupPhoneNumber(
173199
uid,
174200
phoneNumber,
175-
getFormattedMessage
201+
mockGetFormattedMessage
176202
);
177203

178204
expect(result).toBeTruthy();
179205
expect(mockOtpManager.generateCode).toBeCalled();
180206
expect(mockSmsManager.sendSMS).toBeCalledWith({
181207
to: phoneNumber,
182-
body: 'message',
208+
body: (await mockGetFormattedMessage(code)).msg,
183209
});
184210
expect(mockRecoveryPhoneManager.storeUnconfirmed).toBeCalledWith(
185211
uid,
@@ -192,38 +218,38 @@ describe('RecoveryPhoneService', () => {
192218

193219
it('Will reject a phone number that is not part of launch', async () => {
194220
const to = '+16005551234';
195-
await expect(service.setupPhoneNumber(uid, to)).rejects.toEqual(
196-
new RecoveryNumberNotSupportedError(to)
197-
);
221+
await expect(
222+
service.setupPhoneNumber(uid, to, mockGetFormattedMessage)
223+
).rejects.toEqual(new RecoveryNumberNotSupportedError(to));
198224
});
199225

200226
it('Will reject a phone number if it has been used for too many accounts', async () => {
201227
mockRecoveryPhoneManager.getCountByPhoneNumber.mockReturnValue(5);
202228

203-
await expect(service.setupPhoneNumber(uid, phoneNumber)).rejects.toEqual(
204-
new RecoveryPhoneRegistrationLimitReached(phoneNumber)
205-
);
229+
await expect(
230+
service.setupPhoneNumber(uid, phoneNumber, mockGetFormattedMessage)
231+
).rejects.toEqual(new RecoveryPhoneRegistrationLimitReached(phoneNumber));
206232
});
207233

208234
it('Throws error during send sms', async () => {
209235
mockSmsManager.sendSMS.mockRejectedValueOnce(mockError);
210-
await expect(service.setupPhoneNumber(uid, phoneNumber)).rejects.toEqual(
211-
mockError
212-
);
236+
await expect(
237+
service.setupPhoneNumber(uid, phoneNumber, mockGetFormattedMessage)
238+
).rejects.toEqual(mockError);
213239
});
214240

215241
it('Throws error during otp code creation', async () => {
216242
mockOtpManager.generateCode.mockRejectedValueOnce(mockError);
217-
await expect(service.setupPhoneNumber(uid, phoneNumber)).rejects.toEqual(
218-
mockError
219-
);
243+
await expect(
244+
service.setupPhoneNumber(uid, phoneNumber, mockGetFormattedMessage)
245+
).rejects.toEqual(mockError);
220246
});
221247

222248
it('throws error during storing of unconfirmed number', async () => {
223249
mockRecoveryPhoneManager.storeUnconfirmed.mockRejectedValueOnce(mockError);
224-
await expect(service.setupPhoneNumber(uid, phoneNumber)).rejects.toEqual(
225-
mockError
226-
);
250+
await expect(
251+
service.setupPhoneNumber(uid, phoneNumber, mockGetFormattedMessage)
252+
).rejects.toEqual(mockError);
227253
});
228254

229255
describe('has confirmed code', () => {
@@ -458,7 +484,7 @@ describe('RecoveryPhoneService', () => {
458484
mockOtpManager.generateCode.mockResolvedValueOnce(code);
459485
mockSmsManager.sendSMS.mockResolvedValue({ status: 'success' });
460486

461-
const result = await service.sendCode(uid);
487+
const result = await service.sendCode(uid, mockGetFormattedMessage);
462488

463489
expect(result).toBeTruthy();
464490
expect(mockRecoveryPhoneManager.getConfirmedPhoneNumber).toBeCalledWith(
@@ -473,7 +499,47 @@ describe('RecoveryPhoneService', () => {
473499
expect(mockOtpManager.generateCode).toBeCalled();
474500
expect(mockSmsManager.sendSMS).toBeCalledWith({
475501
to: phoneNumber,
476-
body: code,
502+
body: (await mockGetFormattedMessage(code)).msg,
503+
});
504+
});
505+
506+
it('should fallback to the short message', async () => {
507+
mockRecoveryPhoneManager.getConfirmedPhoneNumber.mockResolvedValueOnce({
508+
phoneNumber,
509+
});
510+
mockOtpManager.generateCode.mockResolvedValueOnce(code);
511+
mockSmsManager.sendSMS.mockResolvedValue({ status: 'success' });
512+
513+
const result = await service.sendCode(uid, async () => ({
514+
msg: new Array(161).fill('z').join(''),
515+
shortMsg: `Your Mozilla Account code is ${code}`,
516+
failsafeMsg: `${code}`,
517+
}));
518+
519+
expect(result).toBeTruthy();
520+
expect(mockSmsManager.sendSMS).toBeCalledWith({
521+
to: phoneNumber,
522+
body: `Your Mozilla Account code is ${code}`,
523+
});
524+
});
525+
526+
it('should fallback to the fail safe message', async () => {
527+
mockRecoveryPhoneManager.getConfirmedPhoneNumber.mockResolvedValueOnce({
528+
phoneNumber,
529+
});
530+
mockOtpManager.generateCode.mockResolvedValueOnce(code);
531+
mockSmsManager.sendSMS.mockResolvedValue({ status: 'success' });
532+
533+
const result = await service.sendCode(uid, async () => ({
534+
msg: new Array(161).fill('z').join(''),
535+
shortMsg: new Array(161).fill('z').join(''),
536+
failsafeMsg: `Failsafe: ${code}`,
537+
}));
538+
539+
expect(result).toBeTruthy();
540+
expect(mockSmsManager.sendSMS).toBeCalledWith({
541+
to: phoneNumber,
542+
body: `Failsafe: ${code}`,
477543
});
478544
});
479545

@@ -484,15 +550,17 @@ describe('RecoveryPhoneService', () => {
484550
mockOtpManager.generateCode.mockResolvedValueOnce(code);
485551
mockSmsManager.sendSMS.mockResolvedValue({ status: 'failed' });
486552

487-
const result = await service.sendCode(uid);
553+
const result = await service.sendCode(uid, mockGetFormattedMessage);
488554

489555
expect(result).toBeFalsy();
490556
});
491557

492558
it('should throw error if recovery phone number does not exist', async () => {
493559
const error = new RecoveryNumberNotExistsError(uid);
494560
mockRecoveryPhoneManager.getConfirmedPhoneNumber.mockRejectedValue(error);
495-
expect(service.sendCode(uid)).rejects.toThrow(error);
561+
expect(service.sendCode(uid, mockGetFormattedMessage)).rejects.toThrow(
562+
error
563+
);
496564
});
497565

498566
it('Should send new code for setup phone number', async () => {
@@ -508,7 +576,7 @@ describe('RecoveryPhoneService', () => {
508576
mockOtpManager.generateCode.mockResolvedValueOnce(code);
509577
mockSmsManager.sendSMS.mockResolvedValue({ status: 'success' });
510578

511-
const result = await service.sendCode(uid);
579+
const result = await service.sendCode(uid, mockGetFormattedMessage);
512580

513581
expect(result).toBeTruthy();
514582
expect(mockRecoveryPhoneManager.getConfirmedPhoneNumber).toBeCalledWith(
@@ -523,7 +591,7 @@ describe('RecoveryPhoneService', () => {
523591
expect(mockOtpManager.generateCode).toBeCalled();
524592
expect(mockSmsManager.sendSMS).toBeCalledWith({
525593
to: phoneNumber,
526-
body: code,
594+
body: (await mockGetFormattedMessage(code)).msg,
527595
});
528596

529597
expect(mockRecoveryPhoneManager.removeCode).toBeCalledWith(
@@ -588,12 +656,12 @@ describe('RecoveryPhoneService', () => {
588656
expect(service.confirmSigninCode(uid, '000000')).rejects.toEqual(
589657
new RecoveryPhoneNotEnabled()
590658
);
591-
expect(service.sendCode(uid)).rejects.toEqual(
592-
new RecoveryPhoneNotEnabled()
593-
);
594-
expect(service.setupPhoneNumber(uid, '+15550005555')).rejects.toEqual(
659+
expect(service.sendCode(uid, mockGetFormattedMessage)).rejects.toEqual(
595660
new RecoveryPhoneNotEnabled()
596661
);
662+
expect(
663+
service.setupPhoneNumber(uid, '+15550005555', mockGetFormattedMessage)
664+
).rejects.toEqual(new RecoveryPhoneNotEnabled());
597665
});
598666
});
599667

0 commit comments

Comments
 (0)