feat(phone): Add `getConfirmedPhoneNumber` and `removePhoneNumber` functions

This commit is contained in:
Vijay Budhram 2024-11-14 11:35:01 -05:00
Родитель a2a8c19f45
Коммит 88324a3b1f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 9778545895B2532B
4 изменённых файлов: 111 добавлений и 2 удалений

Просмотреть файл

@ -14,6 +14,12 @@ export class RecoveryPhoneError extends BaseError {
}
}
export class RecoveryNumberNotExistsError extends RecoveryPhoneError {
constructor(uid: string, cause?: Error) {
super('Recovery number does not exist', { uid }, cause);
}
}
export class RecoveryNumberInvalidFormatError extends RecoveryPhoneError {
constructor(uid: string, phoneNumber: string, cause?: Error) {
super('Invalid phone number format', { uid, phoneNumber }, cause);

Просмотреть файл

@ -40,6 +40,32 @@ describe('RecoveryPhoneManager', () => {
await db.destroy();
});
it('should get a recovery phone', async () => {
const mockPhone = RecoveryPhoneFactory();
const { uid, phoneNumber } = mockPhone;
await recoveryPhoneManager.registerPhoneNumber(
uid.toString('hex'),
phoneNumber
);
const result = await recoveryPhoneManager.getConfirmedPhoneNumber(
uid.toString('hex')
);
expect(result.uid).toEqual(mockPhone.uid);
expect(result.phoneNumber).toEqual(mockPhone.phoneNumber);
});
it('should throw if no recovery phone found', async () => {
const mockPhone = RecoveryPhoneFactory();
const { uid } = mockPhone;
await expect(
recoveryPhoneManager.getConfirmedPhoneNumber(uid.toString('hex'))
).rejects.toThrow('Recovery number does not exist');
});
it('should create a recovery phone', async () => {
const insertIntoSpy = jest.spyOn(db, 'insertInto');
const mockPhone = RecoveryPhoneFactory();
@ -62,7 +88,7 @@ describe('RecoveryPhoneManager', () => {
).rejects.toThrow('Invalid phone number format');
});
it('should fail to register if recovery phone already exists', async () => {
it('should throw if recovery phone already exists', async () => {
const mockPhone = RecoveryPhoneFactory();
const { uid, phoneNumber } = mockPhone;
await recoveryPhoneManager.registerPhoneNumber(
@ -75,6 +101,34 @@ describe('RecoveryPhoneManager', () => {
).rejects.toThrow('Recovery number already exists');
});
it('should remove a recovery phone', async () => {
const deleteFromSpy = jest.spyOn(db, 'deleteFrom');
const mockPhone = RecoveryPhoneFactory();
const { uid, phoneNumber } = mockPhone;
await recoveryPhoneManager.registerPhoneNumber(
uid.toString('hex'),
phoneNumber
);
const result = await recoveryPhoneManager.removePhoneNumber(
mockPhone.uid.toString('hex')
);
expect(deleteFromSpy).toBeCalledWith('recoveryPhones');
expect(result).toBe(true);
});
it('should return false if no phone number removed', async () => {
const deleteFromSpy = jest.spyOn(db, 'deleteFrom');
const mockPhone = RecoveryPhoneFactory();
const result = await recoveryPhoneManager.removePhoneNumber(
mockPhone.uid.toString('hex')
);
expect(deleteFromSpy).toBeCalledWith('recoveryPhones');
expect(result).toBe(false);
});
it('should handle database errors gracefully', async () => {
jest.spyOn(db, 'insertInto').mockImplementation(() => {
throw new Error('Database error');

Просмотреть файл

@ -7,10 +7,15 @@ import {
AccountDbProvider,
} from '@fxa/shared/db/mysql/account';
import { Inject, Injectable } from '@nestjs/common';
import { registerPhoneNumber } from './recovery-phone.repository';
import {
getConfirmedPhoneNumber,
registerPhoneNumber,
removePhoneNumber,
} from './recovery-phone.repository';
import {
RecoveryNumberAlreadyExistsError,
RecoveryNumberInvalidFormatError,
RecoveryNumberNotExistsError,
} from './recovery-phone.errors';
import { Redis } from 'ioredis';
@ -64,6 +69,30 @@ export class RecoveryPhoneManager {
}
}
/**
* Get the confirmed phone number for a user.
*
* @param uid
*/
async getConfirmedPhoneNumber(uid: string): Promise<any> {
const uidBuffer = Buffer.from(uid, 'hex');
const result = await getConfirmedPhoneNumber(this.db, uidBuffer);
if (!result) {
throw new RecoveryNumberNotExistsError(uid);
}
return result;
}
/**
* Remove account phone number.
*
* @param uid
*/
async removePhoneNumber(uid: string): Promise<boolean> {
const uidBuffer = Buffer.from(uid, 'hex');
return await removePhoneNumber(this.db, uidBuffer);
}
/**
* Store phone number data and SMS code for a user.
*

Просмотреть файл

@ -4,9 +4,29 @@
import { AccountDatabase } from '@fxa/shared/db/mysql/account';
import { RecoveryPhone } from './recovery-phone.types';
export async function getConfirmedPhoneNumber(
db: AccountDatabase,
uid: Buffer
) {
return db
.selectFrom('recoveryPhones')
.where('uid', '=', uid)
.selectAll()
.executeTakeFirst();
}
export async function registerPhoneNumber(
db: AccountDatabase,
recoveryPhone: RecoveryPhone
) {
return await db.insertInto('recoveryPhones').values(recoveryPhone).execute();
}
export async function removePhoneNumber(db: AccountDatabase, uid: Buffer) {
const result = await db
.deleteFrom('recoveryPhones')
.where('uid', '=', uid)
.executeTakeFirstOrThrow();
return result.numDeletedRows === BigInt(1);
}