Chegamos a parte 3 desta série de artigos, e agora vamos construir nossa lógica de login com os tokens.
Lógica
Nessa parte de validar os tokens, pensei na seguinte lógica: recebo os tokens, valido eles, pesquiso um usuário pelos tokens enviados, verifico se os tokens ainda não foram utilizados e se tiver tudo certo, eu atualizo o campo expired para true, assim bloqueamos caso seja feita nova tentativa de logar com os mesmos tokens, e por último, retornamos um novo token, que em nosso caso, utilizaremos uma lib para retornar um jwt.
Mão na massa
A primeira coisa que vamos fazer, é definir nossas 3 dependências, sendo nossas interfaces de comunicação, e o objeto da requisição.
// src/domain/itoken-repository.js
export default class ITokenRepository {
findByToken(token) {
throw Error('Method must be implemented!');
}
updateExpiredFieldToTrue(id) {
throw Error('Method must be implemented!');
}
}
// src/domain/itoken.js
export default class IToken {
generateWebToken(user) {
throw Error('Method must be implemented!');
}
}
// src/domain/token.js
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
export default class Token {
constructor({token, emailToken}) {
if (!token || !emailToken) {
throwError(InvalidArgumentError, 'Fields token and emailToken cannot be empty!');
}
this.token = token;
this.emailToken = emailToken;
}
}
Agora que já temos nossas dependências, vamos construir nossa classe de regra de negócio:
// src/domain/token-authentication.js
import IToken from './itoken.js';
import ITokenRepository from './itoken-repository.js';
import Token from './token.js';
import isInstanceOf from './instanceof.js';
import DomainError from './domain-error.js';
import GatewayError from './gateway-error.js';
import InvalidArgumentError from './invalid-argument-error.js';
import throwError from './throw-error.js';
import User from './user.js';
export default class TokenAuthentication {
#repository;
#webToken;
constructor(repository, webToken) {
this.#validateDependencies(repository, webToken)
this.#repository = repository;
this.#webToken = webToken;
}
#validateDependencies(repository, webToken) {
if (!this.#isInstanceOf(repository, ITokenRepository)) {
throwError(DomainError, 'Invalid repository dependency');
}
if (!this.#isInstanceOf(webToken, IToken)) {
throwError(DomainError, 'Invalid token dependency');
}
}
async authenticate(token) {
this.#throwExceptionIfTokenIsInvalid(token);
const user = await this.#getUser(token);
this.#throwExceptionIfInvalidUser(user);
try {
await this.#repository.updateExpiredFieldToTrue(user.id);
return this.#webToken.generateWebToken(user);
} catch (error) {
throwError(GatewayError, 'Generic error, check the integrations!');
}
}
#throwExceptionIfInvalidUser(user) {
if (!user) {
throwError(InvalidArgumentError, 'Tokens sent are invalids. Try again!');
}
if (!isInstanceOf(user, User)) {
throwError(DomainError, 'Invalid user instance!');
}
if (user.expired) {
throwError(InvalidArgumentError, 'Token expired. Try login again!');
}
}
async #getUser(token) {
try {
return await this.#repository.findByToken(token);
} catch (e) {
throwError(GatewayError, 'Invalid database connection!');
}
}
#throwExceptionIfTokenIsInvalid(token) {
if (!this.#isInstanceOf(token, Token)) {
throwError(DomainError, 'Invalid token object!');
}
}
#isInstanceOf(object, instanceBase) {
return isInstanceOf(object, instanceBase)
}
}
Aqui não fiz nada que já não viram anteriormente, foram feitas algumas validações de instância e levantamos erros personalizados. A grande observação fica realmente sobre o campo expired, já que se ele vier como true, significa que esses tokens já foram utilizados.
Testes
E assim como fizemos no artigo anterior vamos construir nossos testes. Primeiro vamos simular nossas dependências:
// tests/unit/mocks/web-token-mock.js
import ITokenRepository from '../../../src/domain/itoken-repository.js';
import User from '../../../src/domain/user.js';
class TokenRepositoryMock extends ITokenRepository {
throwException = false;
throwExceptionUpdate = false;
throwExceptionTokenExpired = false;
returnEmpty = false;
returnEmptyObj = false;
constructor() {
super();
}
findByToken(token) {
if (this.returnEmpty) {
return '';
}
if (this.returnEmptyObj) {
return {};
}
if (this.throwException) {
throw Error();
}
const obj = {id: 1, email: 'erandir@email.com', password: '123456'};
if (this.throwExceptionTokenExpired) {
obj.expired = true;
}
let user = new User(obj);
return Promise.resolve(user);
}
updateExpiredFieldToTrue(id) {
if (this.throwExceptionUpdate) {
throw Error();
}
return Promise.resolve(true);
}
}
export default new TokenRepositoryMock();
// tests/unit/mocks/token-repository-mock.js
import IToken from '../../../src/domain/itoken.js';
class WebTokenMock extends IToken {
throwException = false;
constructor() {
super();
}
generateWebToken(user) {
if (this.throwException) {
throw Error();
}
return Promise.resolve('763a5b89-9c96-4f9b-8daa-0b411c7c671e');
}
}
export default new WebTokenMock();
Criamos nossa arquivo chamado token-authentication.test.js dentro de tests/unit/, e adicionamos o conteúdo abaixo:
import TokenAuthentication from '../../src/domain/token-authentication.js';
import TokenRepositoryMock from './mocks/token-repository-mock.js';
import WebTokenMock from './mocks/web-token-mock.js';
import Token from '../../src/domain/token.js';
const tokenAuthentication = new TokenAuthentication(TokenRepositoryMock, WebTokenMock);
const token = new Token({
token: '13eb4cb6-35dd-4536-97e6-0ed0e4fb1fb3',
emailToken: '4RV651gR93hDAGiTCYhmhh'
});
test('Invalid object repository', function () {
const result = () => new TokenAuthentication(
{},
{}
);
expect(result).toThrowError('Invalid repository dependency');
});
test('Invalid object web token', function () {
const result = () => new TokenAuthentication(
TokenRepositoryMock,
{}
);
expect(result).toThrowError('Invalid token dependency');
});
test('Invalid object token', function () {
const result = async () => await tokenAuthentication.authenticate({});
expect(result).rejects.toThrow('Invalid token object!');
});
test('Throw exception get user', function () {
TokenRepositoryMock.throwException = true;
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Invalid database connection!');
});
test('Throw exception get empty user', function () {
TokenRepositoryMock.throwException = false;
TokenRepositoryMock.returnEmpty = true;
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Tokens sent are invalids. Try again!');
});
test('Throw exception invalid user object', function () {
TokenRepositoryMock.returnEmpty = false;
TokenRepositoryMock.returnEmptyObj = true;
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Invalid user instance!');
});
test('Throw exception token expired', function () {
TokenRepositoryMock.returnEmptyObj = false;
TokenRepositoryMock.throwExceptionTokenExpired = true;
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Token expired. Try login again!');
});
test('Throw exception update user', function () {
TokenRepositoryMock.throwExceptionTokenExpired = false;
TokenRepositoryMock.throwExceptionUpdate = true;
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Generic error, check the integrations!');
});
test('Throw exception generate web token', function () {
TokenRepositoryMock.throwExceptionUpdate = false;
WebTokenMock.throwException = true
const result = async () => await tokenAuthentication.authenticate(token);
expect(result).rejects.toThrow('Generic error, check the integrations!');
});
test('Generate token', async () => {
const expected = '763a5b89-9c96-4f9b-8daa-0b411c7c671e';
WebTokenMock.throwException = false;
const result = await tokenAuthentication.authenticate(token);
expect(result).toBe(expected);
});
Se rodarmos os testes, vamos ter um novo relatório de cobertura. Se observarmos esse relatório, percebemos que nosso diretório domain, não está 100% testado, vamos aplicar os testes nas classes que encapsulam dados e também nas que simulam interfaces:
// tests/unit/dependecy.test.js
import IEmail from "./../../src/domain/iemail.js";
import IGenerateToken from '../../src/domain/igenerate-token.js';
import IPasswordHash from '../../src/domain/ipassword-hash.js';
import IRepository from '../../src/domain/irepository.js';
import ITokenRepository from '../../src/domain/itoken-repository.js';
import IToken from '../../src/domain/itoken.js';
import LoginPayload from '../../src/domain/login-payload.js';
import Token from '../../src/domain/token.js';
import User from '../../src/domain/user.js';
const email = new IEmail({});
const generateToken = new IGenerateToken({});
const passwordHash = new IPasswordHash();
const repository = new IRepository();
const tokenRepository = new ITokenRepository();
const tokenWeb = new IToken();
test('Error email not implemented', () => {
const result = () => email.send();
expect(result).toThrowError('Method must be implemented!');
});
test('Error get token not implemented', () => {
const result = () => generateToken.getToken();
expect(result).toThrowError('Method must be implemented!');
});
test('Error get email token implemented', () => {
const result = () => generateToken.getEmailToken();
expect(result).toThrowError('Method must be implemented!');
});
test('Error password compare implemented', () => {
const result = () => passwordHash.compare();
expect(result).toThrowError('Method must be implemented!');
});
test('Error find by email implemented', () => {
const result = () => repository.findByEmail();
expect(result).toThrowError('Method must be implemented!');
});
test('Error update implemented', () => {
const result = () => repository.update({});
expect(result).toThrowError('Method must be implemented!');
});
test('Error find by token implemented', () => {
const result = () => tokenRepository.findByToken({});
expect(result).toThrowError('Method must be implemented!');
});
test('Error update expire field implemented', () => {
const result = () => tokenRepository.updateExpiredFieldToTrue();
expect(result).toThrowError('Method must be implemented!');
});
test('Error generate implemented', () => {
const result = () => tokenWeb.generateWebToken();
expect(result).toThrowError('Method must be implemented!');
});
test('Error parameter not send to login payload object', () => {
const result = () => new LoginPayload();
expect(result).toThrowError('Fields email and password must be filled!');
});
test('Error parameter not send to token object', () => {
const result = () => new Token({});
expect(result).toThrowError('Fields token and emailToken cannot be empty!');
});
test('Error parameter not send to user object', () => {
const result = () => new User({});
expect(result).toThrowError('Invalid user data!');
});
Rodamos os testes novamente para garantir que tudo está validado e funcionando corretamente.
Resumo
Este artigo foi bem menor e bem mais simples que o anterior, tecnicamente validamos nossas lógicas, bastando agora realmente implementar nossas dependências, para aí sim, começarmos a utilizar esse projeto realmente.
A partir do próximo artigo, iremos instalar algumas bibliotecas, encapsular comportamentos e ter um projeto cada vez mais sólido e pronto para ser utilizado no mundo real.
Top comments (0)