Browse Source

test: server-info e2e tests (#3948)

Jason Rasmussen 1 year ago
parent
commit
4e5bf7ae2e

+ 12 - 2
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();
+  }
+}

+ 4 - 0
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();
+  }
 }

+ 0 - 2
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;
 

+ 6 - 2
server/test/e2e/jest-e2e.json

@@ -5,11 +5,15 @@
   "globalSetup": "<rootDir>/test/e2e/setup.ts",
   "testEnvironment": "node",
   "testRegex": ".e2e-spec.ts$",
-  "testTimeout": 15000,
+  "testTimeout": 60000,
   "transform": {
     "^.+\\.(t|j)s$": "ts-jest"
   },
-  "collectCoverageFrom": ["<rootDir>/src/**/*.(t|j)s", "!<rootDir>/src/infra/**/*"],
+  "collectCoverageFrom": [
+    "<rootDir>/src/**/*.(t|j)s",
+    "!<rootDir>/src/**/*.spec.(t|s)s",
+    "!<rootDir>/src/infra/migrations/**"
+  ],
   "coverageDirectory": "./coverage",
   "moduleNameMapper": {
     "^@test(|/.*)$": "<rootDir>/test/$1",

+ 148 - 0
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),
+      });
+    });
+  });
+});

+ 1 - 1
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)

+ 5 - 0
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,