From ab9bb4897e9f6fd96ce7326298bec9314ffda377 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Thu, 5 Jan 2023 16:17:37 +0100 Subject: [PATCH] integration tests --- .env.dist | 9 +- .env.test | 11 ++ README.md | 57 ++++-- docker-compose.yml | 28 ++- package-lock.json | 40 ++++- package.json | 25 +-- .../integration/users.repository.spec.ts | 166 ++++++++++++++++++ 7 files changed, 280 insertions(+), 56 deletions(-) create mode 100644 .env.test create mode 100644 src/modules/users/tests/integration/users.repository.spec.ts diff --git a/.env.dist b/.env.dist index 73d1679..8f0776d 100644 --- a/.env.dist +++ b/.env.dist @@ -1,18 +1,11 @@ # SERVICE -SERVICE_CONTAINER=v3-user SERVICE_URL=0.0.0.0 -SERVICE_PORT=5001 # PRISMA DATABASE_URL="postgresql://user:user@v3-user-db:5432/user?schema=public" # RABBIT MQ -RMQ_URI=amqp://localhost:5672 +RMQ_URI=amqp://v3-broker:5672 # POSTGRES -POSTGRES_CONTAINER=v3-user-db POSTGRES_IMAGE=postgres:15.0 -POSTGRES_DB=user -POSTGRES_PASSWORD=user -POSTGRES_USER=user -POSTGRES_PORT=5501 diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..bc06f04 --- /dev/null +++ b/.env.test @@ -0,0 +1,11 @@ +# SERVICE +SERVICE_URL=0.0.0.0 + +# PRISMA +DATABASE_URL="postgresql://user:user@localhost:5601/user?schema=public" + +# RABBIT MQ +RMQ_URI=amqp://v3-broker:5672 + +# POSTGRES +POSTGRES_IMAGE=postgres:15.0 diff --git a/README.md b/README.md index 36bb80e..05f708f 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,35 @@ User-related data management. You need [Docker](https://docs.docker.com/engine/) and its [compose](https://docs.docker.com/compose/) plugin. +You also need NodeJS installed locally : we **strongly** advise to install [Node Version Manager](https://github.com/nvm-sh/nvm) and use the latest LTS version of Node (check that your local version matches with the one used in the Dockerfile). + +The API will run inside a docker container, **but** the install itself is made outside the container, because during development we need tools that need to be available locally (eg. ESLint, Prettier...). + A RabbitMQ instance is also required to send / receive messages when data has been inserted/updated/deleted. ## Installation -Copy `.env.dist` to `.env` : +- copy `.env.dist` to `.env` : -```bash -cp .env.dist .env -``` + ```bash + cp .env.dist .env + ``` -and modify it to suit your needs. + Modify it if needed. -Then execute : +- start the containers : -```bash -docker compose up -d -``` + ```bash + docker compose up -d + ``` -The app runs automatically on the port defined in `SERVICE_PORT` of `.env` file (default : _5001_). + The app runs automatically on the port defined in `SERVICE_PORT` of `.env` file (default : _5001_). + +- install the dependencies : + + ```bash + npm install + ``` ## Database migration @@ -96,17 +106,30 @@ As mentionned earlier, RabbitMQ messages are sent after these events : Various messages are also sent for logging purpose. -## Test +## Tests + +Tests are run outside the container for ease of use (switching between different environments inside containers using prisma is complicated and error prone). +The integration tests use a dedicated database (see *db-test* section of *docker-compose.yml*). ```bash -# unit tests -docker exec v3-user sh -c "npm run test" - -# test coverage -docker exec v3-user sh -c "npm run test:cov" +# run all tests (unit + integration) +npm run test ``` -Note : you can run all npm commands directly _outside_ the container (see _scripts_ section of `package.json` for available commands), but you need NodeJS installed locally. We **strongly** advise to install [Node Version Manager](https://github.com/nvm-sh/nvm) and use the latest LTS version of Node. +```bash +# unit tests only +npm run test:unit +``` + +```bash +# integration tests only +npm run test:integration +``` + +```bash +# coverage +npm run test:cov +``` ## License diff --git a/docker-compose.yml b/docker-compose.yml index 45b2f17..1a31dac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: api: - container_name: ${SERVICE_CONTAINER} + container_name: v3-user build: dockerfile: Dockerfile context: . @@ -13,7 +13,7 @@ services: - .env command: npm run start:dev ports: - - "${SERVICE_PORT:-5001}:${SERVICE_PORT:-5001}" + - 5001:5001 depends_on: - db networks: @@ -22,14 +22,14 @@ services: - v3-user-api db: - container_name: ${POSTGRES_CONTAINER} + container_name: v3-user-db image: ${POSTGRES_IMAGE} environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: user + POSTGRES_USER: user + POSTGRES_PASSWORD: user ports: - - "${POSTGRES_PORT:-5501}:5432" + - 5501:5432 volumes: - .postgresql:/var/lib/postgresql/data:rw networks: @@ -37,6 +37,20 @@ services: aliases: - v3-user-db + db-test: + container_name: v3-user-db-test + image: ${POSTGRES_IMAGE} + environment: + POSTGRES_DB: user + POSTGRES_USER: user + POSTGRES_PASSWORD: user + ports: + - 5601:5432 + networks: + v3-network: + aliases: + - v3-user-db-test + networks: v3-network: name: v3-network diff --git a/package-lock.json b/package-lock.json index 4ae0c38..d6124ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dotenv-cli": "^6.0.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", @@ -4075,6 +4076,21 @@ "node": ">=12" } }, + "node_modules/dotenv-cli": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-6.0.0.tgz", + "integrity": "sha512-qXlCOi3UMDhCWFKe0yq5sg3X+pJAz+RQDiFN38AMSbUrnY3uZshSfDJUAge951OS7J9gwLZGfsBlWRSOYz/TRg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "dotenv": "cli.js" + } + }, "node_modules/dotenv-expand": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", @@ -6374,9 +6390,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -12115,6 +12131,18 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" }, + "dotenv-cli": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-6.0.0.tgz", + "integrity": "sha512-qXlCOi3UMDhCWFKe0yq5sg3X+pJAz+RQDiFN38AMSbUrnY3uZshSfDJUAge951OS7J9gwLZGfsBlWRSOYz/TRg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.1", + "minimist": "^1.2.5" + } + }, "dotenv-expand": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", @@ -13844,9 +13872,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonc-parser": { diff --git a/package.json b/package.json index 9e2bcc6..62d76b8 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "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": "npm run migrate-test && dotenv -e .env.test jest", + "test:unit": "jest --testPathIgnorePatterns 'integration' --verbose", + "test:integration": "npm run migrate-test && dotenv -e .env.test -- jest --testPathPattern 'integration' --verbose", + "test:cov": "npm run migrate-test && dotenv -e .env.test -- jest --coverage", + "test:e2e": "jest --config ./test/jest-e2e.json", + "migrate-test": "dotenv -e .env.test -- npx prisma migrate dev --name postgres-init" }, "dependencies": { "@automapper/classes": "^8.7.7", @@ -33,23 +34,11 @@ "@nestjs/cqrs": "^9.0.1", "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", - "@prisma/client": "^4.7.1", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", - "reflect-metadata": "^0.1.13", - "rimraf": "^3.0.2", - "rxjs": "^7.2.0" - }, - "devDependencies": { - "@nestjs/cli": "^9.0.0", - "@nestjs/schematics": "^9.0.0", - "@nestjs/testing": "^9.0.0", - "@types/express": "^4.17.13", - "@types/jest": "28.1.8", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "dotenv-cli": "^6.0.0", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/src/modules/users/tests/integration/users.repository.spec.ts b/src/modules/users/tests/integration/users.repository.spec.ts new file mode 100644 index 0000000..c6df393 --- /dev/null +++ b/src/modules/users/tests/integration/users.repository.spec.ts @@ -0,0 +1,166 @@ +import { TestingModule, Test } from '@nestjs/testing'; +import { DatabaseModule } from '../../../database/database.module'; +import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; +import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { UsersRepository } from '../../adapters/secondaries/users.repository'; +import { User } from '../../domain/entities/user'; + +describe('UsersRepository', () => { + let prismaService: PrismaService; + let usersRepository: UsersRepository; + + const createUsers = async (nbToCreate = 10) => { + for (let i = 0; i < nbToCreate; i++) { + await prismaService.user.create({ + data: { + firstName: `firstName-${i}`, + }, + }); + } + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [DatabaseModule], + providers: [UsersRepository, PrismaService], + }).compile(); + + prismaService = module.get(PrismaService); + usersRepository = module.get(UsersRepository); + }); + + afterAll(async () => { + await prismaService.$disconnect(); + }); + + beforeEach(async () => { + await prismaService.user.deleteMany(); + }); + + describe('findAll', () => { + it('should return an empty data array', async () => { + const res = await usersRepository.findAll(); + expect(res).toEqual({ + data: [], + total: 0, + }); + }); + + it('should return an data array with users', async () => { + await createUsers(10); + const users = await usersRepository.findAll(); + expect(users.data.length).toBe(10); + }); + }); + + describe('findOneByUuid', () => { + it('should return a user', async () => { + const userToFind = await prismaService.user.create({ + data: { + firstName: 'test', + }, + }); + + const user = await usersRepository.findOneByUuid(userToFind.uuid); + expect(user.uuid).toBe(userToFind.uuid); + }); + + it('should return null', async () => { + const user = await usersRepository.findOneByUuid( + '544572be-11fb-4244-8235-587221fc9104', + ); + expect(user).toBeNull(); + }); + }); + + describe('findOne', () => { + it('should return a user according to its email', async () => { + const userToFind = await prismaService.user.create({ + data: { + email: 'test@test.com', + }, + }); + + const user = await usersRepository.findOne({ + email: 'test@test.com', + }); + + expect(user.uuid).toBe(userToFind.uuid); + }); + + it('should return null with unknown email', async () => { + const user = await usersRepository.findOne({ + email: 'wrong@email.com', + }); + expect(user).toBeNull(); + }); + }); + + describe('create', () => { + it('should create a user', async () => { + const beforeCount = await prismaService.user.count(); + + const userToCreate: User = new User(); + userToCreate.firstName = 'test'; + const user = await usersRepository.create(userToCreate); + + const afterCount = await prismaService.user.count(); + + expect(afterCount - beforeCount).toBe(1); + expect(user.uuid).toBeDefined(); + }); + }); + + describe('update', () => { + it('should update user firstName', async () => { + const userToUpdate = await prismaService.user.create({ + data: { + firstName: 'test', + }, + }); + + const toUpdate: User = new User(); + toUpdate.firstName = 'updated'; + const updateduser = await usersRepository.update( + userToUpdate.uuid, + toUpdate, + ); + + expect(updateduser.uuid).toBe(userToUpdate.uuid); + expect(updateduser.firstName).toBe('updated'); + }); + + it('should throw DatabaseException', async () => { + const toUpdate: User = new User(); + toUpdate.firstName = 'updated'; + + await expect( + usersRepository.update( + '544572be-11fb-4244-8235-587221fc9104', + toUpdate, + ), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + + describe('delete', () => { + it('should delete a user', async () => { + const userToRemove = await prismaService.user.create({ + data: { + firstName: 'test', + }, + }); + + await usersRepository.delete(userToRemove.uuid); + + const count = await prismaService.user.count(); + expect(count).toBe(0); + }); + + it('should throw DatabaseException', async () => { + await expect( + usersRepository.delete('544572be-11fb-4244-8235-587221fc9104'), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); +});