diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..93bded5 --- /dev/null +++ b/.env.test @@ -0,0 +1,6 @@ +# env file for e2e testing database +DB_HOST='localhost' +DB_PORT=5432 +DB_USERNAME='user' +DB_PASSWORD='password' +DB_NAME='test-db' diff --git a/.jestrc.json b/.jestrc.json index a19db6d..ee0a3f1 100644 --- a/.jestrc.json +++ b/.jestrc.json @@ -1,13 +1,18 @@ { - "rootDir": "./src/", - "coverageDirectory": "../artifacts/coverage", "moduleFileExtensions": ["js", "json", "ts"], - "moduleNameMapper": { - "@app/(.*)$": "app/modules/$1" - }, + "rootDir": ".", + "testEnvironment": "node", + "coverageDirectory": "../tests/coverage", + "setupFilesAfterEnv": ["./tests/setupTests.ts"], + "globalSetup": "/tests/jestGlobalSetup.ts", "testRegex": ".spec.ts$", - "transform": { - ".+\\.(t|j)s$": "ts-jest" + "moduleNameMapper": { + "@modules/(.*)$": "/src/modules/$1", + "@config/(.*)$": "/src/infrastructure/configs/$1", + "@exceptions$": "/src/libs/exceptions", + "@libs/(.*)$": "/src/libs/$1" }, - "testEnvironment": "node" + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } } diff --git a/README.md b/README.md index d1fdf79..8656783 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ -_**This repo is work in progress**_ +## September update: + +There are a lot of updates to this repo lately: + +Added more code examples: + +- Added UnitOfWork +- All Domain Events can now be executed in a single database transaction using a UnitOfWork +- Added Wallet module to show an example of using a UnitOfWork together with Domain Events +- Added BDD tests example + +Refactoring: + +- Refactored Domain Events and Domain Events Handlers +- Commands are now plain objects +- Moved generic files to /libs directory +- Refactored Entity/Aggregate creation +- More small changes + +Updates in readme and code: + +- My opinion on some topics evolve over time so readme and code gets updated constantly. +- Added more resources and topics to readme # Domain-Driven Hexagon @@ -976,7 +998,12 @@ Behavioral tests can be divided in two parts: **Note**: some people try to make e2e tests faster by using in-memory or embedded databases (like [sqlite3](https://www.npmjs.com/package/sqlite3)). This makes tests faster, but reduces the reliability of those tests and should be avoided. Read more: [Don't use In-Memory Databases for Tests](https://phauer.com/2017/dont-use-in-memory-databases-tests-h2/). -Example files: // TODO +For BDD tests [Cucumber](https://cucumber.io/) with [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax can give a structure and meaning to your tests. This way even people not involved in a development can define steps needed for testing. In node.js world [jest-cucumber](https://www.npmjs.com/package/jest-cucumber) is a nice package to achieve that. + +Example files: + +- [create-user.feature](tests/user/create-user/create-user.feature) - feature file that contains Gherkin steps +- [create-user.e2e-spec.ts](tests/user/create-user/create-user.e2e-spec.ts) - spec file that executes Gherkin steps Read more: diff --git a/jest-e2e.json b/jest-e2e.json new file mode 100644 index 0000000..84a5201 --- /dev/null +++ b/jest-e2e.json @@ -0,0 +1,19 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "coverageDirectory": "./coverage", + "setupFilesAfterEnv": ["./tests/jestSetupAfterEnv.ts"], + "globalSetup": "/tests/jestGlobalSetup.ts", + "testRegex": ".e2e-spec.ts$", + "moduleNameMapper": { + "@src/(.*)$": "/src/$1", + "@modules/(.*)$": "/src/modules/$1", + "@config/(.*)$": "/src/infrastructure/configs/$1", + "@exceptions$": "/src/libs/exceptions", + "@libs/(.*)$": "/src/libs/$1" + }, + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/package-lock.json b/package-lock.json index 589c4e8..74b4d68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1839,6 +1839,70 @@ "node-fetch": "^2.3.0" } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "dev": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "dev": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "dev": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "dev": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "dev": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "dev": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "dev": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "dev": true + }, "@schematics/schematics": { "version": "0.1000.7", "resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.1000.7.tgz", @@ -1979,6 +2043,16 @@ "integrity": "sha512-D+gfFWR/YCvlrYL8lgNZO1jKgIUW+cfhxsgMOqUMYwCI+tl0htD7vCCXp/oJsIxJpxuI7zqmo3gpVQBkFCM4iA==", "dev": true }, + "@types/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -2041,12 +2115,24 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "dev": true + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "dev": true }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "@types/node": { "version": "13.13.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.28.tgz", @@ -4056,6 +4142,31 @@ } } }, + "cucumber-messages": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cucumber-messages/-/cucumber-messages-8.0.0.tgz", + "integrity": "sha512-lUnWRMjwA9+KhDec/5xRZV3Du67ISumHnVLywWQXyvzmc4P+Eqx8CoeQrBQoau3Pw1hs4kJLTDyV85hFBF00SQ==", + "dev": true, + "requires": { + "@types/uuid": "^3.4.6", + "protobufjs": "^6.8.8", + "uuid": "^3.3.3" + }, + "dependencies": { + "@types/uuid": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.10.tgz", + "integrity": "sha512-BgeaZuElf7DEYZhWYDTc/XcLZXdVgFkVSTa13BqKvbnmUrxr3TJFKofUxCtDO9UQOdhnV+HPOESdHiHKZOJV1A==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -5718,6 +5829,17 @@ "assert-plus": "^1.0.0" } }, + "gherkin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-9.0.0.tgz", + "integrity": "sha512-6xoAepoxo5vhkBXjB4RCfVnSKHu5z9SqXIQVUyj+Jw8BQX8odATlee5otXgdN8llZvyvHokuvNiBeB3naEnnIQ==", + "dev": true, + "requires": { + "commander": "^4.0.1", + "cucumber-messages": "8.0.0", + "source-map-support": "^0.5.16" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -7006,6 +7128,30 @@ } } }, + "jest-cucumber": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jest-cucumber/-/jest-cucumber-3.0.1.tgz", + "integrity": "sha512-S2EelgezfwWP10VCgUkSOiJYiTIM0yM82KxrwBOn68wMmlqU5jNSf7xDIBS0tGwoFnNwUTFp7LPFmEnfilSJrA==", + "dev": true, + "requires": { + "@types/glob": "^7.1.3", + "@types/jest": "^26.0.7", + "@types/node": "^11.9.4", + "callsites": "^3.0.0", + "gherkin": "^9.0.0", + "glob": "^7.1.6", + "jest": "^26.1.0", + "uuid": "^8.2.0" + }, + "dependencies": { + "@types/node": { + "version": "11.15.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.54.tgz", + "integrity": "sha512-1RWYiq+5UfozGsU6MwJyFX6BtktcT10XRjvcAQmskCtMcW3tPske88lM/nHv7BQG1w9KBXI1zPGuu5PnNCX14g==", + "dev": true + } + } + }, "jest-diff": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", @@ -9255,6 +9401,12 @@ "chalk": "^2.4.2" } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10714,6 +10866,27 @@ "sisteransi": "^1.0.4" } }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "dev": true, + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", diff --git a/package.json b/package.json index c3250e9..2155f38 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "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/jest-e2e.json", + "test:e2e": "jest -i --forceExit --config jest-e2e.json", "test:all": "npm run test && npm run test:e2e", "typeorm": "ts-node -r tsconfig-paths/register --project ./tsconfig.json ./node_modules/.bin/typeorm", "docker:env": "docker-compose --file docker/docker-compose.yml up --build", @@ -68,6 +68,7 @@ "eslint-plugin-import": "^2.20.1", "faker": "^5.3.1", "jest": "26.4.2", + "jest-cucumber": "^3.0.1", "prettier": "^1.19.1", "supertest": "^4.0.2", "ts-jest": "26.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 7847353..961fee2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { UserModule } from 'src/modules/user/user.module'; +import { UserModule } from '@modules/user/user.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { NestEventModule } from 'nest-event'; import { ConsoleModule } from 'nestjs-console'; diff --git a/src/libs/ddd/infrastructure/mocks/generic-model-props.mock.ts b/src/libs/test-utils/mocks/generic-model-props.mock.ts similarity index 100% rename from src/libs/ddd/infrastructure/mocks/generic-model-props.mock.ts rename to src/libs/test-utils/mocks/generic-model-props.mock.ts diff --git a/src/libs/test-utils/snapshot-base-props.ts b/src/libs/test-utils/snapshot-base-props.ts new file mode 100644 index 0000000..0898d0c --- /dev/null +++ b/src/libs/test-utils/snapshot-base-props.ts @@ -0,0 +1,5 @@ +export const snapshotBaseProps = { + id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), +}; diff --git a/src/libs/utils/remove-undefined-props.util.ts b/src/libs/utils/remove-undefined-props.util.ts new file mode 100644 index 0000000..0d527ad --- /dev/null +++ b/src/libs/utils/remove-undefined-props.util.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +/** + * Remove undefined properties from an object + */ +export function removeUndefinedProps(item: any): any { + // TODO: make recursive for nested objects + const filtered: any = {}; + for (const key of Object.keys(item)) { + if (item[key]) filtered[key] = item[key]; + } + return filtered; +} diff --git a/src/main.ts b/src/main.ts index 4b4f1ca..70c25fd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-classes-per-file */ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; diff --git a/src/modules/user/commands/create-user/create-user.request.dto.ts b/src/modules/user/commands/create-user/create-user.request.dto.ts index 12c4b74..dfe3dd8 100644 --- a/src/modules/user/commands/create-user/create-user.request.dto.ts +++ b/src/modules/user/commands/create-user/create-user.request.dto.ts @@ -1,4 +1,4 @@ -import { CreateUser } from 'src/interface-adapters/interfaces/user/create.user.interface'; +import { CreateUser } from '@src/interface-adapters/interfaces/user/create.user.interface'; import { ApiProperty } from '@nestjs/swagger'; import { IsAlphanumeric, diff --git a/src/modules/user/commands/create-user/create-user.service.ts b/src/modules/user/commands/create-user/create-user.service.ts index f3c2210..fa31848 100644 --- a/src/modules/user/commands/create-user/create-user.service.ts +++ b/src/modules/user/commands/create-user/create-user.service.ts @@ -3,7 +3,7 @@ import { UserRepositoryPort } from '@modules/user/database/user.repository.port' import { ConflictException } from '@libs/exceptions'; import { Address } from '@modules/user/domain/value-objects/address.value-object'; import { Email } from '@modules/user/domain/value-objects/email.value-object'; -import { CreateUserUoW } from 'src/infrastructure/database/units-of-work'; +import { CreateUserUoW } from '@src/infrastructure/database/units-of-work'; import { CreateUserCommand } from './create-user.command'; import { UserEntity } from '../../domain/entities/user.entity'; diff --git a/src/modules/user/commands/create-user/create-user.spec.ts b/src/modules/user/commands/create-user/create-user.spec.ts deleted file mode 100644 index b70f86f..0000000 --- a/src/modules/user/commands/create-user/create-user.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: add tests diff --git a/src/modules/user/database/seeding/user.seeds.ts b/src/modules/user/database/seeding/user.seeds.ts index 6b3f72d..3f07189 100644 --- a/src/modules/user/database/seeding/user.seeds.ts +++ b/src/modules/user/database/seeding/user.seeds.ts @@ -1,6 +1,6 @@ import { UserRoles } from '@modules/user/domain/entities/user.types'; import { NonFunctionProperties } from '@libs/types'; -import { createdAtUpdatedAtMock } from '@libs/ddd/infrastructure/mocks/generic-model-props.mock'; +import { createdAtUpdatedAtMock } from '@src/libs/test-utils/mocks/generic-model-props.mock'; import { UserOrmEntity } from '../user.orm-entity'; export const userSeeds: NonFunctionProperties[] = [ diff --git a/src/modules/user/database/user.repository.ts b/src/modules/user/database/user.repository.ts index 6b680a4..360502d 100644 --- a/src/modules/user/database/user.repository.ts +++ b/src/modules/user/database/user.repository.ts @@ -4,13 +4,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { UserEntity, UserProps, -} from 'src/modules/user/domain/entities/user.entity'; +} from '@modules/user/domain/entities/user.entity'; import { NotFoundException } from '@libs/exceptions'; import { TypeormRepositoryBase, WhereCondition, } from '@libs/ddd/infrastructure/database/base-classes/typeorm.repository.base'; import { QueryParams } from '@libs/ddd/domain/ports/repository.ports'; +import { removeUndefinedProps } from '@src/libs/utils/remove-undefined-props.util'; import { UserOrmEntity } from './user.orm-entity'; import { UserRepositoryPort } from './user.repository.port'; import { UserOrmMapper } from './user.orm-mapper'; @@ -60,8 +61,8 @@ export class UserRepository } async findUsers(query: FindUsersQuery): Promise { - const users = await this.repository.find({ where: query }); - + const where: QueryParams = removeUndefinedProps(query); + const users = await this.repository.find({ where }); return users.map(user => this.mapper.toDomainEntity(user)); } diff --git a/src/modules/user/dtos/user.response.dto.ts b/src/modules/user/dtos/user.response.dto.ts index 865287f..aec4052 100644 --- a/src/modules/user/dtos/user.response.dto.ts +++ b/src/modules/user/dtos/user.response.dto.ts @@ -1,6 +1,6 @@ import { UserEntity } from '@modules/user/domain/entities/user.entity'; import { ResponseBase } from '@libs/ddd/interface-adapters/base-classes/response.base'; -import { User } from 'src/interface-adapters/interfaces/user/user.interface'; +import { User } from '@src/interface-adapters/interfaces/user/user.interface'; import { ApiProperty } from '@nestjs/swagger'; export class UserResponse extends ResponseBase implements User { diff --git a/src/modules/user/queries/find-users/find-users.query.ts b/src/modules/user/queries/find-users/find-users.query.ts index 4f25a8f..0f483c8 100644 --- a/src/modules/user/queries/find-users/find-users.query.ts +++ b/src/modules/user/queries/find-users/find-users.query.ts @@ -6,9 +6,9 @@ export class FindUsersQuery { this.street = props.street; } - readonly country: string; + readonly country?: string; - readonly postalCode: string; + readonly postalCode?: string; - readonly street: string; + readonly street?: string; } diff --git a/src/modules/user/queries/find-users/find-users.request.dto.ts b/src/modules/user/queries/find-users/find-users.request.dto.ts index 356b49a..667b48f 100644 --- a/src/modules/user/queries/find-users/find-users.request.dto.ts +++ b/src/modules/user/queries/find-users/find-users.request.dto.ts @@ -6,7 +6,7 @@ import { Matches, IsOptional, } from 'class-validator'; -import { FindUsers } from 'src/interface-adapters/interfaces/user/find-users.interface'; +import { FindUsers } from '@src/interface-adapters/interfaces/user/find-users.interface'; export class FindUsersRequest implements FindUsers { @ApiProperty({ example: 'France', description: 'Country of residence' }) diff --git a/src/modules/user/user.providers.ts b/src/modules/user/user.providers.ts index b62565f..8ff0598 100644 --- a/src/modules/user/user.providers.ts +++ b/src/modules/user/user.providers.ts @@ -1,5 +1,5 @@ import { Logger, Provider } from '@nestjs/common'; -import { CreateUserUoW } from 'src/infrastructure/database/units-of-work'; +import { CreateUserUoW } from '@src/infrastructure/database/units-of-work'; import { UserRepository } from './database/user.repository'; import { CreateUserService } from './commands/create-user/create-user.service'; import { DeleteUserService } from './commands/delete-user/delete-user.service'; diff --git a/src/modules/wallet/wallet.providers.ts b/src/modules/wallet/wallet.providers.ts index a254eb5..5ec64e4 100644 --- a/src/modules/wallet/wallet.providers.ts +++ b/src/modules/wallet/wallet.providers.ts @@ -1,5 +1,5 @@ import { Provider } from '@nestjs/common'; -import { CreateUserUoW } from 'src/infrastructure/database/units-of-work'; +import { CreateUserUoW } from '@src/infrastructure/database/units-of-work'; import { WalletRepository } from './database/wallet.repository'; import { CreateWalletWhenUserIsCreatedDomainEventHandler } from './application/event-handlers/create-wallet-when-user-is-created.domain-event-handler'; diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 0012dcd..0000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from '../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json deleted file mode 100644 index e9d912f..0000000 --- a/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/tests/jestGlobalSetup.ts b/tests/jestGlobalSetup.ts new file mode 100644 index 0000000..4b6e783 --- /dev/null +++ b/tests/jestGlobalSetup.ts @@ -0,0 +1,8 @@ +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +// This will force dotenv to use environmental variables from ".env.test" instead of ".env" +const envPath: string = path.resolve(__dirname, '../.env.test'); +module.exports = async (): Promise => { + dotenv.config({ path: envPath }); +}; diff --git a/tests/jestSetupAfterEnv.ts b/tests/jestSetupAfterEnv.ts new file mode 100644 index 0000000..6c6d3dd --- /dev/null +++ b/tests/jestSetupAfterEnv.ts @@ -0,0 +1,48 @@ +import { Test, TestingModuleBuilder, TestingModule } from '@nestjs/testing'; +import { AppModule } from '@src/app.module'; +import { NestExpressApplication } from '@nestjs/platform-express'; + +export class TestServer { + constructor( + public readonly serverApplication: NestExpressApplication, + public readonly testingModule: TestingModule, + ) {} + + public static async new( + testingModuleBuilder: TestingModuleBuilder, + ): Promise { + const testingModule: TestingModule = await testingModuleBuilder.compile(); + + const serverApplication: NestExpressApplication = testingModule.createNestApplication(); + await serverApplication.init(); + + return new TestServer(serverApplication, testingModule); + } +} + +export async function generateTestingApplication(): Promise<{ + testServer: TestServer; + // api: ApiClient; +}> { + const testServer = await TestServer.new( + Test.createTestingModule({ + imports: [AppModule], + }), + ); + + return { + testServer, + }; +} + +let testServer: TestServer; + +export function getTestServer(): TestServer { + return testServer; +} + +beforeAll( + async (): Promise => { + ({ testServer } = await generateTestingApplication()); + }, +); diff --git a/tests/user/create-user/__snapshots__/create-user.e2e-spec.ts.snap b/tests/user/create-user/__snapshots__/create-user.e2e-spec.ts.snap new file mode 100644 index 0000000..2798442 --- /dev/null +++ b/tests/user/create-user/__snapshots__/create-user.e2e-spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create a user Creating a user happy path 1`] = ` +Object { + "id": Any, +} +`; + +exports[`Create a user Creating a user happy path 2`] = ` +Array [ + Object { + "country": "England", + "createdAt": Any, + "email": "john.doe@gmail.com", + "id": Any, + "postalCode": "29145", + "street": "Road Avenue", + "updatedAt": Any, + }, +] +`; diff --git a/tests/user/create-user/create-user.e2e-spec.ts b/tests/user/create-user/create-user.e2e-spec.ts new file mode 100644 index 0000000..d04a700 --- /dev/null +++ b/tests/user/create-user/create-user.e2e-spec.ts @@ -0,0 +1,62 @@ +import { defineFeature, loadFeature } from 'jest-cucumber'; +import * as request from 'supertest'; +import { CreateUser } from '@src/interface-adapters/interfaces/user/create.user.interface'; +import { Id } from '@src/libs/ddd/interface-adapters/interfaces/id.interface'; +import { UserResponse } from '@src/modules/user/dtos/user.response.dto'; +import { snapshotBaseProps } from '@src/libs/test-utils/snapshot-base-props'; +import { getTestServer, TestServer } from '../../jestSetupAfterEnv'; + +const feature = loadFeature('tests/user/create-user/create-user.feature'); + +defineFeature(feature, test => { + let testServer: TestServer; + let httpServer: request.SuperTest; + + beforeAll(() => { + testServer = getTestServer(); + httpServer = request(testServer.serverApplication.getHttpServer()); + }); + + afterAll(() => { + // TODO: clean db after tests are finished + }); + + test('Creating a user happy path', ({ given, when, then, and }) => { + const userDto: Partial = {}; + let userId: Id; + + given(/^that my email is "(.*)"$/, (email: string) => { + userDto.email = email; + }); + + and( + /^my country is "(.*)", my postal code is "(.*)" and my street is "(.*)"$/, + (country: string, postalCode: string, street: string) => { + userDto.country = country; + userDto.postalCode = postalCode; + userDto.street = street; + }, + ); + + when('I send a request to create a user', async () => { + const res = await httpServer + .post('/users') + .send(userDto) + .expect(201); + userId = res.body; + }); + + then('I receive my user ID', () => { + expect(userId).toMatchSnapshot({ id: expect.any(String) }); + }); + + and('I can see my user in a list of all users', async () => { + const res = await httpServer.get('/users').expect(200); + + expect(res.body).toMatchSnapshot([snapshotBaseProps]); + expect(res.body.some((item: UserResponse) => item.id === userId.id)).toBe( + true, + ); + }); + }); +}); diff --git a/tests/user/create-user/create-user.feature b/tests/user/create-user/create-user.feature new file mode 100644 index 0000000..fe4017b --- /dev/null +++ b/tests/user/create-user/create-user.feature @@ -0,0 +1,8 @@ +Feature: Create a user + +Scenario: Creating a user happy path + Given that my email is "john.doe@gmail.com" + And my country is "England", my postal code is "29145" and my street is "Road Avenue" + When I send a request to create a user + Then I receive my user ID + And I can see my user in a list of all users \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6d5374e..aae1bdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "baseUrl": "./", "incremental": true, "paths": { + "@src/*": ["src/*"], "@modules/*": ["src/modules/*"], "@config/*": ["src/infrastructure/configs/*"], "@exceptions": ["src/libs/exceptions"],