add e2e with docker-compose support

This commit is contained in:
Schneider Roland 2025-10-29 08:12:23 +01:00
parent 7bf514b2aa
commit bdf16a3189
8 changed files with 1566 additions and 1 deletions

6
.env.e2e Normal file
View File

@ -0,0 +1,6 @@
DB_HOST=localhost
DB_PORT=4401
DB_USERNAME=test
DB_PASSWORD=test
DB_DATABASE=test
JWT_SECRET=secret

View File

@ -0,0 +1,11 @@
version: '3.8'
services:
postgres:
image: postgres:18
restart: always
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- '4401:5432'

1360
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.0", "@types/passport-jwt": "^4.0.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"dotenv": "^16.4.5",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2", "eslint-plugin-prettier": "^5.2.2",
@ -60,6 +61,7 @@
"prettier": "^3.4.2", "prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"testcontainers": "^10.9.0",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

41
test/global-setup.ts Normal file
View File

@ -0,0 +1,41 @@
import { exec } from 'child_process';
import * as path from 'path';
import { DockerComposeEnvironment, Wait } from 'testcontainers';
export default async () => {
const composeFilePath = path.resolve(__dirname, '../environment/e2e');
const environment = await new DockerComposeEnvironment(
composeFilePath,
'docker-compose.yaml',
)
.withWaitStrategy('postgres_1', Wait.forHealthCheck())
.up();
// Store the environment details for teardown
(global as any).__TESTCONTAINERS_ENVIRONMENT__ = environment;
// Run migrations
await new Promise<void>((resolve, reject) => {
console.info("running migration")
exec(
'env && npm run migration:run',
{ env: { ...process.env, ...readEnvFile() } },
(err, stdout, stderr) => {
if (err) {
console.error(stderr);
return reject(err);
}
console.log(stdout);
resolve();
},
);
});
};
function readEnvFile() {
const fs = require('fs');
const dotenv = require('dotenv');
const envConfig = dotenv.parse(fs.readFileSync('.env.e2e'));
return envConfig;
}

10
test/global-teardown.ts Normal file
View File

@ -0,0 +1,10 @@
import { DockerComposeEnvironment } from 'testcontainers';
export default async () => {
const environment: DockerComposeEnvironment = (global as any)
.__TESTCONTAINERS_ENVIRONMENT__;
if (environment) {
await environment.down();
}
};

View File

@ -5,5 +5,7 @@
"testRegex": ".e2e-spec.ts$", "testRegex": ".e2e-spec.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
} },
"globalSetup": "<rootDir>/global-setup.ts",
"globalTeardown": "<rootDir>/global-teardown.ts"
} }

133
test/user.e2e-spec.ts Normal file
View File

@ -0,0 +1,133 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { CreateUserDto } from '../src/user/dto/create-user.dto';
import { Role } from '../src/auth/role.enum';
import { UserService } from '../src/user/user.service';
import { User } from '../src/entity/user';
import { UpdateUserDto } from '../src/user/dto/update-user.dto';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.e2e' });
describe('UserController (e2e)', () => {
let app: INestApplication;
let jwtToken: string;
let adminUserId: number;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
const userService = moduleFixture.get<UserService>(UserService);
const adminUser = await userService.create({
username: 'e2e_admin',
password: 'password',
roles: [Role.Admin],
});
adminUserId = adminUser.id;
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'e2e_admin', password: 'password' });
jwtToken = response.body.access_token;
});
afterAll(async () => {
const userService = app.get<UserService>(UserService);
await userService.remove(adminUserId);
await app.close();
});
describe('/users', () => {
it('(GET) should get all users', async () => {
const response = await request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${jwtToken}`);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
it('(POST) should create a user', async () => {
const createUserDto: CreateUserDto = {
username: 'e2e_user',
password: 'password',
roles: [Role.User],
};
const response = await request(app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${jwtToken}`)
.send(createUserDto);
expect(response.status).toBe(201);
expect(response.body.username).toEqual(createUserDto.username);
const userService = app.get<UserService>(UserService);
await userService.remove(response.body.id);
});
});
describe('/users/:id', () => {
let user: User;
let userService: UserService;
beforeEach(async () => {
userService = app.get<UserService>(UserService);
user = await userService.create({
username: 'e2e_test_user',
password: 'password',
roles: [Role.User],
});
});
afterEach(async () => {
const userExists = await userService.findOne(user.id);
if (userExists) {
await userService.remove(user.id);
}
});
it('(GET) should get a user by id', async () => {
const response = await request(app.getHttpServer())
.get(`/users/${user.id}`)
.set('Authorization', `Bearer ${jwtToken}`);
expect(response.status).toBe(200);
expect(response.body.id).toEqual(user.id);
});
it('(PATCH) should update a user', async () => {
const updateUserDto: UpdateUserDto = {
username: 'e2e_updated_user',
};
const response = await request(app.getHttpServer())
.patch(`/users/${user.id}`)
.set('Authorization', `Bearer ${jwtToken}`)
.send(updateUserDto);
expect(response.status).toBe(200);
expect(response.body.username).toEqual(updateUserDto.username);
});
it('(DELETE) should delete a user', async () => {
const response = await request(app.getHttpServer())
.delete(`/users/${user.id}`)
.set('Authorization', `Bearer ${jwtToken}`);
expect(response.status).toBe(200);
const deletedUser = await userService.findOne(user.id);
expect(deletedUser).toBeNull();
});
});
});