From 79dea504b099126568d95ccbca61c5f9f9aeb087 Mon Sep 17 00:00:00 2001 From: Jaime Baez Date: Fri, 20 May 2022 01:30:47 +0200 Subject: [PATCH] Add e2e testing setup (#163) * Setup e2e testing * Add user e2e tests * Rename database host env variable to DB_HOST * Force push (try to recover DB_HOST env) * Rename db host env variable to `DB_HOSTNAME` * Remove unnecessary `initDb` from test-utils The current database.config is running the migrations: `migrationsRun: true` --- Makefile | 5 +- docker/.env.test | 16 +++++ docker/docker-compose.test.yml | 52 +++++++++++++++ server/src/config/database.config.ts | 2 +- server/test/app.e2e-spec.ts | 24 ------- server/test/test-utils.ts | 37 +++++++++++ server/test/user.e2e-spec.ts | 96 ++++++++++++++++++++++++++++ server/tsconfig.json | 6 +- 8 files changed, 208 insertions(+), 30 deletions(-) create mode 100644 docker/.env.test create mode 100644 docker/docker-compose.test.yml delete mode 100644 server/test/app.e2e-spec.ts create mode 100644 server/test/test-utils.ts create mode 100644 server/test/user.e2e-spec.ts diff --git a/Makefile b/Makefile index 8cc41062bb..ee0a7ea309 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,10 @@ dev-update: docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans dev-scale: - docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans + docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans + +test-e2e: + docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test prod: docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans diff --git a/docker/.env.test b/docker/.env.test new file mode 100644 index 0000000000..f5438bb2b1 --- /dev/null +++ b/docker/.env.test @@ -0,0 +1,16 @@ +DB_HOSTNAME=immich_postgres_test +# Database +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE_NAME=e2e_test + +# Upload File Config +UPLOAD_LOCATION=./upload + +# JWT SECRET +JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess + +# MAPBOX +## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY +ENABLE_MAPBOX=false +MAPBOX_KEY= \ No newline at end of file diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml new file mode 100644 index 0000000000..6d7a83700b --- /dev/null +++ b/docker/docker-compose.test.yml @@ -0,0 +1,52 @@ +version: "3.8" + +services: + immich_server_test: + image: immich-server-dev:1.9.0 + build: + context: ../server + dockerfile: Dockerfile + command: npm run test:e2e + expose: + - "3000" + volumes: + - ../server:/usr/src/app + - /usr/src/app/node_modules + env_file: + - .env.test + environment: + - NODE_ENV=development + depends_on: + - redis + - database + networks: + - immich_network_test + + + redis: + container_name: immich_redis_test + image: redis:6.2 + networks: + - immich_network_test + + database: + container_name: immich_postgres_test + image: postgres:14 + env_file: + - .env.test + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + PG_DATA: /var/lib/postgresql/data + volumes: + - pgdata-test:/var/lib/postgresql/data + ports: + - 5432:5432 + networks: + - immich_network_test + +networks: + immich_network_test: +volumes: + pgdata-test: diff --git a/server/src/config/database.config.ts b/server/src/config/database.config.ts index dec963812e..90a09f5ae8 100644 --- a/server/src/config/database.config.ts +++ b/server/src/config/database.config.ts @@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; export const databaseConfig: TypeOrmModuleOptions = { type: 'postgres', - host: 'immich_postgres', + host: process.env.DB_HOSTNAME || 'immich_postgres', port: 5432, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, diff --git a/server/test/app.e2e-spec.ts b/server/test/app.e2e-spec.ts deleted file mode 100644 index 50cda62332..0000000000 --- a/server/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/server/test/test-utils.ts b/server/test/test-utils.ts new file mode 100644 index 0000000000..04aa276d74 --- /dev/null +++ b/server/test/test-utils.ts @@ -0,0 +1,37 @@ +import { getConnection } from 'typeorm'; +import { CanActivate, ExecutionContext } from '@nestjs/common'; +import { TestingModuleBuilder } from '@nestjs/testing'; +import { AuthUserDto } from '../src/decorators/auth-user.decorator'; +import { JwtAuthGuard } from '../src/modules/immich-jwt/guards/jwt-auth.guard'; + +type CustomAuthCallback = () => AuthUserDto; + +export async function clearDb() { + const entities = getConnection().entityMetadatas; + for (const entity of entities) { + const repository = getConnection().getRepository(entity.name); + await repository.query(`TRUNCATE ${entity.tableName} RESTART IDENTITY CASCADE;`); + } +} + +export function getAuthUser(): AuthUserDto { + return { + id: '3108ac14-8afb-4b7e-87fd-39ebb6b79750', + email: 'test@email.com', + }; +} + +export function auth(builder: TestingModuleBuilder): TestingModuleBuilder { + return authCustom(builder, getAuthUser); +} + +export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCallback): TestingModuleBuilder { + const canActivate: CanActivate = { + canActivate: (context: ExecutionContext) => { + const req = context.switchToHttp().getRequest(); + req.user = callback(); + return true; + }, + }; + return builder.overrideGuard(JwtAuthGuard).useValue(canActivate); +} diff --git a/server/test/user.e2e-spec.ts b/server/test/user.e2e-spec.ts new file mode 100644 index 0000000000..4d6d373eb0 --- /dev/null +++ b/server/test/user.e2e-spec.ts @@ -0,0 +1,96 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import request from 'supertest'; +import { clearDb, authCustom } from './test-utils'; +import { databaseConfig } from '../src/config/database.config'; +import { UserModule } from '../src/api-v1/user/user.module'; +import { AuthModule } from '../src/api-v1/auth/auth.module'; +import { AuthService } from '../src/api-v1/auth/auth.service'; +import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module'; +import { SignUpDto } from '../src/api-v1/auth/dto/sign-up.dto'; +import { AuthUserDto } from '../src/decorators/auth-user.decorator'; + +function _createUser(authService: AuthService, data: SignUpDto) { + return authService.signUp(data); +} + +describe('User', () => { + let app: INestApplication; + + afterAll(async () => { + await clearDb(); + await app.close(); + }); + + describe('without auth', () => { + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [UserModule, ImmichJwtModule, TypeOrmModule.forRoot(databaseConfig)], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('prevents fetching users if not auth', async () => { + const { status } = await request(app.getHttpServer()).get('/user'); + expect(status).toEqual(401); + }); + }); + + describe('with auth', () => { + let authService: AuthService; + let authUser: AuthUserDto; + + beforeAll(async () => { + const builder = Test.createTestingModule({ + imports: [UserModule, AuthModule, TypeOrmModule.forRoot(databaseConfig)], + }); + const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); + + app = moduleFixture.createNestApplication(); + authService = app.get(AuthService); + await app.init(); + }); + + describe('with users in DB', () => { + const authUserEmail = 'auth-user@test.com'; + const userOneEmail = 'one@test.com'; + const userTwoEmail = 'two@test.com'; + + beforeAll(async () => { + await Promise.allSettled([ + _createUser(authService, { email: authUserEmail, password: '1234' }).then((user) => (authUser = user)), + _createUser(authService, { email: userOneEmail, password: '1234' }), + _createUser(authService, { email: userTwoEmail, password: '1234' }), + ]); + }); + + it('fetches the user collection excluding the auth user', async () => { + const { status, body } = await request(app.getHttpServer()).get('/user'); + expect(status).toEqual(200); + expect(body).toHaveLength(2); + expect(body).toEqual( + expect.arrayContaining([ + { + email: userOneEmail, + id: expect.anything(), + createdAt: expect.anything(), + }, + { + email: userTwoEmail, + id: expect.anything(), + createdAt: expect.anything(), + }, + ]), + ); + expect(body).toEqual(expect.not.arrayContaining([expect.objectContaining({ email: authUserEmail })])); + }); + }); + }); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json index b9c6829156..7039c7253a 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,15 +9,13 @@ "target": "es2017", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", "incremental": true, "skipLibCheck": true, "esModuleInterop": true, }, "exclude": [ + "dist", + "node_modules", "upload" ], - "include": [ - "src" - ] } \ No newline at end of file