DEV Community

Erandir Junior
Erandir Junior

Posted on

4 1

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.

Image of Datadog

The Future of AI, LLMs, and Observability on Google Cloud

Datadog sat down with Google’s Director of AI to discuss the current and future states of AI, ML, and LLMs on Google Cloud. Discover 7 key insights for technical leaders, covering everything from upskilling teams to observability best practices

Learn More

Top comments (0)