├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── config │ └── typeorm.ts ├── currencies │ ├── currencies.controller.spec.ts │ ├── currencies.controller.ts │ ├── currencies.entity.ts │ ├── currencies.module.ts │ ├── currencies.repository.spec.ts │ ├── currencies.repository.ts │ ├── currencies.service.spec.ts │ ├── currencies.service.ts │ └── dto │ │ └── create-currency.dto.ts ├── exchange │ ├── dto │ │ └── exchange-input.dto.ts │ ├── exchange.controller.spec.ts │ ├── exchange.controller.ts │ ├── exchange.module.ts │ ├── exchange.service.spec.ts │ ├── exchange.service.ts │ └── types │ │ └── exchange.type.ts └── main.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /volumes 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "trailingComma": "all", 6 | "semi": true, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.4-slim 2 | RUN apt-get update && apt-get install -y git python-minimal make gcc g++ 3 | RUN rm -rf /var/lib/apt/lists/* 4 | 5 | WORKDIR /app 6 | COPY . /app 7 | 8 | RUN yarn 9 | EXPOSE 3000 10 | 11 | CMD bash -c "yarn start" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hugouke 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 | ## Description 2 | 3 | API para conversão de moedas desenolvido utilizando a técnica de TDD com as tegnologias: NestJS, Typescript, Mongodb. Acompanhe o processo de desenvolvimento no [youtube](https://www.youtube.com/watch?v=aXwEa60czpg&list=PLVtiMM37bm3bEVcWVVjpZfMVQimRZBwRh&index=2). 4 | 5 | ## Running the app 6 | 7 | ```bash 8 | $ docker-compose up 9 | # listening in http://localhost 10 | ``` 11 | 12 | ## Test 13 | 14 | ```bash 15 | # unit tests 16 | $ npm run test 17 | 18 | # test coverage 19 | $ npm run test:cov 20 | ``` 21 | 22 | 23 |
24 | 25 | ## Endpoints 26 | 27 | ### Currency exchange 28 | 29 | ```bash 30 | GET 31 | http://localhost/exchange/?from=USD&to=BRL&amount=1 32 | ``` 33 | 34 | 35 | 36 | ### Create Currency 37 | 38 | ```bash 39 | POST 40 | http://localhost/currencies/ 41 | Put values in body: 42 | currency=BRL 43 | value=0.2 44 | ``` 45 | 46 | 47 | 48 | ### Update Currency Value 49 | 50 | ```bash 51 | PATCH 52 | http://localhost/currencies/BRL/value 53 | Put value in body: 54 | value=0.22 55 | ``` 56 | 57 | 58 | 59 | ### Delete Currency 60 | 61 | ```bash 62 | DELETE 63 | http://localhost/currencies/BRL 64 | ``` 65 | 66 | 67 | 68 |
69 | 70 | ## License 71 | 72 | Nest is [MIT licensed](LICENSE). 73 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api: 5 | image: exchange-api 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | container_name: exchange-api 10 | command: bash -c "yarn && yarn start" 11 | restart: always 12 | volumes: 13 | - .:/app 14 | working_dir: '/app' 15 | ports: 16 | - '80:3000' 17 | networks: 18 | exchange-network: 19 | ipv4_address: 1.0.0.2 20 | 21 | mongo: 22 | image: mongo:3.4 23 | container_name: mongodb 24 | restart: always 25 | volumes: 26 | - ./volumes/mongo:/data/db 27 | networks: 28 | exchange-network: 29 | ipv4_address: 1.0.0.3 30 | 31 | networks: 32 | exchange-network: 33 | driver: bridge 34 | ipam: 35 | config: 36 | - subnet: 1.0.0.0/24 37 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdd-exchange-api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.0.0", 25 | "@nestjs/core": "^7.0.0", 26 | "@nestjs/platform-express": "^7.0.0", 27 | "@nestjs/typeorm": "^7.1.4", 28 | "class-transformer": "^0.3.1", 29 | "class-validator": "^0.12.2", 30 | "mongodb": "^3.6.2", 31 | "reflect-metadata": "^0.1.13", 32 | "rimraf": "^3.0.2", 33 | "rxjs": "^6.5.4", 34 | "typeorm": "^0.2.28" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^7.0.0", 38 | "@nestjs/schematics": "^7.0.0", 39 | "@nestjs/testing": "^7.0.0", 40 | "@types/express": "^4.17.3", 41 | "@types/jest": "26.0.10", 42 | "@types/node": "^13.9.1", 43 | "@types/supertest": "^2.0.8", 44 | "@typescript-eslint/eslint-plugin": "3.9.1", 45 | "@typescript-eslint/parser": "3.9.1", 46 | "eslint": "7.7.0", 47 | "eslint-config-prettier": "^6.10.0", 48 | "eslint-plugin-import": "^2.20.1", 49 | "jest": "26.4.2", 50 | "prettier": "^1.19.1", 51 | "supertest": "^4.0.2", 52 | "ts-jest": "26.2.0", 53 | "ts-loader": "^6.2.1", 54 | "ts-node": "9.0.0", 55 | "tsconfig-paths": "^3.9.0", 56 | "typescript": "^3.7.4" 57 | }, 58 | "jest": { 59 | "moduleFileExtensions": [ 60 | "js", 61 | "json", 62 | "ts" 63 | ], 64 | "rootDir": "src", 65 | "testRegex": ".spec.ts$", 66 | "transform": { 67 | "^.+\\.(t|j)s$": "ts-jest" 68 | }, 69 | "coverageDirectory": "../coverage", 70 | "testEnvironment": "node" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ExchangeModule } from './exchange/exchange.module'; 3 | import { CurrenciesModule } from './currencies/currencies.module'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TypeOrmConfig } from './config/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forRoot(TypeOrmConfig), ExchangeModule, CurrenciesModule], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /src/config/typeorm.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | import { Currencies } from 'src/currencies/currencies.entity'; 3 | 4 | export const TypeOrmConfig: TypeOrmModuleOptions = { 5 | type: 'mongodb', 6 | url: 'mongodb://1.0.0.3/exchange', 7 | entities: [Currencies], 8 | synchronize: true, 9 | autoLoadEntities: true, 10 | useUnifiedTopology: true, 11 | }; 12 | -------------------------------------------------------------------------------- /src/currencies/currencies.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { CurrenciesController } from './currencies.controller'; 4 | import { Currencies } from './currencies.entity'; 5 | import { CurrenciesService } from './currencies.service'; 6 | 7 | describe('CurrenciesController', () => { 8 | let controller: CurrenciesController; 9 | let service: CurrenciesService; 10 | let mockData; 11 | 12 | beforeEach(async () => { 13 | const mockService = { 14 | getCurrency: jest.fn(), 15 | createCurrency: jest.fn(), 16 | deleteCurrency: jest.fn(), 17 | updateCurrency: jest.fn(), 18 | }; 19 | const module: TestingModule = await Test.createTestingModule({ 20 | controllers: [CurrenciesController], 21 | providers: [ 22 | { 23 | provide: CurrenciesService, 24 | useFactory: () => mockService, 25 | }, 26 | ], 27 | }).compile(); 28 | 29 | controller = module.get(CurrenciesController); 30 | service = module.get(CurrenciesService); 31 | mockData = { currency: 'USD', value: 1 } as Currencies; 32 | }); 33 | 34 | it('should be defined', () => { 35 | expect(controller).toBeDefined(); 36 | }); 37 | 38 | describe('getCurrency()', () => { 39 | it('should be throw when service throw', async () => { 40 | (service.getCurrency as jest.Mock).mockRejectedValue(new BadRequestException()); 41 | await expect(controller.getCurrency('INVALID')).rejects.toThrow(new BadRequestException()); 42 | }); 43 | 44 | it('should be called service with corrects params', async () => { 45 | await controller.getCurrency('USD'); 46 | expect(service.getCurrency).toBeCalledWith('USD'); 47 | }); 48 | 49 | it('should be returns when service returns', async () => { 50 | (service.getCurrency as jest.Mock).mockReturnValue(mockData); 51 | expect(await controller.getCurrency('USD')).toEqual(mockData); 52 | }); 53 | }); 54 | 55 | describe('createCurrency()', () => { 56 | it('should be throw when service throw', async () => { 57 | (service.createCurrency as jest.Mock).mockRejectedValue(new BadRequestException()); 58 | await expect(controller.createCurrency(mockData)).rejects.toThrow(new BadRequestException()); 59 | }); 60 | 61 | it('should be called service with corrects params', async () => { 62 | await controller.createCurrency(mockData); 63 | expect(service.createCurrency).toBeCalledWith(mockData); 64 | }); 65 | 66 | it('should be returns when service returns', async () => { 67 | (service.createCurrency as jest.Mock).mockReturnValue(mockData); 68 | expect(await controller.createCurrency(mockData)).toEqual(mockData); 69 | }); 70 | }); 71 | 72 | describe('deleteCurrency()', () => { 73 | it('should be throw when service throw', async () => { 74 | (service.deleteCurrency as jest.Mock).mockRejectedValue(new BadRequestException()); 75 | await expect(controller.deleteCurrency('INVALID')).rejects.toThrow(new BadRequestException()); 76 | }); 77 | 78 | it('should be called service with corrects params', async () => { 79 | await controller.deleteCurrency('USD'); 80 | expect(service.deleteCurrency).toBeCalledWith('USD'); 81 | }); 82 | }); 83 | 84 | describe('updateCurrency()', () => { 85 | it('should be throw when service throw', async () => { 86 | (service.updateCurrency as jest.Mock).mockRejectedValue(new BadRequestException()); 87 | await expect(controller.updateCurrency('USD', 1)).rejects.toThrow(new BadRequestException()); 88 | }); 89 | 90 | it('should be called service with corrects params', async () => { 91 | await controller.updateCurrency('USD', 1); 92 | expect(service.updateCurrency).toBeCalledWith(mockData); 93 | }); 94 | 95 | it('should be returns when service returns', async () => { 96 | (service.updateCurrency as jest.Mock).mockReturnValue(mockData); 97 | expect(await controller.updateCurrency('USD', 1)).toEqual(mockData); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/currencies/currencies.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Delete, 6 | Get, 7 | Param, 8 | Patch, 9 | Post, 10 | UsePipes, 11 | ValidationPipe, 12 | } from '@nestjs/common'; 13 | import { Currencies } from './currencies.entity'; 14 | import { CurrenciesService } from './currencies.service'; 15 | import { CreateCurrencyDto } from './dto/create-currency.dto'; 16 | 17 | @Controller('currencies') 18 | export class CurrenciesController { 19 | constructor(private currenciesService: CurrenciesService) {} 20 | 21 | @Get('/:currency') 22 | async getCurrency(@Param('currency') currency: string): Promise { 23 | return await this.currenciesService.getCurrency(currency); 24 | } 25 | 26 | @Post() 27 | @UsePipes(ValidationPipe) 28 | async createCurrency(@Body() createCurrencyDto: CreateCurrencyDto): Promise { 29 | return await this.currenciesService.createCurrency(createCurrencyDto); 30 | } 31 | 32 | @Delete('/:currency') 33 | async deleteCurrency(@Param('currency') currency: string): Promise { 34 | return await this.currenciesService.deleteCurrency(currency); 35 | } 36 | 37 | @Patch('/:currency/value') 38 | async updateCurrency(@Param('currency') currency: string, @Body('value') value: number) { 39 | return await this.currenciesService.updateCurrency({ currency, value }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/currencies/currencies.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, Length } from 'class-validator'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | ObjectIdColumn, 7 | PrimaryColumn, 8 | Unique, 9 | UpdateDateColumn, 10 | } from 'typeorm'; 11 | 12 | @Unique(['currency']) 13 | @Entity() 14 | export class Currencies { 15 | @ObjectIdColumn() 16 | _id: string; 17 | 18 | @PrimaryColumn() 19 | @Length(3, 3) 20 | @IsNotEmpty() 21 | currency: string; 22 | 23 | @Column() 24 | @IsNotEmpty() 25 | value: number; 26 | 27 | @CreateDateColumn({ type: 'timestamp' }) 28 | createAt: Date; 29 | 30 | @UpdateDateColumn({ type: 'timestamp' }) 31 | updateAt: Date; 32 | } 33 | -------------------------------------------------------------------------------- /src/currencies/currencies.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CurrenciesService } from './currencies.service'; 3 | import { CurrenciesController } from './currencies.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Currencies } from './currencies.entity'; 6 | import { CurrenciesRepository } from './currencies.repository'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Currencies, CurrenciesRepository])], 10 | providers: [CurrenciesService], 11 | controllers: [CurrenciesController], 12 | exports: [CurrenciesService], 13 | }) 14 | export class CurrenciesModule {} 15 | -------------------------------------------------------------------------------- /src/currencies/currencies.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { CurrenciesRepository } from './currencies.repository'; 4 | import { Currencies } from './currencies.entity'; 5 | 6 | describe('CurrenciesRepository', () => { 7 | let repository; 8 | let mockData; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | providers: [CurrenciesRepository], 13 | }).compile(); 14 | 15 | repository = module.get(CurrenciesRepository); 16 | mockData = { currency: 'USD', value: 1 } as Currencies; 17 | repository.save = jest.fn(); 18 | repository.delete = jest.fn(); 19 | }); 20 | 21 | it('should be defined', () => { 22 | expect(repository).toBeDefined(); 23 | }); 24 | 25 | describe('getCurrency()', () => { 26 | it('should be called findOne with correct params', async () => { 27 | repository.findOne = jest.fn().mockReturnValue({}); 28 | await repository.getCurrency('USD'); 29 | expect(repository.findOne).toBeCalledWith({ currency: 'USD' }); 30 | }); 31 | 32 | it('should be throw findOne returns empty', async () => { 33 | repository.findOne = jest.fn().mockReturnValue(undefined); 34 | await expect(repository.getCurrency('USD')).rejects.toThrow( 35 | new NotFoundException(`The currency USD not found.`), 36 | ); 37 | }); 38 | 39 | it('should be returns when findOne returns', async () => { 40 | repository.findOne = jest.fn().mockReturnValue(mockData); 41 | expect(await repository.getCurrency('USD')).toEqual(mockData); 42 | }); 43 | }); 44 | 45 | describe('createCurrency()', () => { 46 | it('should be called save with correct params', async () => { 47 | repository.save = jest.fn().mockReturnValue(mockData); 48 | await repository.createCurrency(mockData); 49 | expect(repository.save).toBeCalledWith(mockData); 50 | }); 51 | 52 | it('should be throw when save throw', async () => { 53 | repository.save = jest.fn().mockRejectedValue(new Error()); 54 | await expect(repository.createCurrency(mockData)).rejects.toThrow(); 55 | }); 56 | 57 | it('should be throw if called with invalid params', async () => { 58 | mockData.currency = 'INVALID'; 59 | await expect(repository.createCurrency(mockData)).rejects.toThrow(); 60 | }); 61 | 62 | it('should be returns created data', async () => { 63 | expect(await repository.createCurrency(mockData)).toEqual(mockData); 64 | }); 65 | }); 66 | 67 | describe('updateCurrency()', () => { 68 | it('should be called findOne with correct params', async () => { 69 | repository.findOne = jest.fn().mockReturnValue(mockData); 70 | await repository.updateCurrency(mockData); 71 | expect(repository.findOne).toBeCalledWith({ currency: 'USD' }); 72 | }); 73 | 74 | it('should be throw findOne returns empty', async () => { 75 | repository.findOne = jest.fn().mockReturnValue(undefined); 76 | await expect(repository.updateCurrency(mockData)).rejects.toThrow( 77 | new NotFoundException(`The currency ${mockData.currency} not found.`), 78 | ); 79 | }); 80 | 81 | it('should be called save with correct params', async () => { 82 | repository.findOne = jest.fn().mockReturnValue(mockData); 83 | repository.save = jest.fn().mockReturnValue(mockData); 84 | await repository.updateCurrency(mockData); 85 | expect(repository.save).toBeCalledWith(mockData); 86 | }); 87 | 88 | it('should be throw when save throw', async () => { 89 | repository.findOne = jest.fn().mockReturnValue(mockData); 90 | repository.save = jest.fn().mockRejectedValue(new Error()); 91 | await expect(repository.updateCurrency(mockData)).rejects.toThrow(); 92 | }); 93 | 94 | it('should be returns updated data', async () => { 95 | repository.findOne = jest.fn().mockReturnValue({ currency: 'USD', value: 1 }); 96 | repository.save = jest.fn().mockReturnValue({}); 97 | const result = await repository.updateCurrency({ currency: 'USD', value: 2 }); 98 | expect(result).toEqual({ currency: 'USD', value: 2 }); 99 | }); 100 | }); 101 | 102 | describe('deleteCurrency()', () => { 103 | it('should be called findOne with correct params', async () => { 104 | repository.findOne = jest.fn().mockReturnValue(mockData); 105 | await repository.deleteCurrency('USD'); 106 | expect(repository.findOne).toBeCalledWith({ currency: 'USD' }); 107 | }); 108 | 109 | it('should be throw findOne returns empty', async () => { 110 | repository.findOne = jest.fn().mockReturnValue(undefined); 111 | await expect(repository.deleteCurrency('USD')).rejects.toThrow( 112 | new NotFoundException(`The currency ${mockData.currency} not found.`), 113 | ); 114 | }); 115 | 116 | it('should be called delete with correct params', async () => { 117 | repository.findOne = jest.fn().mockReturnValue(mockData); 118 | repository.delete = jest.fn().mockReturnValue({}); 119 | await repository.deleteCurrency('USD'); 120 | expect(repository.delete).toBeCalledWith({ currency: 'USD' }); 121 | }); 122 | 123 | it('should be throw when delete throw', async () => { 124 | repository.findOne = jest.fn().mockReturnValue(mockData); 125 | repository.delete = jest.fn().mockRejectedValue(new Error()); 126 | await expect(repository.deleteCurrency('USD')).rejects.toThrow(); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/currencies/currencies.repository.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; 2 | import { validateOrReject } from 'class-validator'; 3 | import { EntityRepository, Repository } from 'typeorm'; 4 | import { Currencies } from './currencies.entity'; 5 | import { CreateCurrencyDto } from './dto/create-currency.dto'; 6 | 7 | @EntityRepository(Currencies) 8 | export class CurrenciesRepository extends Repository { 9 | async getCurrency(currency: string): Promise { 10 | const result = await this.findOne({ currency }); 11 | 12 | if (!result) { 13 | throw new NotFoundException(`The currency ${currency} not found.`); 14 | } 15 | 16 | return result; 17 | } 18 | 19 | async createCurrency(createCurrencyDto: CreateCurrencyDto): Promise { 20 | const createCurrency = new Currencies(); 21 | createCurrency.currency = createCurrencyDto.currency; 22 | createCurrency.value = createCurrencyDto.value; 23 | 24 | try { 25 | await validateOrReject(createCurrency); 26 | await this.save(createCurrency); 27 | } catch (error) { 28 | throw new InternalServerErrorException(error); 29 | } 30 | 31 | return createCurrency; 32 | } 33 | 34 | async updateCurrency({ currency, value }: CreateCurrencyDto): Promise { 35 | const result = await this.findOne({ currency }); 36 | 37 | if (!result) { 38 | throw new NotFoundException(`The currency ${currency} not found.`); 39 | } 40 | 41 | try { 42 | result.value = value; 43 | await this.save(result); 44 | } catch (error) { 45 | throw new InternalServerErrorException(error); 46 | } 47 | 48 | return result; 49 | } 50 | 51 | async deleteCurrency(currency: string): Promise { 52 | const result = await this.findOne({ currency }); 53 | 54 | if (!result) { 55 | throw new NotFoundException(`The currency ${currency} not found.`); 56 | } 57 | 58 | await this.delete({ currency }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/currencies/currencies.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { CurrenciesRepository } from './currencies.repository'; 4 | import { CurrenciesService } from './currencies.service'; 5 | import { Currencies } from './currencies.entity'; 6 | 7 | describe('CurrenciesService', () => { 8 | let service: CurrenciesService; 9 | let repository: CurrenciesRepository; 10 | let mockData; 11 | 12 | beforeEach(async () => { 13 | const currenciesRepositoryMock = { 14 | getCurrency: jest.fn(), 15 | createCurrency: jest.fn(), 16 | updateCurrency: jest.fn(), 17 | deleteCurrency: jest.fn(), 18 | }; 19 | const module: TestingModule = await Test.createTestingModule({ 20 | providers: [ 21 | CurrenciesService, 22 | { 23 | provide: CurrenciesRepository, 24 | useFactory: () => currenciesRepositoryMock, 25 | }, 26 | ], 27 | }).compile(); 28 | 29 | service = module.get(CurrenciesService); 30 | repository = module.get(CurrenciesRepository); 31 | mockData = { currency: 'USD', value: 1 } as Currencies; 32 | }); 33 | 34 | it('should be defined', () => { 35 | expect(service).toBeDefined(); 36 | }); 37 | 38 | describe('getCurrency()', () => { 39 | it('should be throw if repository throw', async () => { 40 | (repository.getCurrency as jest.Mock).mockRejectedValue(new InternalServerErrorException()); 41 | await expect(service.getCurrency('INVALID')).rejects.toThrow( 42 | new InternalServerErrorException(), 43 | ); 44 | }); 45 | 46 | it('should be not throw if repository returns', async () => { 47 | await expect(service.getCurrency('USD')).resolves.not.toThrow(); 48 | }); 49 | 50 | it('should be called repository with correct params', async () => { 51 | await service.getCurrency('USD'); 52 | expect(repository.getCurrency).toBeCalledWith('USD'); 53 | }); 54 | 55 | it('should be return when repository return', async () => { 56 | (repository.getCurrency as jest.Mock).mockReturnValue(mockData); 57 | expect(await service.getCurrency('USD')).toEqual(mockData); 58 | }); 59 | }); 60 | 61 | describe('createCurrency()', () => { 62 | it('should be throw if repository throw', async () => { 63 | (repository.createCurrency as jest.Mock).mockRejectedValue( 64 | new InternalServerErrorException(), 65 | ); 66 | mockData.currency = 'INVALID'; 67 | await expect(service.createCurrency(mockData)).rejects.toThrow( 68 | new InternalServerErrorException(), 69 | ); 70 | }); 71 | 72 | it('should be not throw if repository returns', async () => { 73 | await expect(service.createCurrency(mockData)).resolves.not.toThrow(); 74 | }); 75 | 76 | it('should be called repository with correct params', async () => { 77 | await service.createCurrency(mockData); 78 | expect(repository.createCurrency).toBeCalledWith(mockData); 79 | }); 80 | 81 | it('should be throw if value <= 0', async () => { 82 | mockData.value = 0; 83 | await expect(service.createCurrency(mockData)).rejects.toThrow( 84 | new BadRequestException('The value must be greater zero.'), 85 | ); 86 | }); 87 | 88 | it('should be return when repository return', async () => { 89 | (repository.createCurrency as jest.Mock).mockReturnValue(mockData); 90 | expect(await service.createCurrency(mockData)).toEqual(mockData); 91 | }); 92 | }); 93 | 94 | describe('updateCurrency()', () => { 95 | it('should be throw if repository throw', async () => { 96 | (repository.updateCurrency as jest.Mock).mockRejectedValue( 97 | new InternalServerErrorException(), 98 | ); 99 | mockData.currency = 'INVALID'; 100 | await expect(service.updateCurrency(mockData)).rejects.toThrow( 101 | new InternalServerErrorException(), 102 | ); 103 | }); 104 | 105 | it('should be not throw if repository returns', async () => { 106 | await expect(service.updateCurrency(mockData)).resolves.not.toThrow(); 107 | }); 108 | 109 | it('should be called repository with correct params', async () => { 110 | await service.updateCurrency(mockData); 111 | expect(repository.updateCurrency).toBeCalledWith(mockData); 112 | }); 113 | 114 | it('should be throw if value <= 0', async () => { 115 | mockData.value = 0; 116 | await expect(service.updateCurrency(mockData)).rejects.toThrow( 117 | new BadRequestException('The value must be greater zero.'), 118 | ); 119 | }); 120 | 121 | it('should be return when repository return', async () => { 122 | (repository.updateCurrency as jest.Mock).mockReturnValue(mockData); 123 | expect(await service.updateCurrency(mockData)).toEqual(mockData); 124 | }); 125 | }); 126 | 127 | describe('deleteCurrency()', () => { 128 | it('should be throw if repository throw', async () => { 129 | (repository.deleteCurrency as jest.Mock).mockRejectedValue( 130 | new InternalServerErrorException(), 131 | ); 132 | await expect(service.deleteCurrency('INVALID')).rejects.toThrow( 133 | new InternalServerErrorException(), 134 | ); 135 | }); 136 | 137 | it('should be not throw if repository returns', async () => { 138 | await expect(service.deleteCurrency('USD')).resolves.not.toThrow(); 139 | }); 140 | 141 | it('should be called repository with correct params', async () => { 142 | await service.deleteCurrency('USD'); 143 | expect(repository.deleteCurrency).toBeCalledWith('USD'); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/currencies/currencies.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { Currencies } from './currencies.entity'; 3 | import { CurrenciesRepository } from './currencies.repository'; 4 | import { CreateCurrencyDto } from './dto/create-currency.dto'; 5 | 6 | @Injectable() 7 | export class CurrenciesService { 8 | constructor(private currenciesRepository: CurrenciesRepository) {} 9 | 10 | async getCurrency(currency: string): Promise { 11 | return await this.currenciesRepository.getCurrency(currency); 12 | } 13 | 14 | async createCurrency({ currency, value }: CreateCurrencyDto): Promise { 15 | if (value <= 0) { 16 | throw new BadRequestException('The value must be greater zero.'); 17 | } 18 | return await this.currenciesRepository.createCurrency({ currency, value }); 19 | } 20 | 21 | async updateCurrency({ currency, value }: CreateCurrencyDto): Promise { 22 | if (value <= 0) { 23 | throw new BadRequestException('The value must be greater zero.'); 24 | } 25 | return await this.currenciesRepository.updateCurrency({ currency, value }); 26 | } 27 | 28 | async deleteCurrency(currency: string): Promise { 29 | return await this.currenciesRepository.deleteCurrency(currency); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/currencies/dto/create-currency.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumberString, Length } from 'class-validator'; 2 | 3 | export class CreateCurrencyDto { 4 | @IsNotEmpty() 5 | @Length(3, 3) 6 | currency: string; 7 | 8 | @IsNotEmpty() 9 | @IsNumberString() 10 | value: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/exchange/dto/exchange-input.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumberString, Length } from 'class-validator'; 2 | 3 | export class ExchangeInputDto { 4 | @IsNotEmpty() 5 | @Length(3, 3) 6 | from: string; 7 | 8 | @IsNotEmpty() 9 | @Length(3, 3) 10 | to: string; 11 | 12 | @IsNotEmpty() 13 | @IsNumberString() 14 | amount: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/exchange/exchange.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { ExchangeController } from './exchange.controller'; 4 | import { ExchangeService } from './exchange.service'; 5 | import { ExchangeType } from './types/exchange.type'; 6 | 7 | describe('ExchangeController', () => { 8 | let controller: ExchangeController; 9 | let service: ExchangeService; 10 | let mockData; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | controllers: [ExchangeController], 15 | providers: [{ provide: ExchangeService, useFactory: () => ({ convertAmount: jest.fn() }) }], 16 | }).compile(); 17 | 18 | controller = module.get(ExchangeController); 19 | service = module.get(ExchangeService); 20 | mockData = { from: 'USD', to: 'BRL', amount: 1 }; 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | describe('convertAmount()', () => { 27 | it('should be throw when service throw', async () => { 28 | (service.convertAmount as jest.Mock).mockRejectedValue(new BadRequestException()); 29 | await expect( 30 | controller.convertAmount({ from: 'INVALID', to: 'INVALID', amount: 1 }), 31 | ).rejects.toThrow(new BadRequestException()); 32 | }); 33 | 34 | it('should be called service with corrects params', async () => { 35 | await controller.convertAmount(mockData); 36 | expect(service.convertAmount).toBeCalledWith(mockData); 37 | }); 38 | 39 | it('should be returns when service returns', async () => { 40 | const mockReturn = { amount: 1 } as ExchangeType; 41 | (service.convertAmount as jest.Mock).mockReturnValue(mockReturn); 42 | expect(await controller.convertAmount(mockData)).toEqual(mockReturn); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/exchange/exchange.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, UsePipes, ValidationPipe } from '@nestjs/common'; 2 | import { ExchangeInputDto } from './dto/exchange-input.dto'; 3 | import { ExchangeService } from './exchange.service'; 4 | import { ExchangeType } from './types/exchange.type'; 5 | 6 | @Controller('exchange') 7 | export class ExchangeController { 8 | constructor(private exchangeService: ExchangeService) {} 9 | 10 | @Get() 11 | @UsePipes(ValidationPipe) 12 | async convertAmount(@Query() exchangeInputDto: ExchangeInputDto): Promise { 13 | return await this.exchangeService.convertAmount(exchangeInputDto); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/exchange/exchange.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CurrenciesModule } from 'src/currencies/currencies.module'; 3 | import { ExchangeService } from './exchange.service'; 4 | import { ExchangeController } from './exchange.controller'; 5 | 6 | @Module({ 7 | imports: [CurrenciesModule], 8 | providers: [ExchangeService], 9 | controllers: [ExchangeController], 10 | }) 11 | export class ExchangeModule {} 12 | -------------------------------------------------------------------------------- /src/exchange/exchange.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { CurrenciesService } from '../currencies/currencies.service'; 4 | import { ExchangeService } from './exchange.service'; 5 | import { ExchangeInputDto } from './dto/exchange-input.dto'; 6 | 7 | describe('ExchangeService', () => { 8 | let service: ExchangeService; 9 | let currenciesService: CurrenciesService; 10 | let mockData; 11 | 12 | beforeEach(async () => { 13 | const currenciesServiceMock = { 14 | getCurrency: jest.fn().mockReturnValue({ value: 1 }), 15 | }; 16 | const module: TestingModule = await Test.createTestingModule({ 17 | providers: [ 18 | ExchangeService, 19 | { provide: CurrenciesService, useFactory: () => currenciesServiceMock }, 20 | ], 21 | }).compile(); 22 | 23 | service = module.get(ExchangeService); 24 | currenciesService = module.get(CurrenciesService); 25 | mockData = { from: 'USD', to: 'BRL', amount: 1 } as ExchangeInputDto; 26 | }); 27 | 28 | it('should be defined', () => { 29 | expect(service).toBeDefined(); 30 | }); 31 | 32 | describe('convertAmount()', () => { 33 | it('should be throw if called with invalid params', async () => { 34 | mockData.from = ''; 35 | await expect(service.convertAmount(mockData)).rejects.toThrow(new BadRequestException()); 36 | 37 | mockData.from = 'USD'; 38 | mockData.amount = 0; 39 | await expect(service.convertAmount(mockData)).rejects.toThrow(new BadRequestException()); 40 | 41 | mockData.from = 'USD'; 42 | mockData.to = ''; 43 | mockData.amount = 1; 44 | await expect(service.convertAmount(mockData)).rejects.toThrow(new BadRequestException()); 45 | }); 46 | 47 | it('should be not throw if called with valid params', async () => { 48 | await expect(service.convertAmount(mockData)).resolves.not.toThrow(); 49 | }); 50 | 51 | it('should be called getCurrency twice', async () => { 52 | await service.convertAmount(mockData); 53 | expect(currenciesService.getCurrency).toBeCalledTimes(2); 54 | }); 55 | 56 | it('should be called getCurrency with correct params', async () => { 57 | await service.convertAmount(mockData); 58 | expect(currenciesService.getCurrency).toBeCalledWith('USD'); 59 | expect(currenciesService.getCurrency).toHaveBeenLastCalledWith('BRL'); 60 | }); 61 | 62 | it('should be throw when getCurrency throw', async () => { 63 | (currenciesService.getCurrency as jest.Mock).mockRejectedValue(new Error()); 64 | mockData.from = 'INVALID'; 65 | await expect(service.convertAmount(mockData)).rejects.toThrow(); 66 | }); 67 | 68 | it('should be return conversion value', async () => { 69 | (currenciesService.getCurrency as jest.Mock).mockResolvedValue({ value: 1 }); 70 | mockData.from = 'USD'; 71 | mockData.to = 'USD'; 72 | expect(await service.convertAmount(mockData)).toEqual({ 73 | amount: 1, 74 | }); 75 | 76 | (currenciesService.getCurrency as jest.Mock).mockResolvedValueOnce({ value: 1 }); 77 | mockData.from = 'USD'; 78 | mockData.to = 'BRL'; 79 | (currenciesService.getCurrency as jest.Mock).mockResolvedValueOnce({ value: 0.2 }); 80 | expect(await service.convertAmount(mockData)).toEqual({ 81 | amount: 5, 82 | }); 83 | 84 | (currenciesService.getCurrency as jest.Mock).mockResolvedValueOnce({ value: 0.2 }); 85 | (currenciesService.getCurrency as jest.Mock).mockResolvedValueOnce({ value: 1 }); 86 | mockData.from = 'BRL'; 87 | mockData.to = 'USD'; 88 | expect(await service.convertAmount(mockData)).toEqual({ 89 | amount: 0.2, 90 | }); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/exchange/exchange.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { CurrenciesService } from '../currencies/currencies.service'; 3 | import { ExchangeInputDto } from './dto/exchange-input.dto'; 4 | import { ExchangeType } from './types/exchange.type'; 5 | 6 | @Injectable() 7 | export class ExchangeService { 8 | constructor(private currenciesService: CurrenciesService) {} 9 | 10 | async convertAmount({ from, to, amount }: ExchangeInputDto): Promise { 11 | if (!from || !to || !amount) { 12 | throw new BadRequestException(); 13 | } 14 | 15 | try { 16 | const currencyFrom = await this.currenciesService.getCurrency(from); 17 | const currencyTo = await this.currenciesService.getCurrency(to); 18 | 19 | return { amount: (currencyFrom.value / currencyTo.value) * amount }; 20 | } catch (error) { 21 | throw new Error(error); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/exchange/types/exchange.type.ts: -------------------------------------------------------------------------------- 1 | export type ExchangeType = { 2 | amount: number; 3 | }; 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------