DEV Community

Erandir Junior
Erandir Junior

Posted on

Construindo aplicação do zero com node.js: Parte 3

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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!');
});
Enter fullscreen mode Exit fullscreen mode

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)