From 4e5bf7ae2edb900211625a112f6d1af03df8c936 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 1 Sep 2023 22:01:54 -0400 Subject: [PATCH] test: server-info e2e tests (#3948) --- server/src/immich/app.module.ts | 14 ++- server/src/immich/app.service.ts | 4 + server/src/immich/main.ts | 2 - server/test/e2e/jest-e2e.json | 8 +- server/test/e2e/server-info.e2e-spec.ts | 148 ++++++++++++++++++++++++ server/test/e2e/setup.ts | 2 +- server/test/fixtures/error.stub.ts | 5 + 7 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 server/test/e2e/server-info.e2e-spec.ts diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index 6b08228bd8..809732ba9c 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -1,7 +1,7 @@ import { DomainModule } from '@app/domain'; import { InfraModule } from '@app/infra'; import { AssetEntity, ExifEntity } from '@app/infra/entities'; -import { Module } from '@nestjs/common'; +import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -66,4 +66,14 @@ import { FileUploadInterceptor, ], }) -export class AppModule {} +export class AppModule implements OnModuleInit, OnModuleDestroy { + constructor(private appService: AppService) {} + + async onModuleInit() { + await this.appService.init(); + } + + onModuleDestroy() { + this.appService.destroy(); + } +} diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts index 9e7b149ab2..6f386eb126 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/immich/app.service.ts @@ -23,4 +23,8 @@ export class AppService { await this.searchService.init(); this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); } + + async destroy() { + this.searchService.teardown(); + } } diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts index 5ba2805ecc..f262a173cd 100644 --- a/server/src/immich/main.ts +++ b/server/src/immich/main.ts @@ -6,7 +6,6 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { json } from 'body-parser'; import cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; -import { AppService } from './app.service'; import { useSwagger } from './app.utils'; const logger = new Logger('ImmichServer'); @@ -27,7 +26,6 @@ export async function bootstrap() { app.useWebSocketAdapter(new RedisIoAdapter(app)); useSwagger(app, isDev); - await app.get(AppService).init(); const server = await app.listen(port); server.requestTimeout = 30 * 60 * 1000; diff --git a/server/test/e2e/jest-e2e.json b/server/test/e2e/jest-e2e.json index 3c536deffe..e860a7a6f3 100644 --- a/server/test/e2e/jest-e2e.json +++ b/server/test/e2e/jest-e2e.json @@ -5,11 +5,15 @@ "globalSetup": "/test/e2e/setup.ts", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", - "testTimeout": 15000, + "testTimeout": 60000, "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": ["/src/**/*.(t|j)s", "!/src/infra/**/*"], + "collectCoverageFrom": [ + "/src/**/*.(t|j)s", + "!/src/**/*.spec.(t|s)s", + "!/src/infra/migrations/**" + ], "coverageDirectory": "./coverage", "moduleNameMapper": { "^@test(|/.*)$": "/test/$1", diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts new file mode 100644 index 0000000000..d04415dc69 --- /dev/null +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -0,0 +1,148 @@ +import { LoginResponseDto } from '@app/domain'; +import { AppModule, ServerInfoController } from '@app/immich'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { errorStub } from '../fixtures'; +import { api, db } from '../test-utils'; + +describe(`${ServerInfoController.name} (e2e)`, () => { + let app: INestApplication; + let server: any; + let accessToken: string; + let loginResponse: LoginResponseDto; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = await moduleFixture.createNestApplication().init(); + server = app.getHttpServer(); + }); + + beforeEach(async () => { + await db.reset(); + await api.adminSignUp(server); + loginResponse = await api.adminLogin(server); + 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'); + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should return the disk information', async () => { + const { status, body } = await request(server).get('/server-info').set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + diskAvailable: expect.any(String), + diskAvailableRaw: expect.any(Number), + diskSize: expect.any(String), + diskSizeRaw: expect.any(Number), + diskUsagePercentage: expect.any(Number), + diskUse: expect.any(String), + diskUseRaw: expect.any(Number), + }); + }); + }); + + describe('GET /server-info/ping', () => { + it('should respond with pong', async () => { + const { status, body } = await request(server).get('/server-info/ping'); + expect(status).toBe(200); + expect(body).toEqual({ res: 'pong' }); + }); + }); + + describe('GET /server-info/version', () => { + it('should respond with the server version', async () => { + const { status, body } = await request(server).get('/server-info/version'); + expect(status).toBe(200); + expect(body).toEqual({ + major: expect.any(Number), + minor: expect.any(Number), + patch: expect.any(Number), + }); + }); + }); + + describe('GET /server-info/features', () => { + it('should respond with the server features', async () => { + const { status, body } = await request(server).get('/server-info/features'); + expect(status).toBe(200); + expect(body).toEqual({ + clipEncode: true, + configFile: false, + facialRecognition: true, + oauth: false, + oauthAutoLaunch: false, + passwordLogin: true, + search: false, + sidecar: true, + tagImage: true, + }); + }); + }); + + describe('GET /server-info/stats', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).get('/server-info/stats'); + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should only work for admins', async () => { + const loginDto = { email: 'test@immich.app', password: 'Immich123' }; + await api.userApi.create(server, accessToken, { ...loginDto, firstName: 'test', lastName: 'test' }); + const { accessToken: userAccessToken } = await api.login(server, loginDto); + const { status, body } = await request(server) + .get('/server-info/stats') + .set('Authorization', `Bearer ${userAccessToken}`); + expect(status).toBe(403); + expect(body).toEqual(errorStub.forbidden); + }); + + it('should return the server stats', async () => { + const { status, body } = await request(server) + .get('/server-info/stats') + .set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + photos: 0, + usage: 0, + usageByUser: [ + { + photos: 0, + usage: 0, + userFirstName: 'Immich', + userId: loginResponse.userId, + userLastName: 'Admin', + videos: 0, + }, + ], + videos: 0, + }); + }); + }); + + describe('GET /server-info/media-types', () => { + it('should return accepted media types', async () => { + const { status, body } = await request(server).get('/server-info/media-types'); + expect(status).toBe(200); + expect(body).toEqual({ + sidecar: ['.xmp'], + image: expect.any(Array), + video: expect.any(Array), + }); + }); + }); +}); diff --git a/server/test/e2e/setup.ts b/server/test/e2e/setup.ts index 6c2395b688..ce0aa348f1 100644 --- a/server/test/e2e/setup.ts +++ b/server/test/e2e/setup.ts @@ -2,7 +2,7 @@ import { PostgreSqlContainer } from '@testcontainers/postgresql'; import { GenericContainer } from 'testcontainers'; export default async () => { process.env.NODE_ENV = 'development'; - process.env.TYPESENSE_API_KEY = 'abc123'; + process.env.TYPESENSE_ENABLED = 'false'; const pg = await new PostgreSqlContainer('postgres') .withExposedPorts(5432) diff --git a/server/test/fixtures/error.stub.ts b/server/test/fixtures/error.stub.ts index fab9451a78..4cfc958090 100644 --- a/server/test/fixtures/error.stub.ts +++ b/server/test/fixtures/error.stub.ts @@ -4,6 +4,11 @@ export const errorStub = { statusCode: 401, message: 'Authentication required', }, + forbidden: { + error: 'Forbidden', + statusCode: 403, + message: expect.any(String), + }, wrongPassword: { error: 'Bad Request', statusCode: 400,