1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-23 12:12:45 +01:00

refactor: e2e tests ()

This commit is contained in:
Jason Rasmussen 2023-10-18 18:02:42 -04:00 committed by GitHub
parent 31987bc043
commit 4b59f83288
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 189 additions and 201 deletions

View file

@ -21,7 +21,7 @@ jobs:
submodules: "recursive" submodules: "recursive"
- name: Run e2e tests - 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: doc-tests:
name: Run documentation checks name: Run documentation checks

View file

@ -20,7 +20,7 @@ pull-stage:
docker-compose -f ./docker/docker-compose.staging.yml pull docker-compose -f ./docker/docker-compose.staging.yml pull
test-e2e: 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: prod:
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@ -32,4 +32,4 @@ api:
cd ./server && npm run api:generate cd ./server && npm run api:generate
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh

View file

@ -1,10 +1,10 @@
version: "3.8" version: "3.8"
# Compose file for dockerized end-to-end testing of the backend name: "immich-test-e2e"
services: services:
immich-server-test: immich-server:
image: immich-server-test image: immich-server-dev:latest
build: build:
context: ../server context: ../server
dockerfile: Dockerfile dockerfile: Dockerfile
@ -14,27 +14,20 @@ services:
- ../server:/usr/src/app - ../server:/usr/src/app
- /usr/src/app/node_modules - /usr/src/app/node_modules
environment: environment:
- DB_HOSTNAME=immich-database-test - DB_HOSTNAME=database
- DB_USERNAME=postgres - DB_USERNAME=postgres
- DB_PASSWORD=postgres - DB_PASSWORD=postgres
- DB_DATABASE_NAME=e2e_test - DB_DATABASE_NAME=e2e_test
- IMMICH_RUN_ALL_TESTS=true - IMMICH_RUN_ALL_TESTS=true
depends_on: depends_on:
- immich-database-test - database
networks:
- immich-test-network
immich-database-test: database:
container_name: immich-database-test
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
command: -c fsync=off
environment: environment:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_DB: e2e_test POSTGRES_DB: e2e_test
networks:
- immich-test-network
logging: logging:
driver: none driver: none
networks:
immich-test-network:

View file

@ -26,7 +26,7 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "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": "typeorm",
"typeorm:migrations:create": "typeorm migration:create", "typeorm:migrations:create": "typeorm migration:create",
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",

View file

