diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33b6cf5ccc..dac6b1f8a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: submodules: "recursive" - name: Run e2e tests - run: docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build + run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build doc-tests: name: Run documentation checks diff --git a/Makefile b/Makefile index a8b86d75c6..504c02c102 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ pull-stage: docker-compose -f ./docker/docker-compose.staging.yml pull test-e2e: - docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build + docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build prod: docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans @@ -32,4 +32,4 @@ api: cd ./server && npm run api:generate attach-server: - docker exec -it docker_immich-server_1 sh \ No newline at end of file + docker exec -it docker_immich-server_1 sh diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml index 57b0123340..df965aa1f6 100644 --- a/docker/docker-compose.test.yml +++ b/docker/docker-compose.test.yml @@ -1,10 +1,10 @@ version: "3.8" -# Compose file for dockerized end-to-end testing of the backend +name: "immich-test-e2e" services: - immich-server-test: - image: immich-server-test + immich-server: + image: immich-server-dev:latest build: context: ../server dockerfile: Dockerfile @@ -14,27 +14,20 @@ services: - ../server:/usr/src/app - /usr/src/app/node_modules environment: - - DB_HOSTNAME=immich-database-test + - DB_HOSTNAME=database - DB_USERNAME=postgres - DB_PASSWORD=postgres - DB_DATABASE_NAME=e2e_test - IMMICH_RUN_ALL_TESTS=true depends_on: - - immich-database-test - networks: - - immich-test-network + - database - immich-database-test: - container_name: immich-database-test + database: image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + command: -c fsync=off environment: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: e2e_test - networks: - - immich-test-network logging: driver: none - -networks: - immich-test-network: diff --git a/server/package.json b/server/package.json index acbd2f334b..def7a64aca 100644 --- a/server/package.json +++ b/server/package.json @@ -26,7 +26,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": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand --forceExit", + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand", "typeorm": "typeorm", "typeorm:migrations:create": "typeorm migration:create", "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 2779df54c6..9a7b4f3e59 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -109,6 +109,10 @@ export class MetadataService { } } + async teardown() { + await this.repository.teardown(); + } + async handleLivePhotoLinking(job: IEntityJob) { const { id } = job; const [asset] = await this.assetRepository.getByIds([id]); diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index a037964f47..084a655c7a 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -26,6 +26,7 @@ export interface ImmichTags extends Omit { export interface IMetadataRepository { init(options: Partial): Promise; + teardown(): Promise; reverseGeocode(point: GeoPoint): Promise; deleteCache(): Promise; getExifTags(path: string): Promise; diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 3cb53e823d..63bc29dcba 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -45,6 +45,10 @@ export class MetadataRepository implements IMetadataRepository { }); } + async teardown() { + await exiftool.end(); + } + async deleteCache() { const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; if (dumpDirectory) { diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 1513c62975..365b073299 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -103,4 +103,8 @@ export class AppService { await this.metadataService.init(); await this.searchService.init(); } + + async teardown() { + await this.metadataService.teardown(); + } } diff --git a/server/test/e2e/album.e2e-spec.ts b/server/test/e2e/album.e2e-spec.ts index 633a825a76..e10f5414f9 100644 --- a/server/test/e2e/album.e2e-spec.ts +++ b/server/test/e2e/album.e2e-spec.ts @@ -2,11 +2,10 @@ import { AlbumResponseDto, LoginResponseDto } from '@app/domain'; import { AlbumController } from '@app/immich'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const user1SharedUser = 'user1SharedUser'; @@ -17,7 +16,6 @@ const user2SharedLink = 'user2SharedLink'; const user2NotShared = 'user2NotShared'; describe(`${AlbumController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; let user1: LoginResponseDto; @@ -27,9 +25,11 @@ describe(`${AlbumController.name} (e2e)`, () => { let user2Albums: AlbumResponseDto[]; beforeAll(async () => { - app = await createTestApp(); + [server] = await testApp.create(); + }); - server = app.getHttpServer(); + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -37,24 +37,30 @@ describe(`${AlbumController.name} (e2e)`, () => { await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); - await api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }); - user1 = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); + await Promise.all([ + api.userApi.create(server, admin.accessToken, { + email: 'user1@immich.app', + password: 'Password123', + firstName: 'User 1', + lastName: 'Test', + }), + api.userApi.create(server, admin.accessToken, { + email: 'user2@immich.app', + password: 'Password123', + firstName: 'User 2', + lastName: 'Test', + }), + ]); - await api.userApi.create(server, admin.accessToken, { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', - }); - user2 = await api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }); + [user1, user2] = await Promise.all([ + api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }), + api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }), + ]); user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example'); - user1Albums = await Promise.all([ + + const albums = await Promise.all([ + // user 1 api.albumApi.create(server, user1.accessToken, { albumName: user1SharedUser, sharedWithUserIds: [user2.userId], @@ -62,15 +68,8 @@ describe(`${AlbumController.name} (e2e)`, () => { }), api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset.id] }), api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }), - ]); - // add shared link to user1SharedLink album - await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: user1Albums[1].id, - }); - - user2Albums = await Promise.all([ + // user 2 api.albumApi.create(server, user2.accessToken, { albumName: user2SharedUser, sharedWithUserIds: [user1.userId], @@ -80,16 +79,22 @@ describe(`${AlbumController.name} (e2e)`, () => { api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }), ]); - // add shared link to user2SharedLink album - await api.sharedLinkApi.create(server, user2.accessToken, { - type: SharedLinkType.ALBUM, - albumId: user2Albums[1].id, - }); - }); + user1Albums = albums.slice(0, 3); + user2Albums = albums.slice(3); - afterAll(async () => { - await db.disconnect(); - await app.close(); + await Promise.all([ + // add shared link to user1SharedLink album + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: user1Albums[1].id, + }), + + // add shared link to user2SharedLink album + api.sharedLinkApi.create(server, user2.accessToken, { + type: SharedLinkType.ALBUM, + albumId: user2Albums[1].id, + }), + ]); }); describe('GET /album', () => { diff --git a/server/test/e2e/asset.e2e-spec.ts b/server/test/e2e/asset.e2e-spec.ts index 4f4021d59e..c18268502b 100644 --- a/server/test/e2e/asset.e2e-spec.ts +++ b/server/test/e2e/asset.e2e-spec.ts @@ -12,7 +12,7 @@ import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp, db } from '@test/test-utils'; +import { db, testApp } from '@test/test-utils'; import { randomBytes } from 'crypto'; import request from 'supertest'; @@ -86,12 +86,14 @@ describe(`${AssetController.name} (e2e)`, () => { let asset4: AssetEntity; beforeAll(async () => { - app = await createTestApp(); - - server = app.getHttpServer(); + [server, app] = await testApp.create(); assetRepository = app.get(IAssetRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); @@ -123,11 +125,6 @@ describe(`${AssetController.name} (e2e)`, () => { }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /asset/upload', () => { it('should require authentication', async () => { const { status, body } = await request(server) @@ -589,9 +586,11 @@ describe(`${AssetController.name} (e2e)`, () => { describe('GET /asset/map-marker', () => { beforeEach(async () => { - await assetRepository.save({ id: asset1.id, isArchived: true }); - await assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }); - await assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }); + await Promise.all([ + assetRepository.save({ id: asset1.id, isArchived: true }), + assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }), + assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }), + ]); }); it('should require authentication', async () => { diff --git a/server/test/e2e/auth.e2e-spec.ts b/server/test/e2e/auth.e2e-spec.ts index 4068634e74..a42e1e1618 100644 --- a/server/test/e2e/auth.e2e-spec.ts +++ b/server/test/e2e/auth.e2e-spec.ts @@ -1,5 +1,4 @@ import { AuthController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { @@ -12,7 +11,7 @@ import { signupResponseStub, uuidStub, } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const firstName = 'Immich'; @@ -21,13 +20,16 @@ const password = 'Password123'; const email = 'admin@immich.app'; describe(`${AuthController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let accessToken: string; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + await testApp.reset(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -37,11 +39,6 @@ describe(`${AuthController.name} (e2e)`, () => { accessToken = response.accessToken; }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /auth/admin-sign-up', () => { beforeEach(async () => { await db.reset(); diff --git a/server/test/e2e/formats.e2e-spec.ts b/server/test/e2e/formats.e2e-spec.ts index 98e24ec9ac..f2fce83acd 100644 --- a/server/test/e2e/formats.e2e-spec.ts +++ b/server/test/e2e/formats.e2e-spec.ts @@ -1,11 +1,9 @@ import { LoginResponseDto } from '@app/domain'; import { AssetType, LibraryType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { IMMICH_TEST_ASSET_PATH, createTestApp, db, runAllTests } from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, db, runAllTests, testApp } from '@test/test-utils'; describe(`Supported file formats (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; @@ -170,8 +168,11 @@ describe(`Supported file formats (e2e)`, () => { const testsToRun = formatTests.filter((formatTest) => formatTest.runTest); beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -181,11 +182,6 @@ describe(`Supported file formats (e2e)`, () => { await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - it.each(testsToRun)('should import file of format $format', async (testedFormat) => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, diff --git a/server/test/e2e/library.e2e-spec.ts b/server/test/e2e/library.e2e-spec.ts index 742e6b7fed..9cfbe89610 100644 --- a/server/test/e2e/library.e2e-spec.ts +++ b/server/test/e2e/library.e2e-spec.ts @@ -1,22 +1,14 @@ import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryController } from '@app/immich'; import { AssetType, LibraryType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - createTestApp, - db, - restoreTempFolder, -} from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, db, restoreTempFolder, testApp } from '@test/test-utils'; import * as fs from 'fs'; import request from 'supertest'; import { utimes } from 'utimes'; import { errorStub, uuidStub } from '../fixtures'; describe(`${LibraryController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; @@ -35,8 +27,12 @@ describe(`${LibraryController.name} (e2e)`, () => { }; beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); + await restoreTempFolder(); }); beforeEach(async () => { @@ -46,12 +42,6 @@ describe(`${LibraryController.name} (e2e)`, () => { admin = await api.authApi.adminLogin(server); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - await restoreTempFolder(); - }); - describe('GET /library', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/library'); diff --git a/server/test/e2e/oauth.e2e-spec.ts b/server/test/e2e/oauth.e2e-spec.ts index d0d2137c64..879d538152 100644 --- a/server/test/e2e/oauth.e2e-spec.ts +++ b/server/test/e2e/oauth.e2e-spec.ts @@ -1,18 +1,19 @@ import { OAuthController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${OAuthController.name} (e2e)`, () => { - let app: INestApplication; let server: any; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -20,11 +21,6 @@ describe(`${OAuthController.name} (e2e)`, () => { await api.authApi.adminSignUp(server); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /oauth/authorize', () => { beforeEach(async () => { await db.reset(); diff --git a/server/test/e2e/partner.e2e-spec.ts b/server/test/e2e/partner.e2e-spec.ts index b0eb1d4ce7..82a09dcc8d 100644 --- a/server/test/e2e/partner.e2e-spec.ts +++ b/server/test/e2e/partner.e2e-spec.ts @@ -4,7 +4,7 @@ import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const user1Dto = { @@ -31,27 +31,29 @@ describe(`${PartnerController.name} (e2e)`, () => { let user2: LoginResponseDto; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server, app] = await testApp.create(); repository = app.get(IPartnerRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); loginResponse = await api.authApi.adminLogin(server); accessToken = loginResponse.accessToken; - await api.userApi.create(server, accessToken, user1Dto); - user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); + await Promise.all([ + api.userApi.create(server, accessToken, user1Dto), + api.userApi.create(server, accessToken, user2Dto), + ]); - await api.userApi.create(server, accessToken, user2Dto); - user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }); - }); - - afterAll(async () => { - await db.disconnect(); - await app.close(); + [user1, user2] = await Promise.all([ + api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }), + api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }), + ]); }); describe('GET /partner', () => { diff --git a/server/test/e2e/person.e2e-spec.ts b/server/test/e2e/person.e2e-spec.ts index f9da56fa81..bb0af4c96c 100644 --- a/server/test/e2e/person.e2e-spec.ts +++ b/server/test/e2e/person.e2e-spec.ts @@ -5,7 +5,7 @@ import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${PersonController.name}`, () => { @@ -18,11 +18,14 @@ describe(`${PersonController.name}`, () => { let hiddenPerson: PersonEntity; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server, app] = await testApp.create(); personRepository = app.get(IPersonRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); @@ -46,11 +49,6 @@ describe(`${PersonController.name}`, () => { await personRepository.createFace({ assetId: faceAsset.id, personId: hiddenPerson.id }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('GET /person', () => { beforeEach(async () => {}); diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts index 43cf471f40..cd6afbc07b 100644 --- a/server/test/e2e/server-info.e2e-spec.ts +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -1,21 +1,22 @@ import { LoginResponseDto } from '@app/domain'; import { ServerInfoController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${ServerInfoController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let accessToken: string; let loginResponse: LoginResponseDto; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -25,11 +26,6 @@ describe(`${ServerInfoController.name} (e2e)`, () => { accessToken = loginResponse.accessToken; }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('GET /server-info', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/server-info'); diff --git a/server/test/e2e/setup.ts b/server/test/e2e/setup.ts index 26849f4686..234deb7545 100644 --- a/server/test/e2e/setup.ts +++ b/server/test/e2e/setup.ts @@ -1,5 +1,5 @@ import { PostgreSqlContainer } from '@testcontainers/postgresql'; -import * as fs from 'fs'; +import { access } from 'fs/promises'; import path from 'path'; export default async () => { @@ -23,8 +23,7 @@ export default async () => { } const directoryExists = async (dirPath: string) => - await fs.promises - .access(dirPath) + await access(dirPath) .then(() => true) .catch(() => false); diff --git a/server/test/e2e/shared-link.e2e-spec.ts b/server/test/e2e/shared-link.e2e-spec.ts index 3a52c15a0f..80d43c7c74 100644 --- a/server/test/e2e/shared-link.e2e-spec.ts +++ b/server/test/e2e/shared-link.e2e-spec.ts @@ -1,16 +1,10 @@ import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; import { PartnerController } from '@app/immich'; import { LibraryType, SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - createTestApp, - restoreTempFolder, -} from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '@test/test-utils'; import { cp } from 'fs/promises'; import request from 'supertest'; @@ -22,7 +16,6 @@ const user1Dto = { }; describe(`${PartnerController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; let user1: LoginResponseDto; @@ -30,8 +23,12 @@ describe(`${PartnerController.name} (e2e)`, () => { let sharedLink: SharedLinkResponseDto; beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); + await restoreTempFolder(); }); beforeEach(async () => { @@ -49,12 +46,6 @@ describe(`${PartnerController.name} (e2e)`, () => { }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - await restoreTempFolder(); - }); - describe('GET /shared-link', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/shared-link'); diff --git a/server/test/e2e/user.e2e-spec.ts b/server/test/e2e/user.e2e-spec.ts index af0cbde745..d20ac729f6 100644 --- a/server/test/e2e/user.e2e-spec.ts +++ b/server/test/e2e/user.e2e-spec.ts @@ -2,10 +2,11 @@ import { LoginResponseDto, UserResponseDto, UserService } from '@app/domain'; import { AppModule, UserController } from '@app/immich'; import { UserEntity } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; +import { getRepositoryToken } from '@nestjs/typeorm'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, userSignupStub, userStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; import { Repository } from 'typeorm'; @@ -18,10 +19,12 @@ describe(`${UserController.name}`, () => { let userRepository: Repository; beforeAll(async () => { - app = await createTestApp(); - userRepository = app.select(AppModule).get('UserEntityRepository'); + [server, app] = await testApp.create(); + userRepository = app.select(AppModule).get(getRepositoryToken(UserEntity)); + }); - server = app.getHttpServer(); + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 13589f15b8..76c6f777a5 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -5,6 +5,7 @@ export const newMetadataRepositoryMock = (): jest.Mocked => deleteCache: jest.fn(), getExifTags: jest.fn(), init: jest.fn(), + teardown: jest.fn(), reverseGeocode: jest.fn(), }; }; diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts index 075e0b69fc..6b45c6ee6c 100644 --- a/server/test/test-utils.ts +++ b/server/test/test-utils.ts @@ -1,9 +1,8 @@ -import { dataSource } from '@app/infra'; - import { IJobRepository, JobItem, JobItemHandler, QueueName } from '@app/domain'; import { AppModule } from '@app/immich'; -import { INestApplication, Logger } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; +import { dataSource } from '@app/infra'; +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import * as fs from 'fs'; import path from 'path'; import { AppService } from '../src/microservices/app.service'; @@ -36,38 +35,48 @@ export const db = { let _handler: JobItemHandler = () => Promise.resolve(); -export async function createTestApp(runJobs = false, log = false): Promise { - const moduleBuilder = Test.createTestingModule({ - imports: [AppModule], - providers: [AppService], - }) - .overrideProvider(IJobRepository) - .useValue({ - addHandler: (_queueName: QueueName, _concurrency: number, handler: JobItemHandler) => (_handler = handler), - queue: (item: JobItem) => runJobs && _handler(item), - resume: jest.fn(), - empty: jest.fn(), - setConcurrency: jest.fn(), - getQueueStatus: jest.fn(), - getJobCounts: jest.fn(), - pause: jest.fn(), - } as IJobRepository); - - const moduleFixture: TestingModule = await moduleBuilder.compile(); - - const app = moduleFixture.createNestApplication(); - if (log) { - app.useLogger(new Logger()); - } else { - app.useLogger(false); - } - await app.init(); - const appService = app.get(AppService); - await appService.init(); - - return app; +interface TestAppOptions { + jobs: boolean; } +let app: INestApplication; + +export const testApp = { + create: async (options?: TestAppOptions): Promise<[any, INestApplication]> => { + const { jobs } = options || { jobs: false }; + + const moduleFixture = await Test.createTestingModule({ imports: [AppModule], providers: [AppService] }) + .overrideProvider(IJobRepository) + .useValue({ + addHandler: (_queueName: QueueName, _concurrency: number, handler: JobItemHandler) => (_handler = handler), + queue: (item: JobItem) => jobs && _handler(item), + resume: jest.fn(), + empty: jest.fn(), + setConcurrency: jest.fn(), + getQueueStatus: jest.fn(), + getJobCounts: jest.fn(), + pause: jest.fn(), + } as IJobRepository) + .compile(); + + app = await moduleFixture.createNestApplication().init(); + + if (jobs) { + await app.get(AppService).init(); + } + + return [app.getHttpServer(), app]; + }, + reset: async () => { + await db.reset(); + }, + teardown: async () => { + await app.get(AppService).teardown(); + await db.disconnect(); + await app.close(); + }, +}; + export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true'; const directoryExists = async (dirPath: string) =>