├── .gitignore ├── .huskyrc.json ├── .lintstagedrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── jest-integration-config.js ├── jest-unit-config.js ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json └── src ├── domain └── usecases │ └── auth-usecase.spec.js ├── presentation ├── errors │ ├── index.js │ ├── server-error.js │ └── unauthorized-error.js ├── helpers │ └── http-response.js └── routers │ ├── login-router.js │ └── login-router.spec.js └── utils ├── errors ├── index.js ├── invalid-param-error.js └── missing-param-error.js └── helpers ├── email-validator.js └── email-validator.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "pre-push": "npm run test:ci" 5 | } 6 | } -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": [ 3 | "standard --fix", 4 | "npm run test:staged" 5 | ] 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | script: 5 | - npm run test:coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 William Koller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/williamkoller/clean-node-api.svg?branch=master)](https://travis-ci.com/williamkoller/clean-node-api) 2 | [![Coverage Status](https://coveralls.io/repos/github/williamkoller/clean-node-api/badge.svg?branch=master)](https://coveralls.io/github/williamkoller/clean-node-api?branch=master) 3 | ![GitHub top language](https://img.shields.io/github/languages/top/williamkoller/clean-node-api) 4 | ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/williamkoller/clean-node-api) 5 | 6 | 7 | # Clean Architecture, Design Patterns, SOLID and JavaScript 8 | 9 | ### Methodologies and Designs 10 | * Clean Architecture 11 | * TDD 12 | * Convertional Commits 13 | * GitFlow 14 | * Use Cases -------------------------------------------------------------------------------- /jest-integration-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./jest.config') 2 | config.testMatch = ['**/*.test.js'] 3 | module.exports = config 4 | -------------------------------------------------------------------------------- /jest-unit-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./jest.config') 2 | config.testMatch = ['**/*.spec.js'] 3 | module.exports = config 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | coverageDirectory: 'coverage', 4 | testEnvironment: 'node', 5 | collectCoverageFrom: ['**/src/**/*.js'], 6 | modulePaths: [''], 7 | moduleNameMapper: { 8 | '@/(.*)': '/src/$1' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "esModuleInterop": true, 6 | "baseUrl": "src", 7 | "rootDir": "src", 8 | "paths": { 9 | "@/*": ["*"] 10 | } 11 | }, 12 | "include": ["src"], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-node-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --passWithNoTests --silent --noStackTrace --runInBand", 8 | "test:ci": "npm test -- --coverage", 9 | "test:unit": "npm test -- --watch -c jest-unit-config.js", 10 | "test:staged": "npm test -- --findRelatedTests", 11 | "test:coveralls": "npm run test:ci && coveralls < coverage/lcov.info" 12 | }, 13 | "standard": { 14 | "env": [ 15 | "jest" 16 | ] 17 | }, 18 | "author": { 19 | "name": "William Koller" 20 | }, 21 | "devDependencies": { 22 | "coveralls": "^3.1.0", 23 | "husky": "^4.2.5", 24 | "jest": "^26.1.0", 25 | "lint-staged": "^10.2.11", 26 | "standard": "^14.3.4" 27 | }, 28 | "dependencies": { 29 | "git-commit-msg-linter": "^2.4.4", 30 | "module-alias": "^2.2.2", 31 | "validator": "^13.1.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/domain/usecases/auth-usecase.spec.js: -------------------------------------------------------------------------------- 1 | const { MissingParamError } = require('@/utils/errors') 2 | 3 | class AuthUseCase { 4 | async auth (email, password) { 5 | if (!email) throw new MissingParamError('email') 6 | if (!password) throw new MissingParamError('password') 7 | } 8 | } 9 | 10 | describe('Auth UseCase', () => { 11 | test('Should throw if no email is provided', async () => { 12 | const sut = new AuthUseCase() 13 | const promise = sut.auth() 14 | expect(promise).rejects.toThrow(new MissingParamError('email')) 15 | }) 16 | 17 | test('Should throw if no password is provided', async () => { 18 | const sut = new AuthUseCase() 19 | const promise = sut.auth('any_email@mail.com') 20 | expect(promise).rejects.toThrow(new MissingParamError('password')) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/presentation/errors/index.js: -------------------------------------------------------------------------------- 1 | const ServerError = require('./server-error') 2 | const UnauthorizedError = require('./unauthorized-error') 3 | 4 | module.exports = { 5 | ServerError, 6 | UnauthorizedError 7 | } 8 | -------------------------------------------------------------------------------- /src/presentation/errors/server-error.js: -------------------------------------------------------------------------------- 1 | module.exports = class ServerError extends Error { 2 | constructor (paramName) { 3 | super('Internal error') 4 | this.name = 'ServerError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/errors/unauthorized-error.js: -------------------------------------------------------------------------------- 1 | module.exports = class UnauthorizedError extends Error { 2 | constructor (paramName) { 3 | super('Unauthorized') 4 | this.name = 'UnauthorizedError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/presentation/helpers/http-response.js: -------------------------------------------------------------------------------- 1 | const { UnauthorizedError, ServerError } = require('@/presentation/errors') 2 | 3 | module.exports = class HttpResponse { 4 | static badRequest (error) { 5 | return { 6 | statusCode: 400, 7 | body: error 8 | } 9 | } 10 | 11 | static serverError () { 12 | return { 13 | statusCode: 500, 14 | body: new ServerError() 15 | } 16 | } 17 | 18 | static unauthorizedError () { 19 | return { 20 | statusCode: 401, 21 | body: new UnauthorizedError() 22 | } 23 | } 24 | 25 | static ok (data) { 26 | return { 27 | statusCode: 200, 28 | body: data 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/presentation/routers/login-router.js: -------------------------------------------------------------------------------- 1 | const HttpResponse = require('@/presentation/helpers/http-response') 2 | const { MissingParamError, InvalidParamError } = require('@/utils/errors') 3 | 4 | module.exports = class LoginRouter { 5 | constructor (authUseCase, emailValidator) { 6 | this.authUseCase = authUseCase 7 | this.emailValidator = emailValidator 8 | } 9 | 10 | async route (httpRequest) { 11 | try { 12 | const { email, password } = httpRequest.body 13 | if (!email) { 14 | return HttpResponse.badRequest(new MissingParamError('email')) 15 | } 16 | if (!this.emailValidator.isValid(email)) { 17 | return HttpResponse.badRequest(new InvalidParamError('email')) 18 | } 19 | if (!password) { 20 | return HttpResponse.badRequest(new MissingParamError('password')) 21 | } 22 | const accessToken = await this.authUseCase.auth(email, password) 23 | if (!accessToken) return HttpResponse.unauthorizedError() 24 | return HttpResponse.ok({ accessToken }) 25 | } catch (error) { 26 | return HttpResponse.serverError() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/presentation/routers/login-router.spec.js: -------------------------------------------------------------------------------- 1 | const LoginRouter = require('./login-router') 2 | const { MissingParamError, InvalidParamError } = require('@/utils/errors') 3 | const { ServerError, UnauthorizedError } = require('@/presentation/errors') 4 | 5 | // Design Pattern Factory 6 | const makeSut = () => { 7 | // Design Pattern Dependecy Injection 8 | const authUseCaseSpy = makeAuthUseCase() 9 | const emailValidatorSpy = makeEmailValidator() 10 | const sut = new LoginRouter(authUseCaseSpy, emailValidatorSpy) 11 | return { 12 | sut, 13 | authUseCaseSpy, 14 | emailValidatorSpy 15 | } 16 | } 17 | 18 | const makeEmailValidator = () => { 19 | class EmailValidatorSpy { 20 | isValid (email) { 21 | this.email = email 22 | return this.isEmailValid 23 | } 24 | } 25 | const emailValidatorSpy = new EmailValidatorSpy() 26 | emailValidatorSpy.isEmailValid = true 27 | return emailValidatorSpy 28 | } 29 | 30 | const makeEmailValidatorWithError = () => { 31 | class EmailValidatorSpy { 32 | isValid () { 33 | throw new Error() 34 | } 35 | } 36 | return new EmailValidatorSpy() 37 | } 38 | 39 | const makeAuthUseCase = () => { 40 | class AuthUseCaseSpy { 41 | async auth (email, password) { 42 | this.email = email 43 | this.password = password 44 | return this.accessToken 45 | } 46 | } 47 | const authUseCaseSpy = new AuthUseCaseSpy() 48 | authUseCaseSpy.accessToken = 'valid_token' 49 | return authUseCaseSpy 50 | } 51 | 52 | const makeAuhtUseCaseWithError = () => { 53 | class AuthUseCaseSpy { 54 | async auth () { 55 | throw new Error() 56 | } 57 | } 58 | return new AuthUseCaseSpy() 59 | } 60 | 61 | describe('Login Router', () => { 62 | test('Should return 400 if no email is provided', async () => { 63 | const { sut } = makeSut() 64 | const httpRequest = { 65 | body: { 66 | password: 'any_password' 67 | } 68 | } 69 | const httpResponse = await sut.route(httpRequest) 70 | expect(httpResponse.statusCode).toBe(400) 71 | expect(httpResponse.body).toEqual(new MissingParamError('email')) 72 | }) 73 | 74 | test('Should return 400 if no password is provided', async () => { 75 | const { sut } = makeSut() 76 | const httpRequest = { 77 | body: { 78 | email: 'any_email@mail.com' 79 | } 80 | } 81 | const httpResponse = await sut.route(httpRequest) 82 | expect(httpResponse.statusCode).toBe(400) 83 | expect(httpResponse.body).toEqual(new MissingParamError('password')) 84 | }) 85 | 86 | test('Should return 500 if no httpRequest is provided', async () => { 87 | const { sut } = makeSut() 88 | const httpResponse = await sut.route() 89 | expect(httpResponse.statusCode).toBe(500) 90 | expect(httpResponse.body).toEqual(new ServerError()) 91 | }) 92 | 93 | test('Should return 500 if no httpRequest has no body', async () => { 94 | const { sut } = makeSut() 95 | const httpResponse = await sut.route({}) 96 | expect(httpResponse.statusCode).toBe(500) 97 | expect(httpResponse.body).toEqual(new ServerError()) 98 | }) 99 | 100 | // testando a integração dos componentes 101 | test('Should call AuthUseCase with correct params', async () => { 102 | const { sut, authUseCaseSpy } = makeSut() 103 | const httpRequest = { 104 | body: { 105 | email: 'any_email@mail.com', 106 | password: 'any_password' 107 | } 108 | } 109 | await sut.route(httpRequest) 110 | expect(authUseCaseSpy.email).toBe(httpRequest.body.email) 111 | expect(authUseCaseSpy.password).toBe(httpRequest.body.password) 112 | }) 113 | 114 | test('Should return 401 when invalid credentials are provided', async () => { 115 | const { sut, authUseCaseSpy } = makeSut() 116 | authUseCaseSpy.accessToken = null 117 | const httpRequest = { 118 | body: { 119 | email: 'invalid_email@mail.com', 120 | password: 'invalid_password' 121 | } 122 | } 123 | const httpResponse = await sut.route(httpRequest) 124 | expect(httpResponse.statusCode).toBe(401) 125 | expect(httpResponse.body).toEqual(new UnauthorizedError()) 126 | }) 127 | 128 | test('Should return 200 when valid credentials are provided', async () => { 129 | const { sut, authUseCaseSpy } = makeSut() 130 | const httpRequest = { 131 | body: { 132 | email: 'valid_email@mail.com', 133 | password: 'valid_password' 134 | } 135 | } 136 | const httpResponse = await sut.route(httpRequest) 137 | expect(httpResponse.statusCode).toBe(200) 138 | expect(httpResponse.body.accessToken).toEqual(authUseCaseSpy.accessToken) 139 | }) 140 | 141 | test('Should return 500 if no AuthUseCase is provided', async () => { 142 | const sut = new LoginRouter() 143 | const httpRequest = { 144 | body: { 145 | email: 'any_email@mail.com', 146 | password: 'any_password' 147 | } 148 | } 149 | const httpResponse = await sut.route(httpRequest) 150 | expect(httpResponse.statusCode).toBe(500) 151 | expect(httpResponse.body).toEqual(new ServerError()) 152 | }) 153 | 154 | test('Should return 500 if no AuthUseCase has no auth method', async () => { 155 | const sut = new LoginRouter({}) 156 | const httpRequest = { 157 | body: { 158 | email: 'any_email@mail.com', 159 | password: 'any_password' 160 | } 161 | } 162 | const httpResponse = await sut.route(httpRequest) 163 | expect(httpResponse.statusCode).toBe(500) 164 | expect(httpResponse.body).toEqual(new ServerError()) 165 | }) 166 | 167 | test('Should return 500 if no AuthUseCase throws', async () => { 168 | const authUseCaseSpy = makeAuhtUseCaseWithError() 169 | const sut = new LoginRouter(authUseCaseSpy) 170 | const httpRequest = { 171 | body: { 172 | email: 'any_email@mail.com', 173 | password: 'any_password' 174 | } 175 | } 176 | const httpResponse = await sut.route(httpRequest) 177 | expect(httpResponse.statusCode).toBe(500) 178 | }) 179 | 180 | test('Should return 400 if an invaid email is provided', async () => { 181 | const { sut, emailValidatorSpy } = makeSut() 182 | emailValidatorSpy.isEmailValid = false 183 | const httpRequest = { 184 | body: { 185 | email: 'invalid_email@mail.com', 186 | password: 'any_password' 187 | } 188 | } 189 | const httpResponse = await sut.route(httpRequest) 190 | expect(httpResponse.statusCode).toBe(400) 191 | expect(httpResponse.body).toEqual(new InvalidParamError('email')) 192 | }) 193 | 194 | test('Should return 500 if no EmailValidator is provided', async () => { 195 | const authUseCaseSpy = makeAuthUseCase() 196 | const sut = new LoginRouter(authUseCaseSpy) 197 | const httpRequest = { 198 | body: { 199 | email: 'any_email@mail.com', 200 | password: 'any_password' 201 | } 202 | } 203 | const httpResponse = await sut.route(httpRequest) 204 | expect(httpResponse.statusCode).toBe(500) 205 | expect(httpResponse.body).toEqual(new ServerError()) 206 | }) 207 | 208 | test('Should return 500 if no EmailValidator has no isValid method', async () => { 209 | const authUseCaseSpy = makeAuthUseCase() 210 | const sut = new LoginRouter(authUseCaseSpy, {}) 211 | const httpRequest = { 212 | body: { 213 | email: 'any_email@mail.com', 214 | password: 'any_password' 215 | } 216 | } 217 | const httpResponse = await sut.route(httpRequest) 218 | expect(httpResponse.statusCode).toBe(500) 219 | expect(httpResponse.body).toEqual(new ServerError()) 220 | }) 221 | 222 | test('Should return 500 if EmailValidator throws', async () => { 223 | const authUseCaseSpy = makeAuthUseCase() 224 | const emailValidatorSpy = makeEmailValidatorWithError() 225 | const sut = new LoginRouter(authUseCaseSpy, emailValidatorSpy) 226 | const httpRequest = { 227 | body: { 228 | email: 'any_email@mail.com', 229 | password: 'any_password' 230 | } 231 | } 232 | const httpResponse = await sut.route(httpRequest) 233 | expect(httpResponse.statusCode).toBe(500) 234 | expect(httpResponse.body).toEqual(new ServerError()) 235 | }) 236 | 237 | test('Should call EmailValidator with correct email', async () => { 238 | const { sut, emailValidatorSpy } = makeSut() 239 | const httpRequest = { 240 | body: { 241 | email: 'any_email@mail.com', 242 | password: 'any_password' 243 | } 244 | } 245 | await sut.route(httpRequest) 246 | expect(emailValidatorSpy.email).toBe(httpRequest.body.email) 247 | }) 248 | }) 249 | -------------------------------------------------------------------------------- /src/utils/errors/index.js: -------------------------------------------------------------------------------- 1 | const MissingParamError = require('./missing-param-error') 2 | const InvalidParamError = require('./invalid-param-error') 3 | 4 | module.exports = { 5 | MissingParamError, 6 | InvalidParamError 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/errors/invalid-param-error.js: -------------------------------------------------------------------------------- 1 | module.exports = class InvalidParamError extends Error { 2 | constructor (paramName) { 3 | super(`Invalid param: ${paramName}`) 4 | this.name = 'InvalidParamError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/errors/missing-param-error.js: -------------------------------------------------------------------------------- 1 | module.exports = class MissingParamError extends Error { 2 | constructor (paramName) { 3 | super(`Missing param: ${paramName}`) 4 | this.name = 'MissingParamError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/helpers/email-validator.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator') 2 | const MissingParamError = require('../errors/missing-param-error') 3 | 4 | module.exports = class EmailValidator { 5 | isValid (email) { 6 | if (!email) { 7 | throw new MissingParamError('email') 8 | } 9 | return validator.isEmail(email) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/helpers/email-validator.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('validator', () => ({ 2 | isEmailValid: true, 3 | isEmail (email) { 4 | this.email = email 5 | return this.isEmailValid 6 | } 7 | })) 8 | 9 | const validator = require('validator') 10 | const MissingParamError = require('../errors/missing-param-error') 11 | const EmailValidator = require('./email-validator') 12 | 13 | const makeSut = () => { 14 | return new EmailValidator() 15 | } 16 | 17 | describe('Email Validator', () => { 18 | test('Should return true if validator returns true', () => { 19 | const sut = makeSut() 20 | const isEmailValid = sut.isValid('valid_email@mail.com') 21 | expect(isEmailValid).toBe(true) 22 | }) 23 | 24 | test('Should return false if validator returns false', () => { 25 | validator.isEmailValid = false 26 | const sut = makeSut() 27 | const isEmailValid = sut.isValid('invalid_email@mail.com') 28 | expect(isEmailValid).toBe(false) 29 | }) 30 | 31 | test('Should call validator with correct email', () => { 32 | const sut = makeSut() 33 | sut.isValid('any_email@mail.com') 34 | expect(validator.email).toBe('any_email@mail.com') 35 | }) 36 | 37 | test('Should throw if no email is provided', async () => { 38 | const sut = makeSut() 39 | expect(() => { sut.isValid() }).toThrow(new MissingParamError('email')) 40 | }) 41 | }) 42 | --------------------------------------------------------------------------------