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)