├── .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 |
--------------------------------------------------------------------------------