Skip to content

Commit

Permalink
feat: add logic to send confirmation code
Browse files Browse the repository at this point in the history
  • Loading branch information
hdev14 committed Feb 11, 2024
1 parent e8ded7c commit e661d29
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 19 deletions.
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NODE_ENV=test

SERVER_PORT=5000
SERVER_URL=http://localhost:5000

DB_HOST=localhost
DB_PORT=5432
Expand Down
1 change: 1 addition & 0 deletions src/b3_stock_alerts/AlertService.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('AlertService', () => {
getUsers: jest.fn(),
updateUser: jest.fn(),
getUserByEmail: jest.fn(),
createConfirmationCode: jest.fn(),
};

const service = new AlertService(alert_repository_mock, user_repository_mock);
Expand Down
24 changes: 24 additions & 0 deletions src/b3_stock_alerts/AuthService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import CredentialError from '@shared/CredentialError';
import NotFoundError from '@shared/NotFoundError';
import { Result } from '@shared/generic_types';
import { randomInt, randomUUID } from 'crypto';
import Authenticator from './Authenticator';
import ConfirmationCode from './ConfirmationCode';
import Encryptor from './Encryptor';
import { User } from './User';
import UserRepository from './UserRepository';
Expand All @@ -16,6 +19,7 @@ export default class AuthService {
private readonly user_repository: UserRepository,
private readonly encryptor: Encryptor,
private readonly authenticator: Authenticator,
private readonly confirmation_code: ConfirmationCode,
) { }

async login(email: string, password: string): Promise<Result<LoginResult>> {
Expand Down Expand Up @@ -43,4 +47,24 @@ export default class AuthService {
async verifyCaptcha(user_ip: string, token: string): Promise<Result<boolean>> {
return { data: await this.authenticator.verifyCaptcha(user_ip, token) };
}

async sendConfirmationCode(email: string): Promise<Result<void>> {
const user = await this.user_repository.getUserByEmail(email);

if (!user) {
return { error: new NotFoundError('Usuário não encontrado.') };
}

const code = randomInt(1000, 9999).toString();

await this.confirmation_code.sendCode({ email, code });

await this.user_repository.createConfirmationCode({
id: randomUUID(),
user_id: user.id,
code,
});

return {};
}
}
79 changes: 79 additions & 0 deletions src/b3_stock_alerts/AuthService.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { faker } from '@faker-js/faker/locale/pt_BR';
import CredentialError from '@shared/CredentialError';
import NotFoundError from '@shared/NotFoundError';
import crypto from 'crypto';
import AuthService from './AuthService';
import { User } from './User';

Expand All @@ -11,6 +13,7 @@ describe("AuthService's unit tests", () => {
updateUser: jest.fn(),
getUsers: jest.fn(),
getUserByEmail: jest.fn(),
createConfirmationCode: jest.fn(),
};

const encryptor_mock = {
Expand All @@ -25,10 +28,15 @@ describe("AuthService's unit tests", () => {
verifyCaptcha: jest.fn(),
};

const confirmation_code_mock = {
sendCode: jest.fn(),
}

const auth_service = new AuthService(
user_repository_mock,
encryptor_mock,
authenticator_mock,
confirmation_code_mock,
);

describe('AuthService.login', () => {
Expand Down Expand Up @@ -97,4 +105,75 @@ describe("AuthService's unit tests", () => {
}
});
});

describe('AuthService.sendConfirmationCode', () => {
const random_int_spy = jest.spyOn(crypto, 'randomInt');

afterEach(() => {
random_int_spy.mockClear();
user_repository_mock.createConfirmationCode.mockClear();
confirmation_code_mock.sendCode.mockClear();
});

it("return result with NotFoundError if user doesn't exist", async () => {
expect.assertions(1);

const email = faker.internet.email();

user_repository_mock.getUserByEmail.mockResolvedValueOnce(null);

const result = await auth_service.sendConfirmationCode(email);

expect(result.error).toBeInstanceOf(NotFoundError);
});

it('calls ConfirmationCode.sendCode with correct code and email', async () => {
expect.assertions(3);

const email = faker.internet.email();

const user: User = {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.string.alphanumeric(10),
phone_number: faker.string.numeric(11),
};

user_repository_mock.getUserByEmail.mockResolvedValueOnce(user);

await auth_service.sendConfirmationCode(email);

expect(random_int_spy).toHaveBeenCalledWith(1000, 9999);
const call = confirmation_code_mock.sendCode.mock.calls[0];

expect(call[0].email).toEqual(email);
expect(call[0].code).toBeDefined();
});

it('saves the code after send it for user email', async () => {
expect.assertions(3);

const email = faker.internet.email();

const user: User = {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.string.alphanumeric(10),
phone_number: faker.string.numeric(11),
};

random_int_spy.mockReturnValueOnce(1234 as any);
user_repository_mock.getUserByEmail.mockResolvedValueOnce(user);

await auth_service.sendConfirmationCode(email);

const params = user_repository_mock.createConfirmationCode.mock.calls[0][0];

expect(params.id).toBeDefined();
expect(params.user_id).toBe(user.id);
expect(params.code).toBe('1234');
});
});
});
10 changes: 10 additions & 0 deletions src/b3_stock_alerts/ConfirmationCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type SendCodeParams = {
email: string;
code: string;
};

interface ConfirmationCode {
sendCode(params: SendCodeParams): Promise<void>;
}

export default ConfirmationCode;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import nodemailer, { Transporter } from 'nodemailer';
import AlertNotification, { AlertNotificationTypes, NotificationData } from './AlertNotification';
import ConfirmationCode, { SendCodeParams } from './ConfirmationCode';

export default class EmailAlertNotification implements AlertNotification {
export default class EmailGateway implements AlertNotification, ConfirmationCode {
private readonly transporter: Transporter;

constructor() {
Expand All @@ -18,7 +19,7 @@ export default class EmailAlertNotification implements AlertNotification {

async notify(data: NotificationData): Promise<void> {
const message = {
from: 'test@server.com',
from: process.env.APPLICATION_EMAIL,
to: data.user.email,
subject: '',
text: '',
Expand All @@ -37,4 +38,16 @@ export default class EmailAlertNotification implements AlertNotification {

await this.transporter.sendMail(message);
}

async sendCode(params: SendCodeParams): Promise<void> {
const message = {
from: process.env.APPLICATION_EMAIL,
to: params.email,
subject: 'Código de confirmação',
text: `Segue o código de confirmação ${params.code}. Acesse o link ${process.env.SERVER_URL}/pages/confirm-code?email=${params.email}.`,
html: `<p>Segue o código de confirmação ${params.code}.</p><br/><p>Acesse o link ${process.env.SERVER_URL}/pages/confirm-code?email=${params.email}.</p>`,
};

await this.transporter.sendMail(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
import { faker } from '@faker-js/faker/locale/pt_BR';
import nodemailer from 'nodemailer';
import { AlertNotificationTypes, NotificationData } from './AlertNotification';
import EmailAlertNotification from './EmailAlertNotification';
import EmailGateway from './EmailGateway';

jest.mock('nodemailer');

const nodemailer_mock = jest.mocked(nodemailer);
describe('EmailAlertNotification', () => {

describe("EmailGateway's unit tests", () => {
const OLD_ENV = process.env;
const transport_mock = { sendMail: jest.fn() } as any;
nodemailer_mock.createTransport.mockReturnValue(transport_mock);
const email_gateway = new EmailGateway();

afterEach(() => {
transport_mock.sendMail.mockClear();
});

beforeAll(() => {
process.env = {
Expand All @@ -17,22 +25,15 @@ describe('EmailAlertNotification', () => {
EMAIL_PORT: '123',
EMAIL_USER: 'test',
EMAIL_PASSWORD: 'test',
SERVER_URL: 'http://localhost:5000',
};
});

afterAll(() => {
process.env = OLD_ENV;
});

describe('EmailAlertNotification.notify', () => {
const transport_mock = { sendMail: jest.fn() } as any;
nodemailer_mock.createTransport.mockReturnValue(transport_mock);
const alert_notification = new EmailAlertNotification();

afterEach(() => {
transport_mock.sendMail.mockClear();
});

describe('EmailGateway.notify', () => {
it('sends an email notification for MAX stock alert', async () => {
expect.assertions(1);

Expand All @@ -49,7 +50,7 @@ describe('EmailAlertNotification', () => {
type: AlertNotificationTypes.MAX,
};

await alert_notification.notify(data);
await email_gateway.notify(data);

expect(transport_mock.sendMail).toHaveBeenCalledWith({
from: 'test@server.com',
Expand All @@ -76,7 +77,7 @@ describe('EmailAlertNotification', () => {
type: AlertNotificationTypes.MIN,
};

await alert_notification.notify(data);
await email_gateway.notify(data);

expect(transport_mock.sendMail).toHaveBeenCalledWith({
from: 'test@server.com',
Expand All @@ -87,4 +88,23 @@ describe('EmailAlertNotification', () => {
});
});
});

describe('EmailGateway.notify', () => {
it('sends an email for code confirmation', async () => {
expect.assertions(1);

const email = faker.internet.email();
const code = faker.string.alphanumeric()

await email_gateway.sendCode({ email, code });

expect(transport_mock.sendMail).toHaveBeenCalledWith({
from: 'test@server.com',
to: email,
subject: 'Código de confirmação',
text: `Segue o código de confirmação ${code}. Acesse o link http://localhost:5000/pages/confirm-code?email=${email}.`,
html: `<p>Segue o código de confirmação ${code}.</p><br/><p>Acesse o link http://localhost:5000/pages/confirm-code?email=${email}.</p>`,
});
});
});
});
8 changes: 8 additions & 0 deletions src/b3_stock_alerts/PgUserRepository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Postgres from '@shared/Postgres';
import { Client } from 'pg';
import { User } from './User';
import { UserConfirmationCode } from './UserConfirmationCode';
import UserRepository from './UserRepository';

export default class PgUserRepository implements UserRepository {
Expand Down Expand Up @@ -75,4 +76,11 @@ export default class PgUserRepository implements UserRepository {

return result.rows[0];
}

async createConfirmationCode(confirmation_code: UserConfirmationCode): Promise<void> {
await this.client.query(
'INSERT INTO user_confirmation_codes (id, user_id, code) VALUES ($1, $2, $3)',
[confirmation_code.id, confirmation_code.user_id, confirmation_code.code],
);
}
}
19 changes: 19 additions & 0 deletions src/b3_stock_alerts/PgUserRepository.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { faker } from '@faker-js/faker/locale/pt_BR';
import Postgres from '@shared/Postgres';
import PgUserRepository from './PgUserRepository';
import { User } from './User';
import { UserConfirmationCode } from './UserConfirmationCode';

const get_client_spy = jest.spyOn(Postgres, 'getClient');

Expand Down Expand Up @@ -168,4 +169,22 @@ describe('PgUserRepository', () => {
expect(result).toBeNull();
});
});

describe('PgUserRepository.createConfirmationCode', () => {
it('inserts a new confirmation code', async () => {
expect.assertions(1);

const confirmation_code: UserConfirmationCode = {
id: faker.string.uuid(),
user_id: faker.string.uuid(),
code: faker.string.numeric(4),
};

await repository.createConfirmationCode(confirmation_code);

expect(query_mock).toHaveBeenCalledWith('INSERT INTO user_confirmation_codes (id, user_id, code) VALUES ($1, $2, $3)', [
confirmation_code.id, confirmation_code.user_id, confirmation_code.code,
]);
});
});
});
1 change: 1 addition & 0 deletions src/b3_stock_alerts/StockEventHandler.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("StockEventHandler's unit tests", () => {
updateUser: jest.fn(),
getUsers: jest.fn(),
getUserByEmail: jest.fn(),
createConfirmationCode: jest.fn(),
};

const handler = new StockEventHandler(alert_notification_mock, user_repository_mock);
Expand Down
5 changes: 5 additions & 0 deletions src/b3_stock_alerts/UserConfirmationCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type UserConfirmationCode = {
id: string;
user_id: string;
code: string;
}
2 changes: 2 additions & 0 deletions src/b3_stock_alerts/UserRepository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { User } from './User';
import { UserConfirmationCode } from './UserConfirmationCode';

export default interface UserRepository {
getUsers(): Promise<Array<User>>;
Expand All @@ -7,4 +8,5 @@ export default interface UserRepository {
updateUser(user: User): Promise<void>;
deleteUser(user_id: string): Promise<void>;
getUserByEmail(email: string): Promise<User | null>;
createConfirmationCode(confirmation_code: UserConfirmationCode): Promise<void>;
}
1 change: 1 addition & 0 deletions src/b3_stock_alerts/UserService.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe("UserService's unit tests", () => {
updateUser: jest.fn(),
getUsers: jest.fn(),
getUserByEmail: jest.fn(),
createConfirmationCode: jest.fn(),
};

const encryptor_mock = {
Expand Down
Loading

0 comments on commit e661d29

Please sign in to comment.