diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..11fde01 --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +MIGRATE_dbConnectionUri= diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 50d6d1f..8602970 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,21 +1,22 @@ -# name: e2e -# on: -# push: -# branches: -# - main -# - development -# jobs: -# e2eTests: -# name: e2e-Tests -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v2 -# with: -# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis -# - name: install -# run: npm install -# - name: e2e test -# run: npm run test:e2e -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any +name: e2e +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +jobs: + e2eTests: + name: e2e-Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: install + run: npm install + - name: e2e test + run: npm run test:e2e + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any diff --git a/.github/workflows/sonar-unit-tests.yml b/.github/workflows/sonar-unit-tests.yml index 03e3ffc..7508d6a 100644 --- a/.github/workflows/sonar-unit-tests.yml +++ b/.github/workflows/sonar-unit-tests.yml @@ -14,9 +14,9 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: install + - name: Install run: npm install - - name: unit test + - name: Unit test run: npm run test:cov - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master diff --git a/.gitignore b/.gitignore index 626b825..ffad418 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ node_modules # Sonar .scannerwork/.sonar_lock .scannerwork/report-task.txt -*.ucfg \ No newline at end of file +*.ucfg + +# Env +.env + diff --git a/config/yaml/values.yaml b/config/yaml/values.yaml index 6640751..16aa11c 100644 --- a/config/yaml/values.yaml +++ b/config/yaml/values.yaml @@ -4,6 +4,6 @@ repository: url: 'https://www.guiamais.com.br/bairros' database: mongodb: - connection: 'mongodb+srv://dev-seeder-root:hpYpxyjrKExNQviu@devseeder.v6xtv.mongodb.net/places' + connection: 'mongodb+srv://dev-seeder-root:hpYpxyjrKExNQviu@devseeder.v6xtv.mongodb.net/places?retryWrites=true&w=majority&ssl=true&wtimeoutMS=0' api: port: 3000 diff --git a/migrate.json b/migrate.json new file mode 100644 index 0000000..ab4bf85 --- /dev/null +++ b/migrate.json @@ -0,0 +1,3 @@ +{ + "migrations-dir": "./src/config/database/migrations" +} \ No newline at end of file diff --git a/package.json b/package.json index cedc094..01729bb 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/e2e/jest-e2e.json" + "test:e2e": "jest --config ./test/e2e/jest-e2e.json", + "migrate:up": "migrate up", + "migrate:down": "migrate down", + "migrate:build-up": "npm run build & migrate up" }, "dependencies": { "@golevelup/ts-jest": "^0.3.3", @@ -85,7 +88,8 @@ "coveragePathIgnorePatterns": [ "/node_modules/", "/src/main.ts", - "/src/app.module.ts" + "/src/app.module.ts", + "/src/config/database" ], "testEnvironment": "node" } diff --git a/sonar-project.properties b/sonar-project.properties index 6b0da3c..7c26efe 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -23,7 +23,9 @@ sonar.test.inclusions=test/**/*.spec.ts sonar.tests=./test -sonar.exclusions= .eslintrc.js, src/main.ts, src/test/*, test/app.e2e-spec.ts, src/app.module.ts +sonar.exclusions= .eslintrc.js, src/main.ts, src/test/*, test/app.e2e-spec.ts, src/app.module.ts, src/config/database/*, src/config/database/migrations/* + +sonar.cpd.exclusions=src/config/database/*, src/config/database/migrations/* sonar.typescript.tsconfigPath=tsconfig.json diff --git a/src/app.module.ts b/src/app.module.ts index 027cc12..cfd0b29 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,18 +2,19 @@ import { ClassSerializerInterceptor, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { MongooseModule } from '@nestjs/mongoose'; -import { PuppeteerModule } from 'nest-puppeteer'; import { FiltersModule } from './core/error-handling/filters.module'; import { TransformResponseInterceptor } from './core/http/transform-response.interceptor'; import { ExtensionsModule } from './microservice/adapter/helper/extensions/exensions.module'; +import { CustomPuppeteerModule } from './microservice/adapter/helper/modules/custom-puppeteer.module'; import { NeighborhoodsModule } from './microservice/adapter/neighborhoods.module'; @Module({ imports: [ FiltersModule, ExtensionsModule, - PuppeteerModule.forRoot({ - isGlobal: true + CustomPuppeteerModule.forRootLaunchOptions({ + isGlobal: true, + ignoreHTTPSErrors: true }), MongooseModule.forRootAsync({ imports: [ConfigModule], diff --git a/src/config/database/migration-connection-factory.js b/src/config/database/migration-connection-factory.js new file mode 100644 index 0000000..a254776 --- /dev/null +++ b/src/config/database/migration-connection-factory.js @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +require(`../../../dist/microservice/adapter/helper/extensions/string.extension.js`); +const config = require('../../../dist/config/configuration.js').default(); + +module.exports = async function (collectionName, schemaName = null) { + schemaName = schemaName ? schemaName : collectionName; + const mongoose = require('mongoose'); + const schemaCollection = require(`../../../dist/microservice/domain/schemas/${schemaName}.schema`); + const modelSchema = schemaCollection[`${schemaName.capitalize()}Schema`]; + await mongoose.connect(config.database.mongodb.connection, { + useNewUrlParser: true, + useUnifiedTopology: true + }); + return mongoose.model(collectionName, modelSchema); +}; diff --git a/src/config/database/migrations/1655748067450-add_alias_prop.js b/src/config/database/migrations/1655748067450-add_alias_prop.js new file mode 100644 index 0000000..bebef45 --- /dev/null +++ b/src/config/database/migrations/1655748067450-add_alias_prop.js @@ -0,0 +1,60 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('countries', 'country'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + const res = (await model).aggregate([ + { + $project: { + alias: { $objectToArray: '$translations' }, + iso2: '$iso2', + iso3: '$iso3', + name: '$name' + } + } + ]); + + for await (const item of res) { + const arrAlias = await item.alias.map((obj) => obj.v); + arrAlias.push(item.name); + arrAlias.push(item.iso2); + arrAlias.push(item.iso3); + await ( + await model + ) + .findByIdAndUpdate(item._id, { + $set: { + alias: arrAlias + } + }) + .exec(); + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + await ( + await model + ) + .updateMany( + {}, + { + $set: { + alias: [] + } + } + ) + .exec(); + mongoose.connection.close(); +} + +module.exports = { up, down }; diff --git a/src/config/database/migrations/1655914826752-rename_country_props.js b/src/config/database/migrations/1655914826752-rename_country_props.js new file mode 100644 index 0000000..044cbd3 --- /dev/null +++ b/src/config/database/migrations/1655914826752-rename_country_props.js @@ -0,0 +1,73 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('countries', 'country'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + numeric_code: 'numericCode', + phone_code: 'phoneCode', + currency_name: 'currencyName', + currency_symbol: 'currencySymbol' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + console.log(err); + mongoose.connection.close(); + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + numericCode: 'numeric_code', + phoneCode: 'phone_code', + currencyName: 'currency_name', + currencySymbol: 'currency_symbol' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + console.log(err); + mongoose.connection.close(); + } +} + +module.exports = { up, down }; diff --git a/src/config/database/migrations/1655917316641-rename_state_props.js b/src/config/database/migrations/1655917316641-rename_state_props.js new file mode 100644 index 0000000..73c1d3c --- /dev/null +++ b/src/config/database/migrations/1655917316641-rename_state_props.js @@ -0,0 +1,73 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('state'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + state_code: 'stateCode', + country_id: 'countryId', + country_name: 'countryName', + country_code: 'countryCode' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + stateCode: 'state_code', + countryId: 'country_id', + countryName: 'country_name', + countryCode: 'country_code' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +module.exports = { up, down }; diff --git a/src/config/database/migrations/1655918773118-add_alias_state.js b/src/config/database/migrations/1655918773118-add_alias_state.js new file mode 100644 index 0000000..4a3d73b --- /dev/null +++ b/src/config/database/migrations/1655918773118-add_alias_state.js @@ -0,0 +1,77 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('state'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $set: { + alias: [] + } + } + ) + .exec(); + + const res = (await model).find({}); + + for await (const item of res) { + const arrAlias = []; + arrAlias.push(item.name); + arrAlias.push(item.stateCode); + await ( + await model + ) + .findByIdAndUpdate(item._id, { + $set: { + alias: arrAlias + } + }) + .exec(); + } + + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $set: { + alias: [] + } + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +module.exports = { up, down }; diff --git a/src/config/database/migrations/1655923117808-rename_city_props.js b/src/config/database/migrations/1655923117808-rename_city_props.js new file mode 100644 index 0000000..00d9dde --- /dev/null +++ b/src/config/database/migrations/1655923117808-rename_city_props.js @@ -0,0 +1,77 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('cities', 'city'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + state_id: 'stateId', + state_name: 'stateName', + state_code: 'stateCode', + country_id: 'countryId', + country_name: 'countryName', + country_code: 'countryCode' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $rename: { + stateId: 'state_id', + stateName: 'state_name', + stateCode: 'state_code', + countryId: 'country_id', + countryName: 'country_name', + countryCode: 'country_code' + } + }, + { + // Strict allows to update keys that do not exist anymore in the schema + strict: false + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +module.exports = { up, down }; diff --git a/src/config/database/migrations/1655923550217-add_alias_city.js b/src/config/database/migrations/1655923550217-add_alias_city.js new file mode 100644 index 0000000..f28bc9b --- /dev/null +++ b/src/config/database/migrations/1655923550217-add_alias_city.js @@ -0,0 +1,76 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const mongoose = require('mongoose'); + +const startConnection = require('../migration-connection-factory.js'); + +const model = startConnection('cities', 'city'); + +/** + * Make any changes you need to make to the database here + */ +async function up() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $set: { + alias: [] + } + } + ) + .exec(); + + const res = (await model).find({}); + + for await (const item of res) { + const arrAlias = []; + arrAlias.push(item.name); + await ( + await model + ) + .findByIdAndUpdate(item._id, { + $set: { + alias: arrAlias + } + }) + .exec(); + } + + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +/** + * Make any changes that UNDO the up function side effects here (if possible) + */ +async function down() { + try { + await ( + await model + ) + .updateMany( + {}, + { + $set: { + alias: [] + } + } + ) + .exec(); + mongoose.connection.close(); + } catch (err) { + console.log('err --> '); + mongoose.connection.close(); + throw err; + } +} + +module.exports = { up, down }; diff --git a/src/core/error-handling/exception/invalid-data.exception.ts b/src/core/error-handling/exception/invalid-data.exception.ts new file mode 100644 index 0000000..eccfe6c --- /dev/null +++ b/src/core/error-handling/exception/invalid-data.exception.ts @@ -0,0 +1,12 @@ +import { HttpStatus } from '@nestjs/common'; +import { CustomErrorException } from './custom-error.exception'; + +export class InvalidDataException extends CustomErrorException { + constructor(element: string, value: string) { + super( + `Invalid ${element.capitalize()} '${value}'`, + HttpStatus.NOT_ACCEPTABLE, + 3 + ); + } +} diff --git a/src/core/error-handling/exception/mongodb-.exception.ts b/src/core/error-handling/exception/mongodb-.exception.ts new file mode 100644 index 0000000..f883a5b --- /dev/null +++ b/src/core/error-handling/exception/mongodb-.exception.ts @@ -0,0 +1,8 @@ +import { HttpStatus } from '@nestjs/common'; +import { CustomErrorException } from './custom-error.exception'; + +export class MongoDBException extends CustomErrorException { + constructor(message: string, code: string | number) { + super(message, HttpStatus.BAD_REQUEST, typeof code == 'number' ? code : 4); + } +} diff --git a/src/core/error-handling/filter/mongodb-exception.filter.ts b/src/core/error-handling/filter/mongodb-exception.filter.ts new file mode 100644 index 0000000..5dcd6ae --- /dev/null +++ b/src/core/error-handling/filter/mongodb-exception.filter.ts @@ -0,0 +1,16 @@ +import { Catch, HttpStatus } from '@nestjs/common'; +import { AbstractExceptionFilter } from './abstract-exception.filter'; +import { CustomExceptionReponse } from '../interface/custom-exception-response.interface'; +import { MongoDBException } from '../exception/mongodb-.exception'; + +@Catch(MongoDBException) +export class MongoDBExceptionFilter extends AbstractExceptionFilter { + makeCustomResponse(exception: MongoDBException): CustomExceptionReponse { + return { + status: HttpStatus.BAD_REQUEST, + message: exception.message, + type: exception.name, + errorCode: exception.errCode + }; + } +} diff --git a/src/core/error-handling/filters.module.ts b/src/core/error-handling/filters.module.ts index 086560e..d6cd69a 100644 --- a/src/core/error-handling/filters.module.ts +++ b/src/core/error-handling/filters.module.ts @@ -3,6 +3,7 @@ import { APP_FILTER } from '@nestjs/core'; import { CustomErrorExceptionFilter } from './filter/custom-error-exception.filter'; import { ErrorExceptionFilter } from './filter/error-exception.filter'; import { HttpExceptionFilter } from './filter/http-exception.filter'; +import { MongoDBExceptionFilter } from './filter/mongodb-exception.filter'; @Module({ imports: [], @@ -19,6 +20,10 @@ import { HttpExceptionFilter } from './filter/http-exception.filter'; { provide: APP_FILTER, useClass: ErrorExceptionFilter + }, + { + provide: APP_FILTER, + useClass: MongoDBExceptionFilter } ] }) diff --git a/src/core/http/transform-response.interceptor.ts b/src/core/http/transform-response.interceptor.ts index 8b089dc..cd5c79a 100644 --- a/src/core/http/transform-response.interceptor.ts +++ b/src/core/http/transform-response.interceptor.ts @@ -18,15 +18,19 @@ export class TransformResponseInterceptor implements NestInterceptor { this.httpAdapter = adapterHost.httpAdapter; } + /* istanbul ignore next */ intercept( context: ExecutionContext, next: CallHandler ): Observable | Promise> { return next.handle().pipe( /* istanbul ignore next */ - map((responseController: NestResponse) => { - return this.interceptResponse(responseController, context); - }) + map( + /* istanbul ignore next */ + (responseController: NestResponse) => { + return this.interceptResponse(responseController, context); + } + ) ); } diff --git a/src/microservice/adapter/controller/neighborhoods.controller.ts b/src/microservice/adapter/controller/neighborhoods.controller.ts index bca40b2..92069e7 100644 --- a/src/microservice/adapter/controller/neighborhoods.controller.ts +++ b/src/microservice/adapter/controller/neighborhoods.controller.ts @@ -12,14 +12,14 @@ export class NeighborhoodsController extends AbstractController { } @Get('/city/:country/:state/:city') - getNeighborhoodsByCity( + async getNeighborhoodsByCity( @Param('country') country, @Param('state') state, @Param('city') city - ): NestResponse { + ): Promise { return this.buildResponse( HttpStatus.OK, - this.getNeighborhoodsByCityService.getNeighborhoodsByCity( + await this.getNeighborhoodsByCityService.getNeighborhoodsByCity( country, state, city diff --git a/src/microservice/adapter/helper/builder/neighborhoods-mongo.builder.ts b/src/microservice/adapter/helper/builder/neighborhoods-mongo.builder.ts deleted file mode 100644 index 00dd645..0000000 --- a/src/microservice/adapter/helper/builder/neighborhoods-mongo.builder.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SearchNeighborhoods } from 'src/microservice/domain/model/search/search-neighborhoods.model'; -import { NeighborhoodsByCity } from '../../../domain/model/neighborhoods-by-city.model'; -import { Neighborhood } from '../../../domain/schemas/neighborhood.schema'; - -export class NeighborhoodsMongoBuilder { - constructor(private readonly puppeteerResponse: NeighborhoodsByCity[]) {} - - build(searchParams: SearchNeighborhoods): Neighborhood[] { - const arr = []; - - this.puppeteerResponse.forEach((item) => { - const obj = new Neighborhood(); - obj.country = searchParams.country; - obj.state = searchParams.state; - obj.city = searchParams.city; - obj.name = item.name; - arr.push(obj); - }); - - return arr; - } -} diff --git a/src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.ts b/src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.ts new file mode 100644 index 0000000..8cc804f --- /dev/null +++ b/src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.ts @@ -0,0 +1,25 @@ +import { ValidOutputSearchNeighborhood } from 'src/microservice/domain/interface/valid-output-search/valid-outpu-search-neighborhood.interface'; +import { NeighborhoodsByCity } from '../../../../domain/model/neighborhoods-by-city.model'; +import { Neighborhood } from '../../../../domain/schemas/neighborhood.schema'; + +export class NeighborhoodsMongoBuilder { + constructor(private readonly puppeteerResponse: NeighborhoodsByCity[]) {} + + build(searchParams: ValidOutputSearchNeighborhood): Neighborhood[] { + const arr = []; + + this.puppeteerResponse.forEach((item) => { + const obj = new Neighborhood(); + obj.countryId = searchParams.country.id; + obj.country = searchParams.country.name; + obj.stateId = searchParams.state.id; + obj.state = searchParams.state.stateCode; + obj.cityId = searchParams.city.id; + obj.city = searchParams.city.name; + obj.name = item.name; + arr.push(obj); + }); + + return arr; + } +} diff --git a/src/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.ts b/src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.ts similarity index 69% rename from src/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.ts rename to src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.ts index 2dd4d8e..430e0f9 100644 --- a/src/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.ts +++ b/src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.ts @@ -1,5 +1,5 @@ -import { NeighborhoodsByCity } from '../../../domain/model/neighborhoods-by-city.model'; -import { Neighborhood } from '../../../domain/schemas/neighborhood.schema'; +import { NeighborhoodsByCity } from '../../../../domain/model/neighborhoods-by-city.model'; +import { Neighborhood } from '../../../../domain/schemas/neighborhood.schema'; export class NeighborhoodsPuppeteerBuilder { constructor(private readonly mongoResponse: Neighborhood[]) {} diff --git a/src/microservice/adapter/helper/builder/neighborhoods/search-neighborhoods-db.builder.ts b/src/microservice/adapter/helper/builder/neighborhoods/search-neighborhoods-db.builder.ts new file mode 100644 index 0000000..24f9d66 --- /dev/null +++ b/src/microservice/adapter/helper/builder/neighborhoods/search-neighborhoods-db.builder.ts @@ -0,0 +1,16 @@ +import { ValidOutputSearchNeighborhood } from '../../../../domain/interface/valid-output-search/valid-outpu-search-neighborhood.interface'; +import { SearchNeighborhoodsDB } from '../../../../domain/model/search/search-neighborhoods-db.model'; + +export class SearchNeighborhoodsDBBuilder { + constructor( + private readonly convertedSearch: ValidOutputSearchNeighborhood + ) {} + + build(): SearchNeighborhoodsDB { + return new SearchNeighborhoodsDB( + this.convertedSearch.country.id, + this.convertedSearch.state.id, + this.convertedSearch.city.id + ); + } +} diff --git a/src/microservice/adapter/helper/modules/custom-puppeteer.module.ts b/src/microservice/adapter/helper/modules/custom-puppeteer.module.ts new file mode 100644 index 0000000..2d182f9 --- /dev/null +++ b/src/microservice/adapter/helper/modules/custom-puppeteer.module.ts @@ -0,0 +1,13 @@ +import { PuppeteerModule } from 'nest-puppeteer'; +import { PuppeteerCoreModule } from 'nest-puppeteer/dist/puppeteer-core.module'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +// const puppeteer_core_module_1 = require('nest-puppeteer/dist/puppeteer-core.module'); +export class CustomPuppeteerModule extends PuppeteerModule { + static forRootLaunchOptions(options) { + return { + module: PuppeteerModule, + global: options === null ? false : options.isGlobal, + imports: [PuppeteerCoreModule.forRoot(options)] + }; + } +} diff --git a/src/microservice/adapter/neighborhoods.module.ts b/src/microservice/adapter/neighborhoods.module.ts index 9ad0f6e..8f54141 100644 --- a/src/microservice/adapter/neighborhoods.module.ts +++ b/src/microservice/adapter/neighborhoods.module.ts @@ -12,6 +12,15 @@ import { } from '../domain/schemas/neighborhood.schema'; import { MongooseModule } from '@nestjs/mongoose'; import { SaveNeighborhoodsByCityService } from '../domain/service/neighborhoods/save-neighborhoods-by-city.service'; +import { Country, CountrySchema } from '../domain/schemas/country.schema'; +import { GetCountryByNameOrAliasService } from '../domain/service/countries/get-country-by-name-or-alias.service'; +import { CountriesMongoose } from './repository/countries/countries-mongoose.repository'; +import { State, StateSchema } from '../domain/schemas/state.schema'; +import { GetStateByNameOrAliasService } from '../domain/service/states/get-state-by-name-or-alias.service'; +import { StatesMongoose } from './repository/states/states-mongoose.repository'; +import { City, CitySchema } from '../domain/schemas/city.schema'; +import { GetCityByNameOrAliasService } from '../domain/service/cities/get-city-by-name-or-alias.service'; +import { CitiesMongoose } from './repository/cities/cities-mongoose.repository'; @Module({ imports: [ @@ -21,7 +30,10 @@ import { SaveNeighborhoodsByCityService } from '../domain/service/neighborhoods/ load: [configuration] }), MongooseModule.forFeature([ - { name: Neighborhood.name, schema: NeighborhoodSchema } + { name: Neighborhood.name, schema: NeighborhoodSchema }, + { name: Country.name, schema: CountrySchema }, + { name: State.name, schema: StateSchema }, + { name: City.name, schema: CitySchema } ]) ], controllers: [NeighborhoodsController], @@ -31,8 +43,14 @@ import { SaveNeighborhoodsByCityService } from '../domain/service/neighborhoods/ useClass: GuiaMaisRepository }, NeighborhoodsMongoose, + CountriesMongoose, + StatesMongoose, + CitiesMongoose, GetNeighborhoodsByCityService, - SaveNeighborhoodsByCityService + SaveNeighborhoodsByCityService, + GetCountryByNameOrAliasService, + GetStateByNameOrAliasService, + GetCityByNameOrAliasService ] }) export class NeighborhoodsModule {} diff --git a/src/microservice/adapter/repository/cities/cities-mongoose.repository.ts b/src/microservice/adapter/repository/cities/cities-mongoose.repository.ts new file mode 100644 index 0000000..af12313 --- /dev/null +++ b/src/microservice/adapter/repository/cities/cities-mongoose.repository.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { City, CityDocument } from '../../../domain/schemas/city.schema'; +import { PlacesMongooseRepository } from '../../../domain/repository/mongoose/places-mongoose.repository'; + +@Injectable() +export class CitiesMongoose extends PlacesMongooseRepository< + City, + CityDocument +> { + constructor( + @InjectModel(City.name) + model: Model + ) { + super(model); + } +} diff --git a/src/microservice/adapter/repository/countries/countries-mongoose.repository.ts b/src/microservice/adapter/repository/countries/countries-mongoose.repository.ts new file mode 100644 index 0000000..e9af171 --- /dev/null +++ b/src/microservice/adapter/repository/countries/countries-mongoose.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { PlacesMongooseRepository } from '../../../domain/repository/mongoose/places-mongoose.repository'; +import { + Country, + CountryDocument +} from '../../../domain/schemas/country.schema'; + +@Injectable() +export class CountriesMongoose extends PlacesMongooseRepository< + Country, + CountryDocument +> { + constructor( + @InjectModel(Country.name) + model: Model + ) { + super(model); + } +} diff --git a/src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.ts b/src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.ts index c1635a2..f8cda48 100644 --- a/src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.ts +++ b/src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.ts @@ -1,15 +1,18 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { SearchNeighborhoods } from 'src/microservice/domain/model/search/search-neighborhoods.model'; -import { MongooseRepository } from '../../../domain/repository/mongoose.repository'; +import { SearchNeighborhoodsDB } from 'src/microservice/domain/model/search/search-neighborhoods-db.model'; +import { PlacesMongooseRepository } from '../../../domain/repository/mongoose/places-mongoose.repository'; import { Neighborhood, NeighborhoodDocument } from '../../../domain/schemas/neighborhood.schema'; @Injectable() -export class NeighborhoodsMongoose extends MongooseRepository { +export class NeighborhoodsMongoose extends PlacesMongooseRepository< + Neighborhood, + NeighborhoodDocument +> { constructor( @InjectModel(Neighborhood.name) model: Model @@ -18,8 +21,12 @@ export class NeighborhoodsMongoose extends MongooseRepository { } async findBySearchParams( - searchParams: SearchNeighborhoods + searchParams: SearchNeighborhoodsDB ): Promise { - return this.model.find(searchParams).select({ _id: 0 }).lean().exec(); + return this.model + .find(this.buildRegexFilterQuery(searchParams)) + .select({ _id: 0 }) + .lean() + .exec(); } } diff --git a/src/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.ts b/src/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.ts index 66d3c73..c386333 100644 --- a/src/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.ts +++ b/src/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { CheerioAPI } from 'cheerio'; import { InjectPage } from 'nest-puppeteer'; import { NeighborhoodsByCity } from '../../../../domain/model/neighborhoods-by-city.model'; -import { SearchNeighborhoods } from '../../../../domain/model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../../domain/model/search/search-neighborhoods-input.model'; import { IPuppeteerNeighborhoodRepository } from '../../../../domain/interface/puppeteer/repository/puppeteer-neighborhood-repository.interface'; import { PuppeteerNeighborhoodRepository } from '../../../../domain/repository/puppeteer/neighborhood/puppeteer-neighborhood.repository'; import { Page } from '../../../../domain/interface/puppeteer/page.interface'; @@ -42,7 +42,9 @@ export class GuiaMaisRepository return arrNeighborhoods; } - async callEndpoint(searchParams: SearchNeighborhoods): Promise { + async callEndpoint( + searchParams: SearchNeighborhoodsInput + ): Promise { const url = `${this.url}/${searchParams.city}-${searchParams.state}`; return this.getDocumentHtml(url); } diff --git a/src/microservice/adapter/repository/states/states-mongoose.repository.ts b/src/microservice/adapter/repository/states/states-mongoose.repository.ts new file mode 100644 index 0000000..9b89350 --- /dev/null +++ b/src/microservice/adapter/repository/states/states-mongoose.repository.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { State, StateDocument } from '../../../domain/schemas/state.schema'; +import { PlacesMongooseRepository } from '../../../domain/repository/mongoose/places-mongoose.repository'; + +@Injectable() +export class StatesMongoose extends PlacesMongooseRepository< + State, + StateDocument +> { + constructor( + @InjectModel(State.name) + model: Model + ) { + super(model); + } +} diff --git a/src/microservice/domain/controller/abstract-controller.ts b/src/microservice/domain/controller/abstract-controller.ts index d942f58..23df143 100644 --- a/src/microservice/domain/controller/abstract-controller.ts +++ b/src/microservice/domain/controller/abstract-controller.ts @@ -1,7 +1,11 @@ +import { Logger } from '@nestjs/common'; import { NestResponseBuilder } from '../../../core/http/nest-response.builder'; export abstract class AbstractController { - buildResponse(status, body, header = {}) { + protected readonly logger: Logger = new Logger(this.constructor.name); + + async buildResponse(status, body, header = {}) { + this.logger.log('Finishing application request...'); return new NestResponseBuilder() .setStatus(status) .setHeader(header) diff --git a/src/microservice/domain/interface/place.interface.ts b/src/microservice/domain/interface/place.interface.ts new file mode 100644 index 0000000..fc56675 --- /dev/null +++ b/src/microservice/domain/interface/place.interface.ts @@ -0,0 +1,5 @@ +export interface Place { + id: number; + name: string; + alias: string[]; +} diff --git a/src/microservice/domain/interface/puppeteer/repository/puppeteer-neighborhood-repository.interface.ts b/src/microservice/domain/interface/puppeteer/repository/puppeteer-neighborhood-repository.interface.ts index 063ff0c..1dd671d 100644 --- a/src/microservice/domain/interface/puppeteer/repository/puppeteer-neighborhood-repository.interface.ts +++ b/src/microservice/domain/interface/puppeteer/repository/puppeteer-neighborhood-repository.interface.ts @@ -1,10 +1,10 @@ -import { SearchNeighborhoods } from '../../../model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../model/search/search-neighborhoods-input.model'; import { NeighborhoodsByCity } from '../../../model/neighborhoods-by-city.model'; import { IPuppeteerRepository } from './puppeteer-repository.interface'; export interface IPuppeteerNeighborhoodRepository - extends IPuppeteerRepository { + extends IPuppeteerRepository { getNeighborhoodsByCity( - searchParams: SearchNeighborhoods + searchParams: SearchNeighborhoodsInput ): Promise; } diff --git a/src/microservice/domain/interface/valid-output-search/valid-outpu-search-neighborhood.interface.ts b/src/microservice/domain/interface/valid-output-search/valid-outpu-search-neighborhood.interface.ts new file mode 100644 index 0000000..435c65c --- /dev/null +++ b/src/microservice/domain/interface/valid-output-search/valid-outpu-search-neighborhood.interface.ts @@ -0,0 +1,9 @@ +import { City } from '../../schemas/city.schema'; +import { Country } from '../../schemas/country.schema'; +import { State } from '../../schemas/state.schema'; + +export interface ValidOutputSearchNeighborhood { + country: Country; + state: State; + city: City; +} diff --git a/src/microservice/domain/model/search/search-neighborhoods-db.model.ts b/src/microservice/domain/model/search/search-neighborhoods-db.model.ts new file mode 100644 index 0000000..1ecd08c --- /dev/null +++ b/src/microservice/domain/model/search/search-neighborhoods-db.model.ts @@ -0,0 +1,9 @@ +export class SearchNeighborhoodsDB { + public name; + + constructor( + public countryId: number, + public stateId: number, + public cityId: number + ) {} +} diff --git a/src/microservice/domain/model/search/search-neighborhoods.model.ts b/src/microservice/domain/model/search/search-neighborhoods-input.model.ts similarity index 77% rename from src/microservice/domain/model/search/search-neighborhoods.model.ts rename to src/microservice/domain/model/search/search-neighborhoods-input.model.ts index 67df7d6..61367c5 100644 --- a/src/microservice/domain/model/search/search-neighborhoods.model.ts +++ b/src/microservice/domain/model/search/search-neighborhoods-input.model.ts @@ -1,4 +1,4 @@ -export class SearchNeighborhoods { +export class SearchNeighborhoodsInput { public name: string; constructor( diff --git a/src/microservice/domain/repository/mongoose.repository.ts b/src/microservice/domain/repository/mongoose.repository.ts deleted file mode 100644 index 547e77f..0000000 --- a/src/microservice/domain/repository/mongoose.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Model } from 'mongoose'; -import { NeighborhoodDocument } from '../schemas/neighborhood.schema'; - -export abstract class MongooseRepository { - constructor(protected model: Model) {} - - async insert(document: Collection): Promise { - this.model.create(document); - } -} diff --git a/src/microservice/domain/repository/mongoose/commented_transactions.txt b/src/microservice/domain/repository/mongoose/commented_transactions.txt new file mode 100644 index 0000000..09287d4 --- /dev/null +++ b/src/microservice/domain/repository/mongoose/commented_transactions.txt @@ -0,0 +1,26 @@ +// private async startSession(): Promise { + // this.session = await this.connection.startSession(); + // return this.session; + // } + + // async startTransaction(): Promise { + // if (!this.session || !this.session.inTransaction()) + // await this.startSession(); + + // await this.session.startTransaction(); + // this.logger.log('Starting transaction...'); + // } + + // async commit(): Promise { + // if (this.session.inTransaction()) { + // this.logger.log('Commit transaction...'); + // await this.session.commitTransaction(); + // } + // } + + // async rollback(): Promise { + // if (this.session.inTransaction()) { + // this.logger.error('Rollback transaction...'); + // await this.session.abortTransaction(); + // } + // } \ No newline at end of file diff --git a/src/microservice/domain/repository/mongoose/mongoose.repository.ts b/src/microservice/domain/repository/mongoose/mongoose.repository.ts new file mode 100644 index 0000000..fa06222 --- /dev/null +++ b/src/microservice/domain/repository/mongoose/mongoose.repository.ts @@ -0,0 +1,27 @@ +import { Logger } from '@nestjs/common'; +import { Model } from 'mongoose'; + +export abstract class MongooseRepository { + protected readonly logger: Logger = new Logger(this.constructor.name); + + constructor(protected model: Model) {} + + async create(document: Collection): Promise { + return new Promise(async (resolve, reject) => { + this.model.create(document, function (err) { + if (err) reject(err); + resolve(); + }); + }); + } + + buildRegexFilterQuery(objSearch: object = {}) { + const objSearchRegex = {}; + Object.keys(objSearch).forEach(function (key) { + objSearchRegex[key] = objSearch[key]; + if (isNaN(objSearch[key])) + objSearchRegex[key] = new RegExp(objSearch[key], 'i'); + }); + return objSearchRegex; + } +} diff --git a/src/microservice/domain/repository/mongoose/places-mongoose.repository.ts b/src/microservice/domain/repository/mongoose/places-mongoose.repository.ts new file mode 100644 index 0000000..b6c3fbf --- /dev/null +++ b/src/microservice/domain/repository/mongoose/places-mongoose.repository.ts @@ -0,0 +1,42 @@ +import { Logger } from '@nestjs/common'; +import { MongoError } from 'mongodb'; +import { Model } from 'mongoose'; +import { MongoDBException } from '../../../../core/error-handling/exception/mongodb-.exception'; +import { Place } from '../../interface/place.interface'; +import { MongooseRepository } from './mongoose.repository'; + +export abstract class PlacesMongooseRepository< + Collection extends Place, + MongooseModel +> extends MongooseRepository { + protected readonly logger: Logger = new Logger(this.constructor.name); + + constructor(protected model: Model) { + super(model); + } + + async insertOne(item: Collection, name: string): Promise { + return this.create(item).then( + () => { + this.logger.log( + `${item.constructor.name} '${name}' saved successfully!` + ); + }, + (err: MongoError) => { + this.logger.error(err.message); + throw new MongoDBException(err.message, err.code); + } + ); + } + + async findByNameOrAlias(name: string, extraSearch = {}): Promise { + const nameRegex = new RegExp(name, 'i'); + return this.model + .find({ + ...extraSearch, + $or: [{ name: nameRegex }, { alias: { $in: [nameRegex] } }] + }) + .lean() + .exec(); + } +} diff --git a/src/microservice/domain/repository/puppeteer/neighborhood/puppeteer-neighborhood.repository.ts b/src/microservice/domain/repository/puppeteer/neighborhood/puppeteer-neighborhood.repository.ts index 75faf7b..effdd75 100644 --- a/src/microservice/domain/repository/puppeteer/neighborhood/puppeteer-neighborhood.repository.ts +++ b/src/microservice/domain/repository/puppeteer/neighborhood/puppeteer-neighborhood.repository.ts @@ -1,6 +1,6 @@ import { CheerioAPI } from 'cheerio'; import { NeighborhoodsByCity } from '../../../model/neighborhoods-by-city.model'; -import { SearchNeighborhoods } from '../../../model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../model/search/search-neighborhoods-input.model'; import { PuppeteerRepository } from '../puppeteer.repository'; import { IPuppeteerNeighborhoodRepository } from '../../../interface/puppeteer/repository/puppeteer-neighborhood-repository.interface'; import { NotFoundException } from '../../../../../core/error-handling/exception/not-found.exception'; @@ -10,7 +10,7 @@ export abstract class PuppeteerNeighborhoodRepository implements IPuppeteerNeighborhoodRepository { async getNeighborhoodsByCity( - searchParams: SearchNeighborhoods + searchParams: SearchNeighborhoodsInput ): Promise { this.validateInput(searchParams); @@ -27,11 +27,11 @@ export abstract class PuppeteerNeighborhoodRepository } abstract buildElementsFromDocument( - _searchParams: SearchNeighborhoods, + _searchParams: SearchNeighborhoodsInput, _$: CheerioAPI ); abstract callEndpoint( - _searchParams: SearchNeighborhoods + _searchParams: SearchNeighborhoodsInput ): Promise; } diff --git a/src/microservice/domain/repository/puppeteer/puppeteer.repository.ts b/src/microservice/domain/repository/puppeteer/puppeteer.repository.ts index 7408986..dcd55d9 100644 --- a/src/microservice/domain/repository/puppeteer/puppeteer.repository.ts +++ b/src/microservice/domain/repository/puppeteer/puppeteer.repository.ts @@ -1,9 +1,12 @@ +import { Logger } from '@nestjs/common'; import * as cheerio from 'cheerio'; import { CheerioAPI } from 'cheerio'; import { Page } from '../../interface/puppeteer/page.interface'; -import { SearchNeighborhoods } from '../../model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../model/search/search-neighborhoods-input.model'; export abstract class PuppeteerRepository { + protected readonly logger: Logger = new Logger(this.constructor.name); + constructor(protected url: string, protected readonly page: Page) {} async getDocumentHtml(url: string): Promise { @@ -13,6 +16,7 @@ export abstract class PuppeteerRepository { } async goToUrl(url: string): Promise { + this.logger.log(`Going to page... '${url}'`); await this.page.goto(url, { waitUntil: 'networkidle0' }); @@ -23,7 +27,7 @@ export abstract class PuppeteerRepository { return this.page.evaluate(() => document.querySelector('*').outerHTML); } - validateInput(searchParams: SearchNeighborhoods) { + validateInput(searchParams: SearchNeighborhoodsInput) { searchParams.validateIsAnyEmptyKey(); } } diff --git a/src/microservice/domain/schemas/city.schema.ts b/src/microservice/domain/schemas/city.schema.ts new file mode 100644 index 0000000..304ba54 --- /dev/null +++ b/src/microservice/domain/schemas/city.schema.ts @@ -0,0 +1,40 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { Place } from '../interface/place.interface'; + +export type CityDocument = City & Document; + +@Schema({ collection: 'cities' }) +export class City implements Place { + @Prop({ required: true }) + id: number; + + @Prop({ required: true }) + name: string; + + @Prop({ required: false }) + alias: string[]; + + @Prop({ required: false }) + countryCode: string; + + @Prop({ required: false }) + countryName: string; + + @Prop({ required: false }) + countryId: number; + + @Prop({ required: false }) + stateCode: string; + + @Prop({ required: false }) + stateName: string; + + @Prop({ required: false }) + stateId: number; +} + +const schema = SchemaFactory.createForClass(City); +schema.index({ name: 1, countryId: 1 }, { unique: true }); + +export const CitySchema = schema; diff --git a/src/microservice/domain/schemas/country.schema.ts b/src/microservice/domain/schemas/country.schema.ts new file mode 100644 index 0000000..976c37d --- /dev/null +++ b/src/microservice/domain/schemas/country.schema.ts @@ -0,0 +1,47 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { Place } from '../interface/place.interface'; + +export type CountryDocument = Country & Document; + +@Schema({ collection: 'countries' }) +export class Country implements Place { + @Prop({ required: true }) + id: number; + + @Prop({ required: true, unique: true }) + name: string; + + @Prop({ required: true }) + iso3: string; + + @Prop({ required: true }) + iso2: string; + + @Prop({ required: true }) + region: string; + + @Prop({ required: true }) + subregion: string; + + @Prop({ required: false, type: Object }) + // eslint-disable-next-line @typescript-eslint/ban-types + translations: Object; + + @Prop({ required: false }) + alias: string[]; + + @Prop({ required: false }) + numericCode: string; + + @Prop({ required: false }) + phoneCode: string; + + @Prop({ required: false }) + currencyName: string; + + @Prop({ required: false }) + currencySymbol: string; +} + +export const CountrySchema = SchemaFactory.createForClass(Country); diff --git a/src/microservice/domain/schemas/neighborhood.schema.ts b/src/microservice/domain/schemas/neighborhood.schema.ts index 66197ed..725dda6 100644 --- a/src/microservice/domain/schemas/neighborhood.schema.ts +++ b/src/microservice/domain/schemas/neighborhood.schema.ts @@ -1,21 +1,41 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; +import { Place } from '../interface/place.interface'; export type NeighborhoodDocument = Neighborhood & Document; @Schema() -export class Neighborhood { +export class Neighborhood implements Place { + id: number; + + alias: string[]; + @Prop({ required: true }) country: string; + @Prop({ required: true }) + countryId: number; + @Prop({ required: true }) state: string; + @Prop({ required: true }) + stateName: string; + + @Prop({ required: true }) + stateId: number; + @Prop({ required: true }) city: string; + @Prop({ required: true }) + cityId: number; + @Prop({ required: true }) name: string; } -export const NeighborhoodSchema = SchemaFactory.createForClass(Neighborhood); +const schema = SchemaFactory.createForClass(Neighborhood); +schema.index({ name: 1, country: 1, state: 1, city: 1 }, { unique: true }); + +export const NeighborhoodSchema = schema; diff --git a/src/microservice/domain/schemas/state.schema.ts b/src/microservice/domain/schemas/state.schema.ts new file mode 100644 index 0000000..6350c6d --- /dev/null +++ b/src/microservice/domain/schemas/state.schema.ts @@ -0,0 +1,43 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { Place } from '../interface/place.interface'; + +export type StateDocument = State & Document; + +@Schema() +export class State implements Place { + @Prop({ required: true }) + id: number; + + @Prop({ required: true }) + name: string; + + @Prop({ required: true }) + iso2: string; + + @Prop({ required: true }) + region: string; + + @Prop({ required: true }) + subregion: string; + + @Prop({ required: false }) + alias: string[]; + + @Prop({ required: false }) + stateCode: string; + + @Prop({ required: false }) + countryCode: string; + + @Prop({ required: false }) + countryName: string; + + @Prop({ required: false }) + countryId: number; +} + +const schema = SchemaFactory.createForClass(State); +schema.index({ name: 1, countryId: 1 }, { unique: true }); + +export const StateSchema = schema; diff --git a/src/microservice/domain/service/cities/cities.service.ts b/src/microservice/domain/service/cities/cities.service.ts new file mode 100644 index 0000000..740566f --- /dev/null +++ b/src/microservice/domain/service/cities/cities.service.ts @@ -0,0 +1,8 @@ +import { CitiesMongoose } from '../../../adapter/repository/cities/cities-mongoose.repository'; +import { AbstractService } from '../abstract-service.service'; + +export abstract class CitiesService extends AbstractService { + constructor(protected readonly mongoRepository: CitiesMongoose) { + super(); + } +} diff --git a/src/microservice/domain/service/cities/get-city-by-name-or-alias.service.ts b/src/microservice/domain/service/cities/get-city-by-name-or-alias.service.ts new file mode 100644 index 0000000..98ce13a --- /dev/null +++ b/src/microservice/domain/service/cities/get-city-by-name-or-alias.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { CitiesMongoose } from '../../../adapter/repository/cities/cities-mongoose.repository'; +import { City } from '../../schemas/city.schema'; +import { CitiesService } from './cities.service'; + +@Injectable() +export class GetCityByNameOrAliasService extends CitiesService { + constructor(mongoRepository: CitiesMongoose) { + super(mongoRepository); + } + + async getCityByNameOrAlias( + name: string, + countryId: number, + stateId: number + ): Promise { + return this.mongoRepository.findByNameOrAlias(name, { countryId, stateId }); + } +} diff --git a/src/microservice/domain/service/countries/countries.service.ts b/src/microservice/domain/service/countries/countries.service.ts new file mode 100644 index 0000000..3bdcb71 --- /dev/null +++ b/src/microservice/domain/service/countries/countries.service.ts @@ -0,0 +1,8 @@ +import { CountriesMongoose } from 'src/microservice/adapter/repository/countries/countries-mongoose.repository'; +import { AbstractService } from '../abstract-service.service'; + +export abstract class CountriesService extends AbstractService { + constructor(protected readonly mongoRepository: CountriesMongoose) { + super(); + } +} diff --git a/src/microservice/domain/service/countries/get-country-by-name-or-alias.service.ts b/src/microservice/domain/service/countries/get-country-by-name-or-alias.service.ts new file mode 100644 index 0000000..2e78e4e --- /dev/null +++ b/src/microservice/domain/service/countries/get-country-by-name-or-alias.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { CountriesMongoose } from '../../../adapter/repository/countries/countries-mongoose.repository'; +import { Country } from '../../schemas/country.schema'; +import { CountriesService } from './countries.service'; + +@Injectable() +export class GetCountryByNameOrAliasService extends CountriesService { + constructor(mongoRepository: CountriesMongoose) { + super(mongoRepository); + } + + async getCountryByNameOrAlias(name: string): Promise { + return this.mongoRepository.findByNameOrAlias(name); + } +} diff --git a/src/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.ts b/src/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.ts index 68bf715..725650c 100644 --- a/src/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.ts +++ b/src/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.ts @@ -1,10 +1,19 @@ import { Inject, Injectable } from '@nestjs/common'; import { NeighborhoodsByCity } from '../../model/neighborhoods-by-city.model'; -import { SearchNeighborhoods } from '../../model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../model/search/search-neighborhoods-input.model'; import { NeighborhoodsMongoose } from '../../../adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; import { GuiaMaisRepository } from '../../../adapter/repository/neighborhoods/puppeteer/guia-mais.repository'; import { SaveNeighborhoodsByCityService } from './save-neighborhoods-by-city.service'; import { NeighborhoodsService } from './neighborhoods.service'; +import { GetCountryByNameOrAliasService } from '../countries/get-country-by-name-or-alias.service'; +import { InvalidDataException } from '../../../../core/error-handling/exception/invalid-data.exception'; +import { GetStateByNameOrAliasService } from '../states/get-state-by-name-or-alias.service'; +import { Country } from '../../schemas/country.schema'; +import { State } from '../../schemas/state.schema'; +import { ValidOutputSearchNeighborhood } from '../../interface/valid-output-search/valid-outpu-search-neighborhood.interface'; +import { GetCityByNameOrAliasService } from '../cities/get-city-by-name-or-alias.service'; +import { City } from '../../schemas/city.schema'; +import { SearchNeighborhoodsDB } from '../../model/search/search-neighborhoods-db.model'; @Injectable() export class GetNeighborhoodsByCityService extends NeighborhoodsService { @@ -12,6 +21,9 @@ export class GetNeighborhoodsByCityService extends NeighborhoodsService { @Inject('GuiaMaisRepository') private readonly guiaMaisRepository: GuiaMaisRepository, private readonly saveNeighborhoodsService: SaveNeighborhoodsByCityService, + private readonly getCountryService: GetCountryByNameOrAliasService, + private readonly getStateService: GetStateByNameOrAliasService, + private readonly getCityService: GetCityByNameOrAliasService, mongoRepository: NeighborhoodsMongoose ) { super(mongoRepository); @@ -22,11 +34,17 @@ export class GetNeighborhoodsByCityService extends NeighborhoodsService { state: string, city: string ): Promise { - const searchParams = new SearchNeighborhoods(country, state, city); + const searchParams = new SearchNeighborhoodsInput(country, state, city); - const resMongo = await this.findInDatabase(searchParams); + const convertedSearch = await this.validateAndConvertSearchParams( + searchParams + ); - if (!resMongo.length) { + const resMongo = await this.findNeighborhoodsByCityInDatabase( + convertedSearch + ); + + if (resMongo.length === 0) { this.logger.log('Searching by puppeteer...'); const resPuppeteer = await this.guiaMaisRepository.getNeighborhoodsByCity( searchParams @@ -34,7 +52,8 @@ export class GetNeighborhoodsByCityService extends NeighborhoodsService { await this.saveNeighborhoodsService.saveNeighborhoodsByCity( resPuppeteer, - searchParams + searchParams, + convertedSearch ); this.logger.log('Returning Puppeteer response...'); @@ -46,4 +65,69 @@ export class GetNeighborhoodsByCityService extends NeighborhoodsService { return resMongo; } + + async validateAndConvertSearchParams( + searchParams: SearchNeighborhoodsInput + ): Promise { + const country = await this.validateCountry(searchParams.country); + const state = await this.validateState(searchParams.state, country.id); + const city = await this.validateCity( + searchParams.city, + country.id, + state.id + ); + return { country, state, city }; + } + + async validateCountry(country: string): Promise { + this.logger.log(`Validating Country '${country}'...`); + + const res = await this.getCountryService.getCountryByNameOrAlias(country); + if (res.length === 0) throw new InvalidDataException('Country', country); + + this.logger.log(`Country: '${res[0].name}'`); + return res[0]; + } + + async validateState(state: string, countryId: number): Promise { + this.logger.log(`Validating State '${state}'...`); + + const res = await this.getStateService.getStateByNameOrAlias( + state, + countryId + ); + if (res.length === 0) throw new InvalidDataException('State', state); + + this.logger.log(`State: '${res[0].name}'`); + return res[0]; + } + + async validateCity( + city: string, + countryId: number, + stateId: number + ): Promise { + this.logger.log(`Validating City '${city}'...`); + + const res = await this.getCityService.getCityByNameOrAlias( + city, + countryId, + stateId + ); + if (res.length === 0) throw new InvalidDataException('City', city); + + this.logger.log(`City: '${res[0].name}'`); + return res[0]; + } + + async findNeighborhoodsByCityInDatabase( + convertedSearch: ValidOutputSearchNeighborhood + ): Promise { + const searchDB = new SearchNeighborhoodsDB( + convertedSearch.country.id, + convertedSearch.state.id, + convertedSearch.city.id + ); + return this.findInDatabase(searchDB); + } } diff --git a/src/microservice/domain/service/neighborhoods/neighborhoods.service.ts b/src/microservice/domain/service/neighborhoods/neighborhoods.service.ts index 9e3ec1b..edf1a73 100644 --- a/src/microservice/domain/service/neighborhoods/neighborhoods.service.ts +++ b/src/microservice/domain/service/neighborhoods/neighborhoods.service.ts @@ -1,7 +1,7 @@ -import { NeighborhoodsPuppeteerBuilder } from '../../../adapter/helper/builder/neighborhoods-puppeteer.builder'; +import { NeighborhoodsPuppeteerBuilder } from '../../../adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder'; import { NeighborhoodsMongoose } from '../../../adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; import { NeighborhoodsByCity } from '../../model/neighborhoods-by-city.model'; -import { SearchNeighborhoods } from '../../model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsDB } from '../../model/search/search-neighborhoods-db.model'; import { AbstractService } from '../abstract-service.service'; export abstract class NeighborhoodsService extends AbstractService { @@ -10,10 +10,10 @@ export abstract class NeighborhoodsService extends AbstractService { } async findInDatabase( - searchParams: SearchNeighborhoods + searchParamsDB: SearchNeighborhoodsDB ): Promise { - this.logger.log('Searching in database...'); - const res = await this.mongoRepository.findBySearchParams(searchParams); + this.logger.log('Searching Neighborhood in database...'); + const res = await this.mongoRepository.findBySearchParams(searchParamsDB); return new NeighborhoodsPuppeteerBuilder(res).build(); } } diff --git a/src/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.ts b/src/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.ts index 8566e41..1e85c47 100644 --- a/src/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.ts +++ b/src/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.ts @@ -1,9 +1,12 @@ import { Injectable } from '@nestjs/common'; import { NeighborhoodsByCity } from '../../model/neighborhoods-by-city.model'; import { NeighborhoodsMongoose } from '../../../adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; -import { NeighborhoodsMongoBuilder } from '../../../adapter/helper/builder/neighborhoods-mongo.builder'; -import { SearchNeighborhoods } from '../../model/search/search-neighborhoods.model'; +import { NeighborhoodsMongoBuilder } from '../../../adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder'; +import { SearchNeighborhoodsInput } from '../../model/search/search-neighborhoods-input.model'; import { NeighborhoodsService } from './neighborhoods.service'; +import { ValidOutputSearchNeighborhood } from '../../interface/valid-output-search/valid-outpu-search-neighborhood.interface'; +import { Neighborhood } from '../../schemas/neighborhood.schema'; +import { SearchNeighborhoodsDBBuilder } from '../../../adapter/helper/builder/neighborhoods/search-neighborhoods-db.builder'; @Injectable() export class SaveNeighborhoodsByCityService extends NeighborhoodsService { @@ -13,30 +16,48 @@ export class SaveNeighborhoodsByCityService extends NeighborhoodsService { async saveNeighborhoodsByCity( neighborhoodsPuppeteer: NeighborhoodsByCity[], - searchParams: SearchNeighborhoods + searchParams: SearchNeighborhoodsInput, + convertedSearch: ValidOutputSearchNeighborhood ): Promise { const arrDocument = new NeighborhoodsMongoBuilder( neighborhoodsPuppeteer - ).build(searchParams); + ).build(convertedSearch); - arrDocument.forEach(async (item) => { + for await (const item of arrDocument) { const responseDB = await this.findNeighborhoodInDatabase( - searchParams, + convertedSearch, item.name ); - if (responseDB.length === 0) { - this.logger.log(`Saving neighborhood '${item.name}'...`); - await this.mongoRepository.insert(item); - } - }); + if (responseDB.length === 0) + await this.createNeighborhood(item, convertedSearch); + } + } + + async createNeighborhood( + item: Neighborhood, + convertedSearch: ValidOutputSearchNeighborhood + ) { + item.countryId = convertedSearch.country.id; + item.country = convertedSearch.country.name.capitalize(); + item.stateId = convertedSearch.state.id; + item.state = convertedSearch.state.stateCode.toUpperCase(); + item.stateName = convertedSearch.state.name.capitalize(); + item.cityId = convertedSearch.city.id; + item.city = convertedSearch.city.name.capitalize(); + this.logger.log(`Saving neighborhood '${item.name}'...`); + await this.mongoRepository.insertOne(item, item.name); } async findNeighborhoodInDatabase( - searchParams: SearchNeighborhoods, + convertedSearch: ValidOutputSearchNeighborhood, name: string ) { + const searchParams = new SearchNeighborhoodsDBBuilder( + convertedSearch + ).build(); searchParams.name = name; + return this.findInDatabase(searchParams); } } diff --git a/src/microservice/domain/service/states/get-state-by-name-or-alias.service.ts b/src/microservice/domain/service/states/get-state-by-name-or-alias.service.ts new file mode 100644 index 0000000..b130815 --- /dev/null +++ b/src/microservice/domain/service/states/get-state-by-name-or-alias.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { StatesMongoose } from '../../../adapter/repository/states/states-mongoose.repository'; +import { State } from '../../schemas/state.schema'; +import { StatesService } from './states.service'; + +@Injectable() +export class GetStateByNameOrAliasService extends StatesService { + constructor(mongoRepository: StatesMongoose) { + super(mongoRepository); + } + + async getStateByNameOrAlias( + name: string, + countryId: number + ): Promise { + return this.mongoRepository.findByNameOrAlias(name, { countryId }); + } +} diff --git a/src/microservice/domain/service/states/states.service.ts b/src/microservice/domain/service/states/states.service.ts new file mode 100644 index 0000000..9108bb7 --- /dev/null +++ b/src/microservice/domain/service/states/states.service.ts @@ -0,0 +1,8 @@ +import { StatesMongoose } from '../../../adapter/repository/states/states-mongoose.repository'; +import { AbstractService } from '../abstract-service.service'; + +export abstract class StatesService extends AbstractService { + constructor(protected readonly mongoRepository: StatesMongoose) { + super(); + } +} diff --git a/test/e2e/app.e2e-spec.ts b/test/e2e/app.e2e-spec.ts index c0c1549..d6ea995 100644 --- a/test/e2e/app.e2e-spec.ts +++ b/test/e2e/app.e2e-spec.ts @@ -1,48 +1,36 @@ -// import { INestApplication } from '@nestjs/common'; -// import * as request from 'supertest'; -// import { expect } from 'chai'; -// import { AppModule } from '../../src/app.module'; -// import { Test } from '@nestjs/testing'; -// import { Connection } from 'mongoose'; -// import { getConnectionToken } from '@nestjs/mongoose'; - -// jest.useFakeTimers(); -// jest.setTimeout(25000); - -// describe('App (e2e)', () => { -// let app: INestApplication; -// let connection: Connection; - -// // const url = `mongodb://dev-seeder-root:hpYpxyjrKExNQviu@devseeder-shard-00-00.v6xtv.mongodb.net:27017,devseeder-shard-00-01.v6xtv.mongodb.net:27017,devseeder-shard-00-02.v6xtv.mongodb.net:27017/places?ssl=true&replicaSet=atlas-4xi3m3-shard-0&authSource=admin&retryWrites=true&w=majority`; - -// beforeAll(async () => { -// const module = await Test.createTestingModule({ -// imports: [AppModule] -// }).compile(); - -// connection = await module.get(getConnectionToken()); - -// app = await module.createNestApplication(); -// await app.init(); -// }); - -// afterEach(async () => { -// await app.close(); -// }); - -// afterAll(async () => { -// await connection.close(/*force:*/ true); // <-- important -// }); - -// it('1 = 1', async () => { -// expect(1).to.be.equal(1); -// }); - -// it('/neighborhoods/city/brasil/sc/orleans (GET)', async () => { -// const actual = await request(app.getHttpServer()) -// .get('/neighborhoods/city/brasil/sc/orleans') -// .expect(200); - -// expect(actual.body).to.be.an('array').that.is.not.empty; -// }); -// }); +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import * as request from 'supertest'; +import { expect } from 'chai'; +import { AppModule } from '../../src/app.module'; +import { NestFactory } from '@nestjs/core'; +import '../../src/microservice/adapter/helper/extensions/exensions.module'; + +jest.setTimeout(50000); + +describe('App (e2e) ', () => { + let app: INestApplication; + + beforeEach(async function () { + app = await NestFactory.create(AppModule); + app.useGlobalPipes( + new ValidationPipe({ + transform: true + }) + ); + await app.init(); + }); + + afterEach(async function () { + await app.close(); + }); + + describe('Neighborhood (e2e) ', () => { + it('/neighborhoods/city/brasil/sc/orleans (GET)', async () => { + const actual = await request(app.getHttpServer()).get( + '/neighborhoods/city/brasil/sc/orleans' + ); + + expect(actual.body).to.be.an('array').that.is.not.empty; + }); + }); +}); diff --git a/test/mock/mongoose/mock-mongoose.ts b/test/mock/mongoose/mock-mongoose.ts index a66452e..b5ef4df 100644 --- a/test/mock/mongoose/mock-mongoose.ts +++ b/test/mock/mongoose/mock-mongoose.ts @@ -7,8 +7,33 @@ export const mockModelMongoose = { findAll: () => { return { exec: jest.fn(() => null) }; }, - create: jest.fn(() => null), + create: jest.fn( + () => + new Promise(async (resolve) => { + resolve({}); + }) + ), findByIdAndUpdate: () => { return { exec: jest.fn(() => null) }; } }; + +export const mockMongooseConnection = () => { + return { + createConnection: jest.fn(() => { + return { + asPromise: jest.fn(() => { + return { + model: jest.fn(), + close: jest.fn() + }; + }) + }; + }), + Schema: jest.fn(() => { + return { + index: jest.fn() + }; + }) + }; +}; diff --git a/test/unit/core/error-handling/exception/invalid-data.exception.spec.ts b/test/unit/core/error-handling/exception/invalid-data.exception.spec.ts new file mode 100644 index 0000000..5af6f89 --- /dev/null +++ b/test/unit/core/error-handling/exception/invalid-data.exception.spec.ts @@ -0,0 +1,13 @@ +import '../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { expect } from 'chai'; +import { HttpStatus } from '@nestjs/common'; +import { InvalidDataException } from '../../../../../src/core/error-handling/exception/invalid-data.exception'; + +describe('InvalidDataExeception ', () => { + it('Should call instanciate InvalidDataExeception correctly', function () { + const exception = new InvalidDataException('any', 'any_value'); + expect(exception.message).to.be.equal(`Invalid Any 'any_value'`); + expect(exception.getStatus()).to.be.equal(HttpStatus.NOT_ACCEPTABLE); + expect(exception.errCode).to.be.equal(3); + }); +}); diff --git a/test/unit/core/error-handling/filter/custom-error-exception.filter.spec.ts b/test/unit/core/error-handling/filter/custom-error-exception.filter.spec.ts index 836c8c7..953984f 100644 --- a/test/unit/core/error-handling/filter/custom-error-exception.filter.spec.ts +++ b/test/unit/core/error-handling/filter/custom-error-exception.filter.spec.ts @@ -11,31 +11,26 @@ import * as sinon from 'sinon'; import { NestFactory } from '@nestjs/core'; import { AppModule } from '../../../../../src/app.module'; import { Neighborhood } from '../../../../../src/microservice/domain/schemas/neighborhood.schema'; -import { mockModelMongoose } from '../../../../mock/mongoose/mock-mongoose'; +import { + mockModelMongoose, + mockMongooseConnection +} from '../../../../mock/mongoose/mock-mongoose'; import { getModelToken } from '@nestjs/mongoose'; +import { State } from '../../../../../src/microservice/domain/schemas/state.schema'; +import { Country } from '../../../../../src/microservice/domain/schemas/country.schema'; +import { City } from '../../../../../src/microservice/domain/schemas/city.schema'; jest.setTimeout(25000); -jest.mock('mongoose', () => { - return { - createConnection: jest.fn(() => { - return { - asPromise: jest.fn(() => { - return { - model: jest.fn(), - close: jest.fn() - }; - }) - }; - }), - Schema: jest.fn() - }; -}); describe('CustomErrorExceptionFilter', () => { let sut: CustomErrorExceptionFilter; let app: TestingModule; let server: INestApplication; + beforeAll(async () => { + jest.mock('mongoose', mockMongooseConnection); + }); + beforeEach(async () => { app = await Test.createTestingModule({ imports: [ExtensionsModule, FiltersModule], @@ -44,6 +39,12 @@ describe('CustomErrorExceptionFilter', () => { }) .overrideProvider(getModelToken(Neighborhood.name)) .useValue(mockModelMongoose) + .overrideProvider(getModelToken(Country.name)) + .useValue(mockModelMongoose) + .overrideProvider(getModelToken(State.name)) + .useValue(mockModelMongoose) + .overrideProvider(getModelToken(City.name)) + .useValue(mockModelMongoose) .compile(); sut = app.get(CustomErrorExceptionFilter); diff --git a/test/unit/core/error-handling/filter/mongodb-exception.filter.spec.ts b/test/unit/core/error-handling/filter/mongodb-exception.filter.spec.ts new file mode 100644 index 0000000..9f31e47 --- /dev/null +++ b/test/unit/core/error-handling/filter/mongodb-exception.filter.spec.ts @@ -0,0 +1,40 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ExtensionsModule } from '../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { expect } from 'chai'; +import { FiltersModule } from '../../../../../src/core/error-handling/filters.module'; +import { MongoDBExceptionFilter } from '../../../../../src/core/error-handling/filter/mongodb-exception.filter'; +import { HttpStatus } from '@nestjs/common'; +import { MongoDBException } from '../../../../../src/core/error-handling/exception/mongodb-.exception'; + +describe('MongoDBExceptionFilter', () => { + let sut: MongoDBExceptionFilter; + let app: TestingModule; + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [ExtensionsModule, FiltersModule], + controllers: [], + providers: [MongoDBExceptionFilter] + }).compile(); + + sut = app.get(MongoDBExceptionFilter); + }); + + afterEach(async () => { + await app.close(); + }); + + describe('makeCustomResponse', () => { + it('should call makeCustomResponse and return the correct response', async () => { + const mockException = new MongoDBException('mongo error', 4); + const mockResponse = { + status: HttpStatus.BAD_REQUEST, + message: 'mongo error', + type: 'MongoDBException', + errorCode: 4 + }; + const actual = await sut.makeCustomResponse(mockException); + expect(JSON.stringify(actual)).to.be.equal(JSON.stringify(mockResponse)); + }); + }); +}); diff --git a/test/unit/core/http/transform-response.interceptor.spec.ts b/test/unit/core/http/transform-response.interceptor.spec.ts index f2ab749..61d919f 100644 --- a/test/unit/core/http/transform-response.interceptor.spec.ts +++ b/test/unit/core/http/transform-response.interceptor.spec.ts @@ -15,23 +15,9 @@ import { NestResponseBuilder } from '../../../../src/core/http/nest-response.bui import { CustomResponse } from '../../../../src/core/interface/custom-response.interface'; import { of } from 'rxjs'; import * as sinon from 'sinon'; +import { mockMongooseConnection } from '../../../mock/mongoose/mock-mongoose'; jest.setTimeout(22000); -jest.mock('mongoose', () => { - return { - createConnection: jest.fn(() => { - return { - asPromise: jest.fn(() => { - return { - model: jest.fn(), - close: jest.fn() - }; - }) - }; - }), - Schema: jest.fn() - }; -}); describe('TransformResponseInterceptor ', () => { let app: INestApplication; @@ -41,6 +27,10 @@ describe('TransformResponseInterceptor ', () => { handle: jest.fn(() => of([mockNestResponse()])) }; + beforeAll(async () => { + jest.mock('mongoose', mockMongooseConnection); + }); + beforeEach(async function () { app = await NestFactory.create(AppModule); app.useGlobalPipes( diff --git a/test/unit/microservice/adapter/helper/builder/neighborhoods-mongo.builder.spec.ts b/test/unit/microservice/adapter/helper/builder/neighborhoods-mongo.builder.spec.ts deleted file mode 100644 index 7d400e7..0000000 --- a/test/unit/microservice/adapter/helper/builder/neighborhoods-mongo.builder.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; -import { expect } from 'chai'; -import { NeighborhoodsMongoBuilder } from '../../../../../../src/microservice/adapter/helper/builder/neighborhoods-mongo.builder'; -import { SearchNeighborhoods } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; -import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; - -const mockNeighborhoodsByCity = [ - { - name: 'Harlem', - city: 'New York City - NY' - }, - { - name: `Hell's Kitchen`, - city: 'New York City - NY' - } -]; - -const mockMongoNeighborhoods = () => { - const arr = []; - const item1 = new Neighborhood(); - item1.country = 'USA'; - item1.state = 'NY'; - item1.city = 'New York City'; - item1.name = 'Harlem'; - arr.push(item1); - - const item2 = new Neighborhood(); - item2.country = 'USA'; - item2.state = 'NY'; - item2.city = 'New York City'; - item2.name = `Hell's Kitchen`; - arr.push(item2); - - return arr; -}; - -describe('NeighborhoodsMongoBuilder ', () => { - it('Should instanciate NeighborhoodsMongoBuilder and build correctly', function () { - const nestBuilder = new NeighborhoodsMongoBuilder(mockNeighborhoodsByCity); - const mockSearch = new SearchNeighborhoods('USA', 'NY', 'New York City'); - const actual = nestBuilder.build(mockSearch); - expect(JSON.stringify(actual)).to.be.equal( - JSON.stringify(mockMongoNeighborhoods()) - ); - }); -}); diff --git a/test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.spec.ts b/test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.spec.ts new file mode 100644 index 0000000..ed03916 --- /dev/null +++ b/test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder.spec.ts @@ -0,0 +1,62 @@ +import '../../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { expect } from 'chai'; +import { NeighborhoodsMongoBuilder } from '../../../../../../../src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-mongo.builder'; +import { Neighborhood } from '../../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { Country } from '../../../../../../../src/microservice/domain/schemas/country.schema'; +import { State } from '../../../../../../../src/microservice/domain/schemas/state.schema'; +import { City } from '../../../../../../../src/microservice/domain/schemas/city.schema'; + +const mockNeighborhoodsByCity = [ + { + name: 'Harlem', + city: 'New York City - NY' + }, + { + name: `Hell's Kitchen`, + city: 'New York City - NY' + } +]; + +const mockMongoNeighborhoods = () => { + const arr = []; + const item1 = new Neighborhood(); + item1.country = 'USA'; + item1.state = 'NY'; + item1.city = 'New York City'; + item1.name = 'Harlem'; + arr.push(item1); + + const item2 = new Neighborhood(); + item2.country = 'USA'; + item2.state = 'NY'; + item2.city = 'New York City'; + item2.name = `Hell's Kitchen`; + arr.push(item2); + + return arr; +}; + +const mockConvertedSearch = () => { + const mockCountry = new Country(); + mockCountry.name = 'USA'; + const mockState = new State(); + mockState.name = 'New York'; + mockState.stateCode = 'NY'; + const mockCity = new City(); + mockCity.name = 'New York City'; + return { + country: mockCountry, + state: mockState, + city: mockCity + }; +}; + +describe('NeighborhoodsMongoBuilder ', () => { + it('Should instanciate NeighborhoodsMongoBuilder and build correctly', function () { + const nestBuilder = new NeighborhoodsMongoBuilder(mockNeighborhoodsByCity); + const actual = nestBuilder.build(mockConvertedSearch()); + expect(JSON.stringify(actual)).to.be.equal( + JSON.stringify(mockMongoNeighborhoods()) + ); + }); +}); diff --git a/test/unit/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.spec.ts b/test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.spec.ts similarity index 75% rename from test/unit/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.spec.ts rename to test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.spec.ts index 81393a2..4461785 100644 --- a/test/unit/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder.spec.ts +++ b/test/unit/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder.spec.ts @@ -1,7 +1,7 @@ -import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import '../../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; import { expect } from 'chai'; -import { NeighborhoodsPuppeteerBuilder } from '../../../../../../src/microservice/adapter/helper/builder/neighborhoods-puppeteer.builder'; -import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { Neighborhood } from '../../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { NeighborhoodsPuppeteerBuilder } from '../../../../../../../src/microservice/adapter/helper/builder/neighborhoods/neighborhoods-puppeteer.builder'; const mockNeighborhoodsByCity = [ { diff --git a/test/unit/microservice/adapter/helper/extensions/object.extension.spec.ts b/test/unit/microservice/adapter/helper/extensions/object.extension.spec.ts index 53bedea..101ca95 100644 --- a/test/unit/microservice/adapter/helper/extensions/object.extension.spec.ts +++ b/test/unit/microservice/adapter/helper/extensions/object.extension.spec.ts @@ -1,6 +1,6 @@ import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; import { expect } from 'chai'; -import { SearchNeighborhoods } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods-input.model'; describe('object.extension', () => { describe('getMethods', function () { @@ -13,7 +13,7 @@ describe('object.extension', () => { describe('validateIsAnyEmptyKey', function () { it('Should call validateIsAnyEmptyKey and throw exccption', function () { const validation = () => { - const obj = new SearchNeighborhoods('brasil', 'sc', 'orleans'); + const obj = new SearchNeighborhoodsInput('brasil', 'sc', 'orleans'); obj.city = ''; obj.validateIsAnyEmptyKey(); }; diff --git a/test/unit/microservice/adapter/helper/modules/custom-puppeteer.module.spec.ts b/test/unit/microservice/adapter/helper/modules/custom-puppeteer.module.spec.ts new file mode 100644 index 0000000..f321a52 --- /dev/null +++ b/test/unit/microservice/adapter/helper/modules/custom-puppeteer.module.spec.ts @@ -0,0 +1,17 @@ +import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { expect } from 'chai'; +import { CustomPuppeteerModule } from '../../../../../../src/microservice/adapter/helper/modules/custom-puppeteer.module'; + +describe('CustomPuppeteerModule ', () => { + it('Should call CustomPuppeteerModule.forRootLaunchOptions with null and set isGlobal false', function () { + const actual = CustomPuppeteerModule.forRootLaunchOptions(null); + expect(actual.global).to.be.equal(false); + }); + + it('Should call CustomPuppeteerModule.forRootLaunchOptions with obj and set isGlobal true', function () { + const actual = CustomPuppeteerModule.forRootLaunchOptions({ + isGlobal: true + }); + expect(actual.global).to.be.equal(true); + }); +}); diff --git a/test/unit/microservice/adapter/neighborhoods.module.spec.ts b/test/unit/microservice/adapter/neighborhoods.module.spec.ts index 3a3b0c0..9e10a00 100644 --- a/test/unit/microservice/adapter/neighborhoods.module.spec.ts +++ b/test/unit/microservice/adapter/neighborhoods.module.spec.ts @@ -7,6 +7,12 @@ import { NeighborhoodsMongoose } from '../../../../src/microservice/adapter/repo import { getModelToken } from '@nestjs/mongoose'; import { Neighborhood } from '../../../../src/microservice/domain/schemas/neighborhood.schema'; import { mockModelMongoose } from '../../../mock/mongoose/mock-mongoose'; +import { Country } from '../../../../src/microservice/domain/schemas/country.schema'; +import { CountriesMongoose } from '../../../../src/microservice/adapter/repository/countries/countries-mongoose.repository'; +import { State } from '../../../../src/microservice/domain/schemas/state.schema'; +import { City } from '../../../../src/microservice/domain/schemas/city.schema'; +import { StatesMongoose } from '../../../../src/microservice/adapter/repository/states/states-mongoose.repository'; +import { CitiesMongoose } from '../../../../src/microservice/adapter/repository/cities/cities-mongoose.repository'; describe('NeighborhoodsModule', () => { let sut: NeighborhoodsController; @@ -18,7 +24,7 @@ describe('NeighborhoodsModule', () => { } }; - const mockMongooseRepository = { + const mockNeighborhoodsMongooseRepository = { findBySearchParams: () => { return []; }, @@ -27,6 +33,12 @@ describe('NeighborhoodsModule', () => { } }; + const mockPlacesMongooseRepository = { + findByNameOrAlias: () => { + return []; + } + }; + beforeEach(async function () { app = await Test.createTestingModule({ imports: [NeighborhoodsModule, ExtensionsModule], @@ -35,9 +47,21 @@ describe('NeighborhoodsModule', () => { .overrideProvider('GuiaMaisRepository') .useValue(mockGuiaMaisRepository) .overrideProvider(NeighborhoodsMongoose) - .useValue(mockMongooseRepository) + .useValue(mockNeighborhoodsMongooseRepository) + .overrideProvider(CountriesMongoose) + .useValue(mockPlacesMongooseRepository) + .overrideProvider(StatesMongoose) + .useValue(mockPlacesMongooseRepository) + .overrideProvider(CitiesMongoose) + .useValue(mockPlacesMongooseRepository) .overrideProvider(getModelToken(Neighborhood.name)) .useValue(mockModelMongoose) + .overrideProvider(getModelToken(Country.name)) + .useValue(mockModelMongoose) + .overrideProvider(getModelToken(State.name)) + .useValue(mockModelMongoose) + .overrideProvider(getModelToken(City.name)) + .useValue(mockModelMongoose) .compile(); sut = app.get(NeighborhoodsController); diff --git a/test/unit/microservice/adapter/repository/countries/countries-mongoose.repository.spec.ts b/test/unit/microservice/adapter/repository/countries/countries-mongoose.repository.spec.ts new file mode 100644 index 0000000..e2e199f --- /dev/null +++ b/test/unit/microservice/adapter/repository/countries/countries-mongoose.repository.spec.ts @@ -0,0 +1,71 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ExtensionsModule } from '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { CountriesMongoose } from '../../../../../../src/microservice/adapter/repository/countries/countries-mongoose.repository'; +import { getModelToken } from '@nestjs/mongoose'; +import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { mockModelMongoose } from '../../../../../mock/mongoose/mock-mongoose'; +import { Country } from '../../../../../../src/microservice/domain/schemas/country.schema'; + +jest.useFakeTimers(); +jest.setTimeout(20000); + +describe('CountriesMongoose', () => { + let sut: CountriesMongoose; + let app: TestingModule; + + const mockCountries = () => { + const arr = []; + const item1 = new Neighborhood(); + item1.country = 'USA'; + item1.state = 'NJ'; + item1.city = 'Gotham City'; + item1.name = 'Brideshead'; + arr.push(item1); + + return arr; + }; + + const mockFindCountries = { + lean: jest.fn(() => { + return { + exec: jest.fn(() => mockCountries()) + }; + }) + }; + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [ExtensionsModule], + controllers: [], + providers: [ + CountriesMongoose, + { + provide: getModelToken(Country.name), + useValue: mockModelMongoose + } + ] + }).compile(); + + sut = app.get(CountriesMongoose); + }); + + afterEach(async () => { + await app.close(); + }); + + describe('findBySearchParams', () => { + it('should call findBySearchParams and return an array', async () => { + const findManyStub = sinon + .stub(mockModelMongoose, 'find') + .returns(mockFindCountries); + + const actual = await sut.findByNameOrAlias('any'); + + expect(actual).to.be.an('array').that.is.not.empty; + + findManyStub.restore(); + }); + }); +}); diff --git a/test/unit/microservice/adapter/repository/neighborhoods-mongoose.repository.spec.ts b/test/unit/microservice/adapter/repository/neighborhoods-mongoose.repository.spec.ts deleted file mode 100644 index 9f498ac..0000000 --- a/test/unit/microservice/adapter/repository/neighborhoods-mongoose.repository.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { SearchNeighborhoods } from '../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; -import { ExtensionsModule } from '../../../../../src/microservice/adapter/helper/extensions/exensions.module'; -import { NeighborhoodsMongoose } from '../../../../../src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; -import { getModelToken } from '@nestjs/mongoose'; -import { Neighborhood } from '../../../../../src/microservice/domain/schemas/neighborhood.schema'; -import { mockModelMongoose } from '../../../../mock/mongoose/mock-mongoose'; - -jest.useFakeTimers(); -jest.setTimeout(50000); - -describe('NeighborhoodsMongoose', () => { - let sut: NeighborhoodsMongoose; - let app: TestingModule; - - const mockNeighborhoods = () => { - const arr = []; - const item1 = new Neighborhood(); - item1.country = 'USA'; - item1.state = 'NJ'; - item1.city = 'Gotham City'; - item1.name = 'Brideshead'; - arr.push(item1); - - const item2 = new Neighborhood(); - item2.country = 'USA'; - item2.state = 'NJ'; - item2.city = 'Gotham City'; - item2.name = `Bristol Township`; - arr.push(item2); - - return arr; - }; - - const mockFindNeighborhoods = { - select: jest.fn(() => { - return { - lean: jest.fn(() => { - return { - exec: jest.fn(() => mockNeighborhoods()) - }; - }) - }; - }) - }; - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: [ExtensionsModule], - controllers: [], - providers: [ - NeighborhoodsMongoose, - { - provide: getModelToken(Neighborhood.name), - useValue: mockModelMongoose - } - ] - }).compile(); - - sut = app.get(NeighborhoodsMongoose); - }); - - afterEach(async () => { - await app.close(); - }); - - describe('findBySearchParams', () => { - it('should call findBySearchParams and return an array', async () => { - const mockSearchParams = new SearchNeighborhoods( - 'USA', - 'NJ', - 'Gotham City' - ); - - const findManyStub = sinon - .stub(mockModelMongoose, 'find') - .returns(mockFindNeighborhoods); - - const actual = await sut.findBySearchParams(mockSearchParams); - - expect(actual).to.be.an('array').that.is.not.empty; - - findManyStub.restore(); - }); - }); - - describe('insert', () => { - it('should call insert and call model.create with the correct params', async () => { - const createSpy = sinon.spy(mockModelMongoose, 'create'); - - const doc = new Neighborhood(); - - await sut.insert(doc); - - sinon.assert.calledOnceWithExactly(createSpy, doc); - - createSpy.restore(); - }); - }); -}); diff --git a/test/unit/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.spec.ts b/test/unit/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.spec.ts new file mode 100644 index 0000000..3ec405d --- /dev/null +++ b/test/unit/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository.spec.ts @@ -0,0 +1,152 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ExtensionsModule } from '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { NeighborhoodsMongoose } from '../../../../../../src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; +import { getModelToken } from '@nestjs/mongoose'; +import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { mockModelMongoose } from '../../../../../mock/mongoose/mock-mongoose'; +import { MongoDBException } from '../../../../../../src/core/error-handling/exception/mongodb-.exception'; +import { SearchNeighborhoodsDB } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods-db.model'; + +jest.useFakeTimers(); +jest.setTimeout(20000); + +describe('NeighborhoodsMongoose', () => { + let sut: NeighborhoodsMongoose; + let app: TestingModule; + + const mockNeighborhoods = () => { + const arr = []; + const item1 = new Neighborhood(); + item1.country = 'USA'; + item1.state = 'NJ'; + item1.city = 'Gotham City'; + item1.name = 'Brideshead'; + arr.push(item1); + + const item2 = new Neighborhood(); + item2.country = 'USA'; + item2.state = 'NJ'; + item2.city = 'Gotham City'; + item2.name = `Bristol Township`; + arr.push(item2); + + return arr; + }; + + const mockFindNeighborhoods = { + select: jest.fn(() => { + return { + lean: jest.fn(() => { + return { + exec: jest.fn(() => mockNeighborhoods()) + }; + }) + }; + }) + }; + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [ExtensionsModule], + controllers: [], + providers: [ + NeighborhoodsMongoose, + { + provide: getModelToken(Neighborhood.name), + useValue: mockModelMongoose + } + ] + }).compile(); + + sut = app.get(NeighborhoodsMongoose); + }); + + afterEach(async () => { + await app.close(); + }); + + describe('findBySearchParams', () => { + it('should call findBySearchParams and return an array', async () => { + const mockSearchParams = new SearchNeighborhoodsDB(1, 2, 3); + + const findManyStub = sinon + .stub(mockModelMongoose, 'find') + .returns(mockFindNeighborhoods); + + const actual = await sut.findBySearchParams(mockSearchParams); + + expect(actual).to.be.an('array').that.is.not.empty; + + findManyStub.restore(); + }); + }); + + describe('buildRegexFilterQuery', () => { + it('should call buildRegexFilterQuery and return regex filter', async () => { + const mockParams = { + countryId: 1, + stateId: 2, + cityId: 3, + name: 'any' + }; + + const mockRegex = { + countryId: 1, + stateId: 2, + cityId: 3, + name: new RegExp('any', 'i') + }; + + const actual = await sut.buildRegexFilterQuery(mockParams); + + expect(JSON.stringify(actual)).to.be.equal(JSON.stringify(mockRegex)); + }); + + it('should call buildRegexFilterQuery and return regex filter with empty obj', async () => { + const actual = await sut.buildRegexFilterQuery(); + expect(JSON.stringify(actual)).to.be.equal(JSON.stringify({})); + }); + }); + + describe('insertOne', () => { + it('should call insertOne and call model.create with the correct params', async () => { + const doc = new Neighborhood(); + + const createStubMongo = sinon + .stub(mockModelMongoose, 'create') + .yields(null, () => { + return; + }); + + const createStubSpy = sinon.spy(sut, 'create'); + + await sut.insertOne(doc, 'any'); + + sinon.assert.calledOnce(createStubMongo); + sinon.assert.calledOnceWithExactly(createStubSpy, doc); + + createStubMongo.restore(); + createStubSpy.restore(); + }); + + it('should call insertOne and call model.create and throws a errors', async () => { + const doc = new Neighborhood(); + + const createStubMongo = sinon + .stub(mockModelMongoose, 'create') + .yields(new MongoDBException('any', 4), () => { + return; + }); + + try { + await sut.insertOne(doc, 'any'); + } catch (err) { + expect(err.message).to.be.equal('any'); + } + + createStubMongo.restore(); + }); + }); +}); diff --git a/test/unit/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.spec.ts b/test/unit/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.spec.ts index 2c16ed4..aca4ca6 100644 --- a/test/unit/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.spec.ts +++ b/test/unit/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository.spec.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { GuiaMaisRepository } from '../../../../../../../src/microservice/adapter/repository/neighborhoods/puppeteer/guia-mais.repository'; import { PuppeteerModule } from 'nest-puppeteer'; -import { SearchNeighborhoods } from '../../../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../../../../../src/microservice/domain/model/search/search-neighborhoods-input.model'; import * as fs from 'fs'; import { ExtensionsModule } from '../../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; import * as cheerio from 'cheerio'; @@ -56,7 +56,7 @@ describe('GuiaMaisRepository', () => { describe('getNeighborhoodsByCity', () => { it('should call getNeighborhoodsByCity and return an array', async () => { - const mockSearchParams = new SearchNeighborhoods( + const mockSearchParams = new SearchNeighborhoodsInput( 'brasil', 'sc', 'orleans' @@ -77,7 +77,7 @@ describe('GuiaMaisRepository', () => { describe('callEndpoint', () => { it('should call callEndpoint and call getDocumentHtml with the correct params', async () => { - const mockSearchParams = new SearchNeighborhoods( + const mockSearchParams = new SearchNeighborhoodsInput( 'brasil', 'sc', 'orleans' diff --git a/test/unit/microservice/domain/model/search/search-neighborhoods-input.model.spec.ts b/test/unit/microservice/domain/model/search/search-neighborhoods-input.model.spec.ts new file mode 100644 index 0000000..f95fb4a --- /dev/null +++ b/test/unit/microservice/domain/model/search/search-neighborhoods-input.model.spec.ts @@ -0,0 +1,22 @@ +import { expect } from 'chai'; +import { SearchNeighborhoodsInput } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods-input.model'; + +describe('SearchNeighborhoodsInput', () => { + it('should instance SearchNeighborhoodsInput and return the object with the correct properties', async () => { + const model = new SearchNeighborhoodsInput('brasil', 'sc', 'praia grande'); + + expect(model.country).to.be.equal('brasil'); + expect(model.state).to.be.equal('sc'); + expect(model.city).to.be.equal('praia grande'); + }); + + it('should instance SearchNeighborhoodsInput, setName and return the object with the correct properties', async () => { + const model = new SearchNeighborhoodsInput('brasil', 'sc', 'praia grande'); + model.setName('Vila Rosa'); + + expect(model.country).to.be.equal('brasil'); + expect(model.state).to.be.equal('sc'); + expect(model.city).to.be.equal('praia grande'); + expect(model.name).to.be.equal('Vila Rosa'); + }); +}); diff --git a/test/unit/microservice/domain/model/search/search-neighborhoods.model.spec.ts b/test/unit/microservice/domain/model/search/search-neighborhoods.model.spec.ts deleted file mode 100644 index 14093d1..0000000 --- a/test/unit/microservice/domain/model/search/search-neighborhoods.model.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from 'chai'; -import { SearchNeighborhoods } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; - -describe('SearchNeighborhoods', () => { - it('should instance SearchNeighborhoods and return the object with the correct properties', async () => { - const model = new SearchNeighborhoods('brasil', 'sc', 'praia grande'); - - expect(model.country).to.be.equal('brasil'); - expect(model.state).to.be.equal('sc'); - expect(model.city).to.be.equal('praia grande'); - }); - - it('should instance SearchNeighborhoods, setName and return the object with the correct properties', async () => { - const model = new SearchNeighborhoods('brasil', 'sc', 'praia grande'); - model.setName('Vila Rosa'); - - expect(model.country).to.be.equal('brasil'); - expect(model.state).to.be.equal('sc'); - expect(model.city).to.be.equal('praia grande'); - expect(model.name).to.be.equal('Vila Rosa'); - }); -}); diff --git a/test/unit/microservice/domain/service/cities/get-cities-by-city.service.spec.ts b/test/unit/microservice/domain/service/cities/get-cities-by-city.service.spec.ts new file mode 100644 index 0000000..7de9307 --- /dev/null +++ b/test/unit/microservice/domain/service/cities/get-cities-by-city.service.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { CitiesMongoose } from '../../../../../../src/microservice/adapter/repository/cities/cities-mongoose.repository'; +import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { GetCityByNameOrAliasService } from '../../../../../../src/microservice/domain/service/cities/get-city-by-name-or-alias.service'; +import { City } from '../../../../../../src/microservice/domain/schemas/city.schema'; + +describe('GetCityByNameOrAliasService', () => { + let sut: GetCityByNameOrAliasService; + + const mockCitiesMongooseRepository = { + findByNameOrAlias: () => { + return [new City()]; + } + }; + + const mockMongoCities = () => { + const arr = []; + const item1 = new City(); + arr.push(item1); + return arr; + }; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [], + controllers: [], + providers: [ + { + provide: CitiesMongoose, + useValue: mockCitiesMongooseRepository + }, + GetCityByNameOrAliasService + ] + }).compile(); + + sut = app.get(GetCityByNameOrAliasService); + }); + + describe('GetCityByNameOrAliasService', () => { + it('should call getCityByCity and return an array by mongodb', async () => { + const mongoFindStub = sinon + .stub(mockCitiesMongooseRepository, 'findByNameOrAlias') + .returns(mockMongoCities()); + + const actual = await sut.getCityByNameOrAlias('orleans', 1, 2); + + expect(JSON.stringify(actual)).to.be.equal( + JSON.stringify(mockMongoCities()) + ); + + mongoFindStub.restore(); + }); + }); +}); diff --git a/test/unit/microservice/domain/service/countries/get-neighborhoods-by-city.service.spec.ts b/test/unit/microservice/domain/service/countries/get-neighborhoods-by-city.service.spec.ts new file mode 100644 index 0000000..fa9b041 --- /dev/null +++ b/test/unit/microservice/domain/service/countries/get-neighborhoods-by-city.service.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { CountriesMongoose } from '../../../../../../src/microservice/adapter/repository/countries/countries-mongoose.repository'; +import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { GetCountryByNameOrAliasService } from '../../../../../../src/microservice/domain/service/countries/get-country-by-name-or-alias.service'; +import { Country } from '../../../../../../src/microservice/domain/schemas/country.schema'; + +describe('GetCountryByNameOrAliasService', () => { + let sut: GetCountryByNameOrAliasService; + + const mockCountriesMongooseRepository = { + findByNameOrAlias: () => { + return [new Country()]; + } + }; + + const mockMongoCountries = () => { + const arr = []; + const item1 = new Country(); + arr.push(item1); + return arr; + }; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [], + controllers: [], + providers: [ + { + provide: CountriesMongoose, + useValue: mockCountriesMongooseRepository + }, + GetCountryByNameOrAliasService + ] + }).compile(); + + sut = app.get( + GetCountryByNameOrAliasService + ); + }); + + describe('GetCountryByNameOrAliasService', () => { + it('should call getCountryByCity and return an array by mongodb', async () => { + const mongoFindStub = sinon + .stub(mockCountriesMongooseRepository, 'findByNameOrAlias') + .returns(mockMongoCountries()); + + const actual = await sut.getCountryByNameOrAlias('brasil'); + + expect(JSON.stringify(actual)).to.be.equal( + JSON.stringify(mockMongoCountries()) + ); + + mongoFindStub.restore(); + }); + }); +}); diff --git a/test/unit/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.spec.ts b/test/unit/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.spec.ts index dc3be53..dd2b873 100644 --- a/test/unit/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.spec.ts +++ b/test/unit/microservice/domain/service/neighborhoods/get-neighborhoods-by-city.service.spec.ts @@ -7,6 +7,15 @@ import { SaveNeighborhoodsByCityService } from '../../../../../../src/microservi import { NeighborhoodsMongoose } from '../../../../../../src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { GetCountryByNameOrAliasService } from '../../../../../../src/microservice/domain/service/countries/get-country-by-name-or-alias.service'; +import { Country } from '../../../../../../src/microservice/domain/schemas/country.schema'; +import { CountriesMongoose } from '../../../../../../src/microservice/adapter/repository/countries/countries-mongoose.repository'; +import { CitiesMongoose } from '../../../../../../src/microservice/adapter/repository/cities/cities-mongoose.repository'; +import { StatesMongoose } from '../../../../../../src/microservice/adapter/repository/states/states-mongoose.repository'; +import { GetCityByNameOrAliasService } from '../../../../../../src/microservice/domain/service/cities/get-city-by-name-or-alias.service'; +import { GetStateByNameOrAliasService } from '../../../../../../src/microservice/domain/service/states/get-state-by-name-or-alias.service'; +import { City } from '../../../../../../src/microservice/domain/schemas/city.schema'; +import { State } from '../../../../../../src/microservice/domain/schemas/state.schema'; describe('GetNeighborhoodsByCityService', () => { let sut: GetNeighborhoodsByCityService; @@ -17,12 +26,18 @@ describe('GetNeighborhoodsByCityService', () => { } }; - const mockMongooseRepository = { + const mockNeighborhoodsMongooseRepository = { findBySearchParams: () => { return []; } }; + const mockPlacesMongooseRepository = { + findByNameOrAlias: () => { + return []; + } + }; + const mockSaveNeighborhoodsService = { saveNeighborhoodsByCity: () => { return; @@ -32,6 +47,24 @@ describe('GetNeighborhoodsByCityService', () => { } }; + const mockGetCountryService = { + getCountryByNameOrAlias: () => { + return [new Country()]; + } + }; + + const mockGetStateService = { + getStateByNameOrAlias: () => { + return [new State()]; + } + }; + + const mockGetCityService = { + getCityByNameOrAlias: () => { + return [new City()]; + } + }; + const mockMongoNeighborhoods = () => { const arr = []; const item1 = new Neighborhood(); @@ -61,12 +94,36 @@ describe('GetNeighborhoodsByCityService', () => { }, { provide: NeighborhoodsMongoose, - useValue: mockMongooseRepository + useValue: mockNeighborhoodsMongooseRepository + }, + { + provide: CountriesMongoose, + useValue: mockPlacesMongooseRepository + }, + { + provide: StatesMongoose, + useValue: mockPlacesMongooseRepository + }, + { + provide: CitiesMongoose, + useValue: mockPlacesMongooseRepository }, { provide: SaveNeighborhoodsByCityService, useFactory: () => mockSaveNeighborhoodsService }, + { + provide: GetCountryByNameOrAliasService, + useFactory: () => mockGetCountryService + }, + { + provide: GetStateByNameOrAliasService, + useFactory: () => mockGetStateService + }, + { + provide: GetCityByNameOrAliasService, + useFactory: () => mockGetCityService + }, GetNeighborhoodsByCityService ] }).compile(); @@ -109,5 +166,53 @@ describe('GetNeighborhoodsByCityService', () => { mongoFindStub.restore(); }); + + describe('validateCountry', () => { + it('should call validateCountry and throws invalid data exception', async () => { + const getCountryStub = sinon + .stub(mockGetCountryService, 'getCountryByNameOrAlias') + .returns([]); + + try { + await sut.validateCountry('brasil'); + } catch (err) { + expect(err.message).to.be.equal(`Invalid Country 'brasil'`); + } + + getCountryStub.restore(); + }); + }); + + describe('validateState', () => { + it('should call validateState and throws invalid data exception', async () => { + const getStateStub = sinon + .stub(mockGetStateService, 'getStateByNameOrAlias') + .returns([]); + + try { + await sut.validateState('sc', 1); + } catch (err) { + expect(err.message).to.be.equal(`Invalid State 'sc'`); + } + + getStateStub.restore(); + }); + }); + + describe('validateCity', () => { + it('should call validateCity and throws invalid data exception', async () => { + const getCityStub = sinon + .stub(mockGetCityService, 'getCityByNameOrAlias') + .returns([]); + + try { + await sut.validateCity('orleans', 1, 2); + } catch (err) { + expect(err.message).to.be.equal(`Invalid City 'orleans'`); + } + + getCityStub.restore(); + }); + }); }); }); diff --git a/test/unit/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.spec.ts b/test/unit/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.spec.ts index f7188ca..99a17c0 100644 --- a/test/unit/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.spec.ts +++ b/test/unit/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service.spec.ts @@ -3,9 +3,13 @@ import * as sinon from 'sinon'; import { SaveNeighborhoodsByCityService } from '../../../../../../src/microservice/domain/service/neighborhoods/save-neighborhoods-by-city.service'; import { NeighborhoodsMongoose } from '../../../../../../src/microservice/adapter/repository/neighborhoods/neighborhoods-mongoose.repository'; import { ExtensionsModule } from '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; -import { SearchNeighborhoods } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods.model'; +import { SearchNeighborhoodsInput } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods-input.model'; import { NeighborhoodsByCity } from '../../../../../../src/microservice/domain/model/neighborhoods-by-city.model'; import { Neighborhood } from '../../../../../../src/microservice/domain/schemas/neighborhood.schema'; +import { Country } from '../../../../../../src/microservice/domain/schemas/country.schema'; +import { State } from '../../../../../../src/microservice/domain/schemas/state.schema'; +import { City } from '../../../../../../src/microservice/domain/schemas/city.schema'; +import { SearchNeighborhoodsDB } from '../../../../../../src/microservice/domain/model/search/search-neighborhoods-db.model'; describe('SaveNeighborhoodsByCityService', () => { let sut: SaveNeighborhoodsByCityService; @@ -25,7 +29,16 @@ describe('SaveNeighborhoodsByCityService', () => { findBySearchParams: () => { return []; }, - insert: () => { + insertOne: () => { + return; + }, + startTransaction: () => { + return; + }, + commit: () => { + return; + }, + rollback: () => { return; } }; @@ -37,6 +50,24 @@ describe('SaveNeighborhoodsByCityService', () => { return arr; }; + const mockConvertedSearch = () => { + const mockCountry = new Country(); + mockCountry.name = 'Brazil'; + mockCountry.id = 1; + const mockState = new State(); + mockState.name = 'Santa Catarina'; + mockState.id = 2; + mockState.stateCode = 'SC'; + const mockCity = new City(); + mockCity.name = 'Orleans'; + mockCity.id = 3; + return { + country: mockCountry, + state: mockState, + city: mockCity + }; + }; + beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ imports: [ExtensionsModule], @@ -61,11 +92,22 @@ describe('SaveNeighborhoodsByCityService', () => { .stub(sut, 'findNeighborhoodInDatabase') .returns([]); - const insertSpy = sinon.spy(mockNeighborhoodMongooseRepository, 'insert'); + const insertSpy = sinon.spy( + mockNeighborhoodMongooseRepository, + 'insertOne' + ); - const searchParams = new SearchNeighborhoods('brasil', 'sc', 'orleans'); + const searchParams = new SearchNeighborhoodsInput( + 'brasil', + 'sc', + 'orleans' + ); - await sut.saveNeighborhoodsByCity(mockNeighborhoods, searchParams); + await sut.saveNeighborhoodsByCity( + mockNeighborhoods, + searchParams, + mockConvertedSearch() + ); sinon.assert.calledTwice(insertSpy); @@ -78,11 +120,22 @@ describe('SaveNeighborhoodsByCityService', () => { .stub(sut, 'findNeighborhoodInDatabase') .returns(mockMongoNeighborhoods()); - const insertSpy = sinon.spy(mockNeighborhoodMongooseRepository, 'insert'); + const insertSpy = sinon.spy( + mockNeighborhoodMongooseRepository, + 'insertOne' + ); - const searchParams = new SearchNeighborhoods('brasil', 'sc', 'orleans'); + const searchParams = new SearchNeighborhoodsInput( + 'brasil', + 'sc', + 'orleans' + ); - await sut.saveNeighborhoodsByCity(mockNeighborhoods, searchParams); + await sut.saveNeighborhoodsByCity( + mockNeighborhoods, + searchParams, + mockConvertedSearch() + ); sinon.assert.notCalled(insertSpy); @@ -95,9 +148,12 @@ describe('SaveNeighborhoodsByCityService', () => { it('should call saveNeighborhoodsByCity and call insert twice', async () => { const findInDatabaseStub = sinon.stub(sut, 'findInDatabase').returns(); - const searchParams = new SearchNeighborhoods('brasil', 'sc', 'orleans'); + const searchParams = new SearchNeighborhoodsDB(1, 2, 3); - await sut.findNeighborhoodInDatabase(searchParams, 'Alto ParanĂ¡'); + await sut.findNeighborhoodInDatabase( + mockConvertedSearch(), + 'Alto ParanĂ¡' + ); searchParams.name = 'Alto ParanĂ¡'; diff --git a/test/unit/microservice/domain/service/states/get-states-by-city.service.spec.ts b/test/unit/microservice/domain/service/states/get-states-by-city.service.spec.ts new file mode 100644 index 0000000..e3c5912 --- /dev/null +++ b/test/unit/microservice/domain/service/states/get-states-by-city.service.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { StatesMongoose } from '../../../../../../src/microservice/adapter/repository/states/states-mongoose.repository'; +import '../../../../../../src/microservice/adapter/helper/extensions/exensions.module'; +import { GetStateByNameOrAliasService } from '../../../../../../src/microservice/domain/service/states/get-state-by-name-or-alias.service'; +import { State } from '../../../../../../src/microservice/domain/schemas/state.schema'; + +describe('GetStateByNameOrAliasService', () => { + let sut: GetStateByNameOrAliasService; + + const mockStatesMongooseRepository = { + findByNameOrAlias: () => { + return [new State()]; + } + }; + + const mockMongoStates = () => { + const arr = []; + const item1 = new State(); + arr.push(item1); + return arr; + }; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [], + controllers: [], + providers: [ + { + provide: StatesMongoose, + useValue: mockStatesMongooseRepository + }, + GetStateByNameOrAliasService + ] + }).compile(); + + sut = app.get(GetStateByNameOrAliasService); + }); + + describe('GetStateByNameOrAliasService', () => { + it('should call getStateByCity and return an array by mongodb', async () => { + const mongoFindStub = sinon + .stub(mockStatesMongooseRepository, 'findByNameOrAlias') + .returns(mockMongoStates()); + + const actual = await sut.getStateByNameOrAlias('sc', 1); + + expect(JSON.stringify(actual)).to.be.equal( + JSON.stringify(mockMongoStates()) + ); + + mongoFindStub.restore(); + }); + }); +});