@ -109,6 +109,10 @@ export class MetadataService {
} }
} }
async teardown() {
await this.repository.teardown();
}
async handleLivePhotoLinking(job: IEntityJob) { async handleLivePhotoLinking(job: IEntityJob) {
const { id } = job; const { id } = job;
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);

View file

@ -26,6 +26,7 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength'> {
export interface IMetadataRepository { export interface IMetadataRepository {
init(options: Partial<InitOptions>): Promise<void>; init(options: Partial<InitOptions>): Promise<void>;
teardown(): Promise<void>;
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>; reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
deleteCache(): Promise<void>; deleteCache(): Promise<void>;
getExifTags(path: string): Promise<ImmichTags | null>; getExifTags(path: string): Promise<ImmichTags | null>;

View file

@ -45,6 +45,10 @@ export class MetadataRepository implements IMetadataRepository {
}); });
} }
async teardown() {
await exiftool.end();
}
async deleteCache() { async deleteCache() {
const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY;
if (dumpDirectory) { if (dumpDirectory) {

View file

@ -103,4 +103,8 @@ export class AppService {
await this.metadataService.init(); await this.metadataService.init();
await this.searchService.init(); await this.searchService.init();
} }
async teardown() {
await this.metadataService.teardown();
}
} }

View file

@ -2,11 +2,10 @@ import { AlbumResponseDto, LoginResponseDto } from '@app/domain';
import { AlbumController } from '@app/immich'; import { AlbumController } from '@app/immich';
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { SharedLinkType } from '@app/infra/entities'; import { SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub, uuidStub } from '@test/fixtures'; import { errorStub, uuidStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
const user1SharedUser = 'user1SharedUser'; const user1SharedUser = 'user1SharedUser';
@ -17,7 +16,6 @@ const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared'; const user2NotShared = 'user2NotShared';
describe(`${AlbumController.name} (e2e)`, () => { describe(`${AlbumController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
@ -27,9 +25,11 @@ describe(`${AlbumController.name} (e2e)`, () => {
let user2Albums: AlbumResponseDto[]; let user2Albums: AlbumResponseDto[];
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server] = await testApp.create();
});
server = app.getHttpServer(); afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -37,24 +37,30 @@ describe(`${AlbumController.name} (e2e)`, () => {
await api.authApi.adminSignUp(server); await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server); admin = await api.authApi.adminLogin(server);
await api.userApi.create(server, admin.accessToken, { await Promise.all([
email: 'user1@immich.app', api.userApi.create(server, admin.accessToken, {
password: 'Password123', email: 'user1@immich.app',
firstName: 'User 1', password: 'Password123',
lastName: 'Test', firstName: 'User 1',
}); lastName: 'Test',
user1 = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); }),
api.userApi.create(server, admin.accessToken, {
email: 'user2@immich.app',
password: 'Password123',
firstName: 'User 2',
lastName: 'Test',
}),
]);
await api.userApi.create(server, admin.accessToken, { [user1, user2] = await Promise.all([
email: 'user2@immich.app', api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }),
password: 'Password123', api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }),
firstName: 'User 2', ]);
lastName: 'Test',
});
user2 = await api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' });
user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example'); 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, { api.albumApi.create(server, user1.accessToken, {
albumName: user1SharedUser, albumName: user1SharedUser,
sharedWithUserIds: [user2.userId], 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: user1SharedLink, assetIds: [user1Asset.id] }),
api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }), api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }),
]);
// add shared link to user1SharedLink album // user 2
await api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
albumId: user1Albums[1].id,
});
user2Albums = await Promise.all([
api.albumApi.create(server, user2.accessToken, { api.albumApi.create(server, user2.accessToken, {
albumName: user2SharedUser, albumName: user2SharedUser,
sharedWithUserIds: [user1.userId], sharedWithUserIds: [user1.userId],
@ -80,16 +79,22 @@ describe(`${AlbumController.name} (e2e)`, () => {
api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }), api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }),
]); ]);
// add shared link to user2SharedLink album user1Albums = albums.slice(0, 3);
await api.sharedLinkApi.create(server, user2.accessToken, { user2Albums = albums.slice(3);
type: SharedLinkType.ALBUM,
albumId: user2Albums[1].id,
});
});
afterAll(async () => { await Promise.all([
await db.disconnect(); // add shared link to user1SharedLink album
await app.close(); 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', () => { describe('GET /album', () => {

View file

@ -12,7 +12,7 @@ import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { errorStub, uuidStub } from '@test/fixtures'; 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 { randomBytes } from 'crypto';
import request from 'supertest'; import request from 'supertest';
@ -86,12 +86,14 @@ describe(`${AssetController.name} (e2e)`, () => {
let asset4: AssetEntity; let asset4: AssetEntity;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server, app] = await testApp.create();
server = app.getHttpServer();
assetRepository = app.get<IAssetRepository>(IAssetRepository); assetRepository = app.get<IAssetRepository>(IAssetRepository);
}); });
afterAll(async () => {
await testApp.teardown();
});
beforeEach(async () => { beforeEach(async () => {
await db.reset(); await db.reset();
await api.authApi.adminSignUp(server); 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', () => { describe('POST /asset/upload', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
@ -589,9 +586,11 @@ describe(`${AssetController.name} (e2e)`, () => {
describe('GET /asset/map-marker', () => { describe('GET /asset/map-marker', () => {
beforeEach(async () => { beforeEach(async () => {
await assetRepository.save({ id: asset1.id, isArchived: true }); await Promise.all([
await assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }); assetRepository.save({ id: asset1.id, isArchived: true }),
await assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }); assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }),
assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }),
]);
}); });
it('should require authentication', async () => { it('should require authentication', async () => {

View file

@ -1,5 +1,4 @@
import { AuthController } from '@app/immich'; import { AuthController } from '@app/immich';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { import {
@ -12,7 +11,7 @@ import {
signupResponseStub, signupResponseStub,
uuidStub, uuidStub,
} from '@test/fixtures'; } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
const firstName = 'Immich'; const firstName = 'Immich';
@ -21,13 +20,16 @@ const password = 'Password123';
const email = 'admin@immich.app'; const email = 'admin@immich.app';
describe(`${AuthController.name} (e2e)`, () => { describe(`${AuthController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let accessToken: string; let accessToken: string;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); await testApp.reset();
server = app.getHttpServer(); [server] = await testApp.create();
});
afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -37,11 +39,6 @@ describe(`${AuthController.name} (e2e)`, () => {
accessToken = response.accessToken; accessToken = response.accessToken;
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('POST /auth/admin-sign-up', () => { describe('POST /auth/admin-sign-up', () => {
beforeEach(async () => { beforeEach(async () => {
await db.reset(); await db.reset();

View file

@ -1,11 +1,9 @@
import { LoginResponseDto } from '@app/domain'; import { LoginResponseDto } from '@app/domain';
import { AssetType, LibraryType } from '@app/infra/entities'; import { AssetType, LibraryType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; 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)`, () => { describe(`Supported file formats (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let admin: LoginResponseDto; let admin: LoginResponseDto;
@ -170,8 +168,11 @@ describe(`Supported file formats (e2e)`, () => {
const testsToRun = formatTests.filter((formatTest) => formatTest.runTest); const testsToRun = formatTests.filter((formatTest) => formatTest.runTest);
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(true); [server] = await testApp.create({ jobs: true });
server = app.getHttpServer(); });
afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -181,11 +182,6 @@ describe(`Supported file formats (e2e)`, () => {
await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); 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) => { it.each(testsToRun)('should import file of format $format', async (testedFormat) => {
const library = await api.libraryApi.create(server, admin.accessToken, { const library = await api.libraryApi.create(server, admin.accessToken, {
type: LibraryType.EXTERNAL, type: LibraryType.EXTERNAL,

View file

@ -1,22 +1,14 @@
import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryResponseDto, LoginResponseDto } from '@app/domain';
import { LibraryController } from '@app/immich'; import { LibraryController } from '@app/immich';
import { AssetType, LibraryType } from '@app/infra/entities'; import { AssetType, LibraryType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, db, restoreTempFolder, testApp } from '@test/test-utils';
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
createTestApp,
db,
restoreTempFolder,
} from '@test/test-utils';
import * as fs from 'fs'; import * as fs from 'fs';
import request from 'supertest'; import request from 'supertest';
import { utimes } from 'utimes'; import { utimes } from 'utimes';
import { errorStub, uuidStub } from '../fixtures'; import { errorStub, uuidStub } from '../fixtures';
describe(`${LibraryController.name} (e2e)`, () => { describe(`${LibraryController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let admin: LoginResponseDto; let admin: LoginResponseDto;
@ -35,8 +27,12 @@ describe(`${LibraryController.name} (e2e)`, () => {
}; };
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(true); [server] = await testApp.create({ jobs: true });
server = app.getHttpServer(); });
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -46,12 +42,6 @@ describe(`${LibraryController.name} (e2e)`, () => {
admin = await api.authApi.adminLogin(server); admin = await api.authApi.adminLogin(server);
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
await restoreTempFolder();
});
describe('GET /library', () => { describe('GET /library', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/library'); const { status, body } = await request(server).get('/library');

View file

@ -1,18 +1,19 @@
import { OAuthController } from '@app/immich'; import { OAuthController } from '@app/immich';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub } from '@test/fixtures'; import { errorStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
describe(`${OAuthController.name} (e2e)`, () => { describe(`${OAuthController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server] = await testApp.create();
server = app.getHttpServer(); });
afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -20,11 +21,6 @@ describe(`${OAuthController.name} (e2e)`, () => {
await api.authApi.adminSignUp(server); await api.authApi.adminSignUp(server);
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('POST /oauth/authorize', () => { describe('POST /oauth/authorize', () => {
beforeEach(async () => { beforeEach(async () => {
await db.reset(); await db.reset();

View file

@ -4,7 +4,7 @@ import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub } from '@test/fixtures'; import { errorStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
const user1Dto = { const user1Dto = {
@ -31,27 +31,29 @@ describe(`${PartnerController.name} (e2e)`, () => {
let user2: LoginResponseDto; let user2: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server, app] = await testApp.create();
server = app.getHttpServer();
repository = app.get<IPartnerRepository>(IPartnerRepository); repository = app.get<IPartnerRepository>(IPartnerRepository);
}); });
afterAll(async () => {
await testApp.teardown();
});
beforeEach(async () => { beforeEach(async () => {
await db.reset(); await db.reset();
await api.authApi.adminSignUp(server); await api.authApi.adminSignUp(server);
loginResponse = await api.authApi.adminLogin(server); loginResponse = await api.authApi.adminLogin(server);
accessToken = loginResponse.accessToken; accessToken = loginResponse.accessToken;
await api.userApi.create(server, accessToken, user1Dto); await Promise.all([
user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); api.userApi.create(server, accessToken, user1Dto),
api.userApi.create(server, accessToken, user2Dto),
]);
await api.userApi.create(server, accessToken, user2Dto); [user1, user2] = await Promise.all([
user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }); api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }),
}); api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }),
]);
afterAll(async () => {
await db.disconnect();
await app.close();
}); });
describe('GET /partner', () => { describe('GET /partner', () => {

View file

@ -5,7 +5,7 @@ import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub, uuidStub } from '@test/fixtures'; import { errorStub, uuidStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
describe(`${PersonController.name}`, () => { describe(`${PersonController.name}`, () => {
@ -18,11 +18,14 @@ describe(`${PersonController.name}`, () => {
let hiddenPerson: PersonEntity; let hiddenPerson: PersonEntity;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server, app] = await testApp.create();
server = app.getHttpServer();
personRepository = app.get<IPersonRepository>(IPersonRepository); personRepository = app.get<IPersonRepository>(IPersonRepository);
}); });
afterAll(async () => {
await testApp.teardown();
});
beforeEach(async () => { beforeEach(async () => {
await db.reset(); await db.reset();
await api.authApi.adminSignUp(server); await api.authApi.adminSignUp(server);
@ -46,11 +49,6 @@ describe(`${PersonController.name}`, () => {
await personRepository.createFace({ assetId: faceAsset.id, personId: hiddenPerson.id }); await personRepository.createFace({ assetId: faceAsset.id, personId: hiddenPerson.id });
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('GET /person', () => { describe('GET /person', () => {
beforeEach(async () => {}); beforeEach(async () => {});

View file

@ -1,21 +1,22 @@
import { LoginResponseDto } from '@app/domain'; import { LoginResponseDto } from '@app/domain';
import { ServerInfoController } from '@app/immich'; import { ServerInfoController } from '@app/immich';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub } from '@test/fixtures'; import { errorStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
describe(`${ServerInfoController.name} (e2e)`, () => { describe(`${ServerInfoController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let accessToken: string; let accessToken: string;
let loginResponse: LoginResponseDto; let loginResponse: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server] = await testApp.create();
server = app.getHttpServer(); });
afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -25,11 +26,6 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
accessToken = loginResponse.accessToken; accessToken = loginResponse.accessToken;
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
});
describe('GET /server-info', () => { describe('GET /server-info', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/server-info'); const { status, body } = await request(server).get('/server-info');

View file

@ -1,5 +1,5 @@
import { PostgreSqlContainer } from '@testcontainers/postgresql'; import { PostgreSqlContainer } from '@testcontainers/postgresql';
import * as fs from 'fs'; import { access } from 'fs/promises';
import path from 'path'; import path from 'path';
export default async () => { export default async () => {
@ -23,8 +23,7 @@ export default async () => {
} }
const directoryExists = async (dirPath: string) => const directoryExists = async (dirPath: string) =>
await fs.promises await access(dirPath)
.access(dirPath)
.then(() => true) .then(() => true)
.catch(() => false); .catch(() => false);

View file

@ -1,16 +1,10 @@
import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain';
import { PartnerController } from '@app/immich'; import { PartnerController } from '@app/immich';
import { LibraryType, SharedLinkType } from '@app/infra/entities'; import { LibraryType, SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub, uuidStub } from '@test/fixtures'; import { errorStub, uuidStub } from '@test/fixtures';
import { import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '@test/test-utils';
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
createTestApp,
restoreTempFolder,
} from '@test/test-utils';
import { cp } from 'fs/promises'; import { cp } from 'fs/promises';
import request from 'supertest'; import request from 'supertest';
@ -22,7 +16,6 @@ const user1Dto = {
}; };
describe(`${PartnerController.name} (e2e)`, () => { describe(`${PartnerController.name} (e2e)`, () => {
let app: INestApplication;
let server: any; let server: any;
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
@ -30,8 +23,12 @@ describe(`${PartnerController.name} (e2e)`, () => {
let sharedLink: SharedLinkResponseDto; let sharedLink: SharedLinkResponseDto;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(true); [server] = await testApp.create({ jobs: true });
server = app.getHttpServer(); });
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
}); });
beforeEach(async () => { beforeEach(async () => {
@ -49,12 +46,6 @@ describe(`${PartnerController.name} (e2e)`, () => {
}); });
}); });
afterAll(async () => {
await db.disconnect();
await app.close();
await restoreTempFolder();
});
describe('GET /shared-link', () => { describe('GET /shared-link', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/shared-link'); const { status, body } = await request(server).get('/shared-link');

View file

@ -2,10 +2,11 @@ import { LoginResponseDto, UserResponseDto, UserService } from '@app/domain';
import { AppModule, UserController } from '@app/immich'; import { AppModule, UserController } from '@app/immich';
import { UserEntity } from '@app/infra/entities'; import { UserEntity } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { api } from '@test/api'; import { api } from '@test/api';
import { db } from '@test/db'; import { db } from '@test/db';
import { errorStub, userSignupStub, userStub } from '@test/fixtures'; import { errorStub, userSignupStub, userStub } from '@test/fixtures';
import { createTestApp } from '@test/test-utils'; import { testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -18,10 +19,12 @@ describe(`${UserController.name}`, () => {
let userRepository: Repository<UserEntity>; let userRepository: Repository<UserEntity>;
beforeAll(async () => { beforeAll(async () => {
app = await createTestApp(); [server, app] = await testApp.create();
userRepository = app.select(AppModule).get('UserEntityRepository'); userRepository = app.select(AppModule).get(getRepositoryToken(UserEntity));
});
server = app.getHttpServer(); afterAll(async () => {
await testApp.teardown();
}); });
beforeEach(async () => { beforeEach(async () => {

View file

@ -5,6 +5,7 @@ export const newMetadataRepositoryMock = (): jest.Mocked<IMetadataRepository> =>
deleteCache: jest.fn(), deleteCache: jest.fn(),
getExifTags: jest.fn(), getExifTags: jest.fn(),
init: jest.fn(), init: jest.fn(),
teardown: jest.fn(),
reverseGeocode: jest.fn(), reverseGeocode: jest.fn(),
}; };
}; };

View file

@ -1,9 +1,8 @@
import { dataSource } from '@app/infra';
import { IJobRepository, JobItem, JobItemHandler, QueueName } from '@app/domain'; import { IJobRepository, JobItem, JobItemHandler, QueueName } from '@app/domain';
import { AppModule } from '@app/immich'; import { AppModule } from '@app/immich';
import { INestApplication, Logger } from '@nestjs/common'; import { dataSource } from '@app/infra';
import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as fs from 'fs'; import * as fs from 'fs';
import path from 'path'; import path from 'path';
import { AppService } from '../src/microservices/app.service'; import { AppService } from '../src/microservices/app.service';
@ -36,38 +35,48 @@ export const db = {
let _handler: JobItemHandler = () => Promise.resolve(); let _handler: JobItemHandler = () => Promise.resolve();
export async function createTestApp(runJobs = false, log = false): Promise<INestApplication> { interface TestAppOptions {
const moduleBuilder = Test.createTestingModule({ jobs: boolean;
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;
} }
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'; export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true';
const directoryExists = async (dirPath: string) => const directoryExists = async (dirPath: string) =>