diff --git a/Makefile b/Makefile index 39141ef4bd..e66714a20f 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ pull-stage: server-e2e-jobs: docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build +server-e2e-api: + npm run e2e:api --prefix server + prod: docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans diff --git a/cli/test/e2e/jest-e2e.json b/cli/test/e2e/jest-e2e.json index 6146c09876..aee8fb458d 100644 --- a/cli/test/e2e/jest-e2e.json +++ b/cli/test/e2e/jest-e2e.json @@ -16,6 +16,7 @@ ], "coverageDirectory": "./coverage", "moduleNameMapper": { + "^@api(|/.*)$": "../server/e2e/client/$1", "^@test(|/.*)$": "../server/test/$1", "^@app/immich(|/.*)$": "../server/src/immich/$1", "^@app/infra(|/.*)$": "../server/src/infra/$1", diff --git a/cli/test/e2e/login-key.e2e-spec.ts b/cli/test/e2e/login-key.e2e-spec.ts index cd41cf7294..30e12616f6 100644 --- a/cli/test/e2e/login-key.e2e-spec.ts +++ b/cli/test/e2e/login-key.e2e-spec.ts @@ -1,5 +1,5 @@ import { APIKeyCreateResponseDto } from '@app/domain'; -import { api } from '@test/../e2e/api/client'; +import { api } from '@api'; import { restoreTempFolder, testApp } from '@test/../e2e/jobs/utils'; import { LoginResponseDto } from '@immich/sdk'; import { LoginKey } from 'src/commands/login/key'; diff --git a/cli/test/e2e/server-info.e2e-spec.ts b/cli/test/e2e/server-info.e2e-spec.ts index ab5506491d..ed2e8d0483 100644 --- a/cli/test/e2e/server-info.e2e-spec.ts +++ b/cli/test/e2e/server-info.e2e-spec.ts @@ -1,5 +1,5 @@ import { APIKeyCreateResponseDto } from '@app/domain'; -import { api } from '@test/../e2e/api/client'; +import { api } from '@api'; import { restoreTempFolder, testApp } from '@test/../e2e/jobs/utils'; import { LoginResponseDto } from '@immich/sdk'; import { ServerInfo } from 'src/commands/server-info'; diff --git a/cli/test/e2e/upload.e2e-spec.ts b/cli/test/e2e/upload.e2e-spec.ts index cd80b14b21..aef6a6f7f1 100644 --- a/cli/test/e2e/upload.e2e-spec.ts +++ b/cli/test/e2e/upload.e2e-spec.ts @@ -1,5 +1,5 @@ import { APIKeyCreateResponseDto } from '@app/domain'; -import { api } from '@test/../e2e/api/client'; +import { api } from '@api'; import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test/../e2e/jobs/utils'; import { LoginResponseDto } from '@immich/sdk'; import { Upload } from 'src/commands/upload'; diff --git a/cli/tsconfig.json b/cli/tsconfig.json index b570c1157b..085c7596e4 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -17,6 +17,8 @@ "rootDirs": ["src", "../server/src"], "baseUrl": "./", "paths": { + "@api": ["../server/e2e/client"], + "@api/*": ["../server/e2e/client/*"], "@test": ["../server/test"], "@test/*": ["../server/test/*"], "@app/immich": ["../server/src/immich"], diff --git a/docs/docs/developer/testing.md b/docs/docs/developer/testing.md index 27cbf71bb3..9a17345a44 100644 --- a/docs/docs/developer/testing.md +++ b/docs/docs/developer/testing.md @@ -8,10 +8,20 @@ Unit are run by calling `npm run test` from the `server` directory. ### End to end tests -The backend has an end-to-end test suite that can be called with `npm run test:e2e` from the `server` directory. This will set up a dummy database inside a temporary container and run the tests against it. Setup and teardown is automatically taken care of. That test, however, can not set up all prerequisites to parse file formats, as that is very complex and error-prone. As such, this test excludes some test cases like HEIC file imports. The test suite will also print a friendly warning to remind you that not all tests are being run. +The backend has two end-to-end test suites that can be called with the following two commands from the project root directory: +- `make server-e2e-api` +- `make server-e2e-jobs` + +#### API (e2e) + +The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints. + +#### Jobs (e2e) + +The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated. + +:::note Note that there is a bug in nodejs \<20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8. - -To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perform the tests and exit. - -If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run e2e:jobs`. +::: +/follow diff --git a/server/bin/immich-test b/server/bin/immich-test new file mode 100755 index 0000000000..e469e845ba --- /dev/null +++ b/server/bin/immich-test @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +NODE_OPTIONS='--experimental-vm-modules' node /usr/src/app/node_modules/.bin/jest --config e2e/$1/jest-e2e.json --runInBand diff --git a/server/e2e/api/specs/activity.e2e-spec.ts b/server/e2e/api/specs/activity.e2e-spec.ts index 03c6799b5b..47d2d7a199 100644 --- a/server/e2e/api/specs/activity.e2e-spec.ts +++ b/server/e2e/api/specs/activity.e2e-spec.ts @@ -4,7 +4,7 @@ import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dt import { ActivityEntity } from '@app/infra/entities'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${ActivityController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/album.e2e-spec.ts b/server/e2e/api/specs/album.e2e-spec.ts index 0df753a846..312816035c 100644 --- a/server/e2e/api/specs/album.e2e-spec.ts +++ b/server/e2e/api/specs/album.e2e-spec.ts @@ -4,7 +4,7 @@ import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dt import { SharedLinkType } from '@app/infra/entities'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; const user1SharedUser = 'user1SharedUser'; diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index bd3d0a8a39..4fc9a0e16b 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -16,7 +16,7 @@ import { INestApplication } from '@nestjs/common'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; import { randomBytes } from 'crypto'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { generateAsset, testApp, today, yesterday } from '../utils'; const makeUploadDto = (options?: { omit: string }): Record => { diff --git a/server/e2e/api/specs/auth.e2e-spec.ts b/server/e2e/api/specs/auth.e2e-spec.ts index 179fef7951..eeb4723faa 100644 --- a/server/e2e/api/specs/auth.e2e-spec.ts +++ b/server/e2e/api/specs/auth.e2e-spec.ts @@ -9,7 +9,7 @@ import { uuidStub, } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; const name = 'Immich Admin'; diff --git a/server/e2e/api/specs/library.e2e-spec.ts b/server/e2e/api/specs/library.e2e-spec.ts index 114c0a5bb7..0ce192e479 100644 --- a/server/e2e/api/specs/library.e2e-spec.ts +++ b/server/e2e/api/specs/library.e2e-spec.ts @@ -3,7 +3,7 @@ import { LibraryController } from '@app/immich'; import { LibraryType } from '@app/infra/entities'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${LibraryController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/oauth.e2e-spec.ts b/server/e2e/api/specs/oauth.e2e-spec.ts index 75bde10853..b6136971d0 100644 --- a/server/e2e/api/specs/oauth.e2e-spec.ts +++ b/server/e2e/api/specs/oauth.e2e-spec.ts @@ -1,7 +1,7 @@ import { OAuthController } from '@app/immich'; import { errorStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${OAuthController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/partner.e2e-spec.ts b/server/e2e/api/specs/partner.e2e-spec.ts index 60c62a2af3..b254aa61f2 100644 --- a/server/e2e/api/specs/partner.e2e-spec.ts +++ b/server/e2e/api/specs/partner.e2e-spec.ts @@ -2,7 +2,7 @@ import { LoginResponseDto, PartnerDirection } from '@app/domain'; import { PartnerController } from '@app/immich'; import { errorStub, userDto } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${PartnerController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/person.e2e-spec.ts b/server/e2e/api/specs/person.e2e-spec.ts index f8b8691d76..51c8704281 100644 --- a/server/e2e/api/specs/person.e2e-spec.ts +++ b/server/e2e/api/specs/person.e2e-spec.ts @@ -4,7 +4,7 @@ import { PersonEntity } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { errorStub, uuidStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${PersonController.name}`, () => { diff --git a/server/e2e/api/specs/search.e2e-spec.ts b/server/e2e/api/specs/search.e2e-spec.ts index 3b73b29524..8d17ee9304 100644 --- a/server/e2e/api/specs/search.e2e-spec.ts +++ b/server/e2e/api/specs/search.e2e-spec.ts @@ -10,7 +10,7 @@ import { SearchController } from '@app/immich'; import { INestApplication } from '@nestjs/common'; import { errorStub, searchStub } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { generateAsset, testApp } from '../utils'; describe(`${SearchController.name}`, () => { diff --git a/server/e2e/api/specs/server-info.e2e-spec.ts b/server/e2e/api/specs/server-info.e2e-spec.ts index 015fd53dcd..d142704346 100644 --- a/server/e2e/api/specs/server-info.e2e-spec.ts +++ b/server/e2e/api/specs/server-info.e2e-spec.ts @@ -2,7 +2,7 @@ import { LoginResponseDto } from '@app/domain'; import { ServerInfoController } from '@app/immich'; import { errorStub, userDto } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${ServerInfoController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/shared-link.e2e-spec.ts b/server/e2e/api/specs/shared-link.e2e-spec.ts index 9d4bdae04e..034b2f2637 100644 --- a/server/e2e/api/specs/shared-link.e2e-spec.ts +++ b/server/e2e/api/specs/shared-link.e2e-spec.ts @@ -11,7 +11,7 @@ import { INestApplication } from '@nestjs/common'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; import { DateTime } from 'luxon'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${SharedLinkController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/system-config.e2e-spec.ts b/server/e2e/api/specs/system-config.e2e-spec.ts index fe1470f7f5..a6b7387091 100644 --- a/server/e2e/api/specs/system-config.e2e-spec.ts +++ b/server/e2e/api/specs/system-config.e2e-spec.ts @@ -2,7 +2,7 @@ import { LoginResponseDto } from '@app/domain'; import { SystemConfigController } from '@app/immich'; import { errorStub, userDto } from '@test/fixtures'; import request from 'supertest'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${SystemConfigController.name} (e2e)`, () => { diff --git a/server/e2e/api/specs/user.e2e-spec.ts b/server/e2e/api/specs/user.e2e-spec.ts index 35a9a56e2b..cd7c8147a3 100644 --- a/server/e2e/api/specs/user.e2e-spec.ts +++ b/server/e2e/api/specs/user.e2e-spec.ts @@ -6,7 +6,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { errorStub, userDto, userSignupStub, userStub } from '@test/fixtures'; import request from 'supertest'; import { Repository } from 'typeorm'; -import { api } from '../client'; +import { api } from '../../client'; import { testApp } from '../utils'; describe(`${UserController.name}`, () => { diff --git a/server/e2e/api/client/activity-api.ts b/server/e2e/client/activity-api.ts similarity index 100% rename from server/e2e/api/client/activity-api.ts rename to server/e2e/client/activity-api.ts diff --git a/server/e2e/api/client/album-api.ts b/server/e2e/client/album-api.ts similarity index 100% rename from server/e2e/api/client/album-api.ts rename to server/e2e/client/album-api.ts diff --git a/server/e2e/api/client/api-key-api.ts b/server/e2e/client/api-key-api.ts similarity index 100% rename from server/e2e/api/client/api-key-api.ts rename to server/e2e/client/api-key-api.ts diff --git a/server/e2e/api/client/asset-api.ts b/server/e2e/client/asset-api.ts similarity index 87% rename from server/e2e/api/client/asset-api.ts rename to server/e2e/client/asset-api.ts index e40a8c150d..b9ae897b33 100644 --- a/server/e2e/api/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -4,7 +4,7 @@ import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dt import { randomBytes } from 'crypto'; import request from 'supertest'; -type UploadDto = Partial & { content?: Buffer }; +type UploadDto = Partial & { content?: Buffer; filename?: string }; const asset = { deviceAssetId: 'test-1', @@ -45,19 +45,19 @@ export const assetApi = { expect(status).toBe(200); return body as AssetResponseDto[]; }, - upload: async (server: any, accessToken: string, id: string, dto: UploadDto = {}) => { - const { content, isFavorite = false, isArchived = false } = dto; + upload: async (server: any, accessToken: string, deviceAssetId: string, dto: UploadDto = {}) => { + const { content, filename, isFavorite = false, isArchived = false } = dto; const { body, status } = await request(server) .post('/asset/upload') .set('Authorization', `Bearer ${accessToken}`) - .field('deviceAssetId', id) + .field('deviceAssetId', deviceAssetId) .field('deviceId', 'TEST') .field('fileCreatedAt', new Date().toISOString()) .field('fileModifiedAt', new Date().toISOString()) .field('isFavorite', isFavorite) .field('isArchived', isArchived) .field('duration', '0:00:00.000000') - .attach('assetData', content || randomBytes(32), 'example.jpg'); + .attach('assetData', content || randomBytes(32), filename || 'example.jpg'); expect(status).toBe(201); return body as AssetFileUploadResponseDto; diff --git a/server/e2e/api/client/auth-api.ts b/server/e2e/client/auth-api.ts similarity index 100% rename from server/e2e/api/client/auth-api.ts rename to server/e2e/client/auth-api.ts diff --git a/server/e2e/api/client/index.ts b/server/e2e/client/index.ts similarity index 100% rename from server/e2e/api/client/index.ts rename to server/e2e/client/index.ts diff --git a/server/e2e/api/client/library-api.ts b/server/e2e/client/library-api.ts similarity index 100% rename from server/e2e/api/client/library-api.ts rename to server/e2e/client/library-api.ts diff --git a/server/e2e/api/client/partner-api.ts b/server/e2e/client/partner-api.ts similarity index 100% rename from server/e2e/api/client/partner-api.ts rename to server/e2e/client/partner-api.ts diff --git a/server/e2e/api/client/server-info-api.ts b/server/e2e/client/server-info-api.ts similarity index 100% rename from server/e2e/api/client/server-info-api.ts rename to server/e2e/client/server-info-api.ts diff --git a/server/e2e/api/client/shared-link-api.ts b/server/e2e/client/shared-link-api.ts similarity index 100% rename from server/e2e/api/client/shared-link-api.ts rename to server/e2e/client/shared-link-api.ts diff --git a/server/e2e/api/client/user-api.ts b/server/e2e/client/user-api.ts similarity index 100% rename from server/e2e/api/client/user-api.ts rename to server/e2e/client/user-api.ts diff --git a/server/e2e/docker-compose.server-e2e.yml b/server/e2e/docker-compose.server-e2e.yml index 854f592351..c9d656cedb 100644 --- a/server/e2e/docker-compose.server-e2e.yml +++ b/server/e2e/docker-compose.server-e2e.yml @@ -9,8 +9,7 @@ services: context: ../../ dockerfile: server/Dockerfile target: dev - entrypoint: ['/usr/local/bin/npm', 'run'] - command: e2e:jobs + command: ['/usr/src/app/bin/immich-test', 'jobs'] volumes: - /usr/src/app/node_modules environment: @@ -18,7 +17,6 @@ services: - DB_USERNAME=postgres - DB_PASSWORD=postgres - DB_DATABASE_NAME=e2e_test - - IMMICH_RUN_ALL_TESTS=true depends_on: - database diff --git a/server/e2e/jobs/client/asset-api.ts b/server/e2e/jobs/client/asset-api.ts deleted file mode 100644 index e40a8c150d..0000000000 --- a/server/e2e/jobs/client/asset-api.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { AssetResponseDto } from '@app/domain'; -import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto'; -import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { randomBytes } from 'crypto'; -import request from 'supertest'; - -type UploadDto = Partial & { content?: Buffer }; - -const asset = { - deviceAssetId: 'test-1', - deviceId: 'test', - fileCreatedAt: new Date(), - fileModifiedAt: new Date(), -}; - -export const assetApi = { - create: async ( - server: any, - accessToken: string, - dto?: Omit, - ): Promise => { - dto = dto || asset; - const { status, body } = await request(server) - .post(`/asset/upload`) - .field('deviceAssetId', dto.deviceAssetId) - .field('deviceId', dto.deviceId) - .field('fileCreatedAt', dto.fileCreatedAt.toISOString()) - .field('fileModifiedAt', dto.fileModifiedAt.toISOString()) - .attach('assetData', randomBytes(32), 'example.jpg') - .set('Authorization', `Bearer ${accessToken}`); - - expect([200, 201].includes(status)).toBe(true); - - return body as AssetResponseDto; - }, - get: async (server: any, accessToken: string, id: string): Promise => { - const { body, status } = await request(server) - .get(`/asset/assetById/${id}`) - .set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body as AssetResponseDto; - }, - getAllAssets: async (server: any, accessToken: string) => { - const { body, status } = await request(server).get(`/asset/`).set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body as AssetResponseDto[]; - }, - upload: async (server: any, accessToken: string, id: string, dto: UploadDto = {}) => { - const { content, isFavorite = false, isArchived = false } = dto; - const { body, status } = await request(server) - .post('/asset/upload') - .set('Authorization', `Bearer ${accessToken}`) - .field('deviceAssetId', id) - .field('deviceId', 'TEST') - .field('fileCreatedAt', new Date().toISOString()) - .field('fileModifiedAt', new Date().toISOString()) - .field('isFavorite', isFavorite) - .field('isArchived', isArchived) - .field('duration', '0:00:00.000000') - .attach('assetData', content || randomBytes(32), 'example.jpg'); - - expect(status).toBe(201); - return body as AssetFileUploadResponseDto; - }, - getWebpThumbnail: async (server: any, accessToken: string, assetId: string) => { - const { body, status } = await request(server) - .get(`/asset/thumbnail/${assetId}`) - .set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body; - }, - getJpegThumbnail: async (server: any, accessToken: string, assetId: string) => { - const { body, status } = await request(server) - .get(`/asset/thumbnail/${assetId}?format=JPEG`) - .set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body; - }, -}; diff --git a/server/e2e/jobs/client/auth-api.ts b/server/e2e/jobs/client/auth-api.ts deleted file mode 100644 index 3043c941f2..0000000000 --- a/server/e2e/jobs/client/auth-api.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AuthDeviceResponseDto, LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain'; -import { adminSignupStub, loginResponseStub, loginStub } from '@test'; -import request from 'supertest'; - -export const authApi = { - adminSignUp: async (server: any) => { - const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub); - - expect(status).toBe(201); - - return body as UserResponseDto; - }, - adminLogin: async (server: any) => { - const { status, body } = await request(server).post('/auth/login').send(loginStub.admin); - - expect(body).toEqual(loginResponseStub.admin.response); - expect(body).toMatchObject({ accessToken: expect.any(String) }); - expect(status).toBe(201); - - return body as LoginResponseDto; - }, - login: async (server: any, dto: LoginCredentialDto) => { - const { status, body } = await request(server).post('/auth/login').send(dto); - - expect(status).toEqual(201); - expect(body).toMatchObject({ accessToken: expect.any(String) }); - - return body as LoginResponseDto; - }, - getAuthDevices: async (server: any, accessToken: string) => { - const { status, body } = await request(server).get('/auth/devices').set('Authorization', `Bearer ${accessToken}`); - - expect(body).toEqual(expect.any(Array)); - expect(status).toBe(200); - - return body as AuthDeviceResponseDto[]; - }, - validateToken: async (server: any, accessToken: string) => { - const { status, body } = await request(server) - .post('/auth/validateToken') - .set('Authorization', `Bearer ${accessToken}`); - expect(body).toEqual({ authStatus: true }); - expect(status).toBe(200); - }, -}; diff --git a/server/e2e/jobs/client/index.ts b/server/e2e/jobs/client/index.ts deleted file mode 100644 index 6040d026d7..0000000000 --- a/server/e2e/jobs/client/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { assetApi } from './asset-api'; -import { authApi } from './auth-api'; -import { libraryApi } from './library-api'; -import { userApi } from './user-api'; - -export const api = { - authApi, - assetApi, - libraryApi, - userApi, -}; diff --git a/server/e2e/jobs/client/library-api.ts b/server/e2e/jobs/client/library-api.ts deleted file mode 100644 index d70e7bd623..0000000000 --- a/server/e2e/jobs/client/library-api.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { CreateLibraryDto, LibraryResponseDto, LibraryStatsResponseDto, ScanLibraryDto } from '@app/domain'; -import request from 'supertest'; - -export const libraryApi = { - getAll: async (server: any, accessToken: string) => { - const { body, status } = await request(server).get(`/library/`).set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body as LibraryResponseDto[]; - }, - create: async (server: any, accessToken: string, dto: CreateLibraryDto) => { - const { body, status } = await request(server) - .post(`/library/`) - .set('Authorization', `Bearer ${accessToken}`) - .send(dto); - expect(status).toBe(201); - return body as LibraryResponseDto; - }, - setImportPaths: async (server: any, accessToken: string, id: string, importPaths: string[]) => { - const { body, status } = await request(server) - .put(`/library/${id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ importPaths }); - expect(status).toBe(200); - return body as LibraryResponseDto; - }, - scanLibrary: async (server: any, accessToken: string, id: string, dto: ScanLibraryDto = {}) => { - const { status } = await request(server) - .post(`/library/${id}/scan`) - .set('Authorization', `Bearer ${accessToken}`) - .send(dto); - expect(status).toBe(201); - }, - removeOfflineFiles: async (server: any, accessToken: string, id: string) => { - const { status } = await request(server) - .post(`/library/${id}/removeOffline`) - .set('Authorization', `Bearer ${accessToken}`) - .send(); - expect(status).toBe(201); - }, - getLibraryStatistics: async (server: any, accessToken: string, id: string): Promise => { - const { body, status } = await request(server) - .get(`/library/${id}/statistics`) - .set('Authorization', `Bearer ${accessToken}`); - expect(status).toBe(200); - return body; - }, -}; diff --git a/server/e2e/jobs/client/user-api.ts b/server/e2e/jobs/client/user-api.ts deleted file mode 100644 index 5ed0838f75..0000000000 --- a/server/e2e/jobs/client/user-api.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CreateUserDto, UpdateUserDto, UserResponseDto } from '@app/domain'; -import request from 'supertest'; - -export const userApi = { - create: async (server: any, accessToken: string, dto: CreateUserDto) => { - const { status, body } = await request(server) - .post('/user') - .set('Authorization', `Bearer ${accessToken}`) - .send(dto); - - expect(status).toBe(201); - expect(body).toMatchObject({ - id: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - email: dto.email, - }); - - return body as UserResponseDto; - }, - get: async (server: any, accessToken: string, id: string) => { - const { status, body } = await request(server) - .get(`/user/info/${id}`) - .set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ id }); - - return body as UserResponseDto; - }, - update: async (server: any, accessToken: string, dto: UpdateUserDto) => { - const { status, body } = await request(server).put('/user').set('Authorization', `Bearer ${accessToken}`).send(dto); - - expect(status).toBe(200); - expect(body).toMatchObject({ id: dto.id }); - - return body as UserResponseDto; - }, - setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => { - return await userApi.update(server, accessToken, { id, externalPath }); - }, - delete: async (server: any, accessToken: string, id: string) => { - const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ id, deletedAt: expect.any(String) }); - - return body as UserResponseDto; - }, -}; diff --git a/server/e2e/jobs/setup.ts b/server/e2e/jobs/setup.ts index e3f9ef17fc..601d99cc28 100644 --- a/server/e2e/jobs/setup.ts +++ b/server/e2e/jobs/setup.ts @@ -3,16 +3,6 @@ import { access } from 'fs/promises'; import path from 'path'; export default async () => { - const allTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true'; - - if (!allTests) { - console.warn( - `\n\n - *** Not running all server e2e tests. Run 'make test-e2e' to run all tests inside Docker (recommended)\n - *** or set 'IMMICH_RUN_ALL_TESTS=true' to run all tests (requires dependencies to be installed)\n`, - ); - } - let IMMICH_TEST_ASSET_PATH: string = ''; if (process.env.IMMICH_TEST_ASSET_PATH === undefined) { diff --git a/server/e2e/jobs/specs/formats.e2e-spec.ts b/server/e2e/jobs/specs/formats.e2e-spec.ts index eb7f623c97..6f862c050d 100644 --- a/server/e2e/jobs/specs/formats.e2e-spec.ts +++ b/server/e2e/jobs/specs/formats.e2e-spec.ts @@ -1,84 +1,43 @@ import { LoginResponseDto } from '@app/domain'; -import { AssetType, LibraryType } from '@app/infra/entities'; -import { api } from '../client'; -import { IMMICH_TEST_ASSET_PATH, runAllTests, testApp } from '../utils'; +import { AssetType } from '@app/infra/entities'; +import { readFile } from 'fs/promises'; +import { basename, join } from 'path'; +import { api } from '../../client'; +import { IMMICH_TEST_ASSET_PATH, testApp } from '../utils'; -describe(`Supported file formats (e2e)`, () => { - let server: any; - let admin: LoginResponseDto; +const JPEG = { + type: AssetType.IMAGE, + originalFileName: 'el_torcal_rocks', + resized: true, + exifInfo: { + dateTimeOriginal: '2012-08-05T11:39:59.000Z', + exifImageWidth: 512, + exifImageHeight: 341, + latitude: null, + longitude: null, + focalLength: 75, + iso: 200, + fNumber: 11, + exposureTime: '1/160', + fileSizeInByte: 53493, + make: 'SONY', + model: 'DSLR-A550', + orientation: null, + description: 'SONY DSC', + }, +}; - interface FormatTest { - format: string; - path: string; - runTest: boolean; - expectedAsset: any; - expectedExif: any; - } - - const formatTests: FormatTest[] = [ - { - format: 'jpg', - path: 'jpg', - runTest: true, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - resized: true, - }, - expectedExif: { - dateTimeOriginal: '2012-08-05T11:39:59.000Z', - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - focalLength: 75, - iso: 200, - fNumber: 11, - exposureTime: '1/160', - fileSizeInByte: 53493, - make: 'SONY', - model: 'DSLR-A550', - orientation: null, - description: 'SONY DSC', - }, - }, - { - format: 'jpeg', - path: 'jpeg', - runTest: true, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - resized: true, - }, - expectedExif: { - dateTimeOriginal: '2012-08-05T11:39:59.000Z', - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - focalLength: 75, - iso: 200, - fNumber: 11, - exposureTime: '1/160', - fileSizeInByte: 53493, - make: 'SONY', - model: 'DSLR-A550', - orientation: null, - description: 'SONY DSC', - }, - }, - { - format: 'heic', - path: 'heic', - runTest: runAllTests, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'IMG_2682', - resized: true, - fileCreatedAt: '2019-03-21T16:04:22.348Z', - }, - expectedExif: { +const tests = [ + { input: 'formats/jpg/el_torcal_rocks.jpg', expected: JPEG }, + { input: 'formats/jpeg/el_torcal_rocks.jpeg', expected: JPEG }, + { + input: 'formats/heic/IMG_2682.heic', + expected: { + type: AssetType.IMAGE, + originalFileName: 'IMG_2682', + resized: true, + fileCreatedAt: '2019-03-21T16:04:22.348Z', + exifInfo: { dateTimeOriginal: '2019-03-21T16:04:22.348Z', exifImageWidth: 4032, exifImageHeight: 3024, @@ -95,16 +54,14 @@ describe(`Supported file formats (e2e)`, () => { timeZone: 'America/Chicago', }, }, - { - format: 'png', - path: 'png', - runTest: true, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'density_plot', - resized: true, - }, - expectedExif: { + }, + { + input: 'formats/png/density_plot.png', + expected: { + type: AssetType.IMAGE, + originalFileName: 'density_plot', + resized: true, + exifInfo: { exifImageWidth: 800, exifImageHeight: 800, latitude: null, @@ -112,17 +69,15 @@ describe(`Supported file formats (e2e)`, () => { fileSizeInByte: 25408, }, }, - { - format: 'nef (Nikon D80)', - path: 'raw/Nikon/D80', - runTest: true, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'glarus', - resized: true, - fileCreatedAt: '2010-07-20T17:27:12.000Z', - }, - expectedExif: { + }, + { + input: 'formats/raw/Nikon/D80/glarus.nef', + expected: { + type: AssetType.IMAGE, + originalFileName: 'glarus', + resized: true, + fileCreatedAt: '2010-07-20T17:27:12.000Z', + exifInfo: { make: 'NIKON CORPORATION', model: 'NIKON D80', exposureTime: '1/200', @@ -136,17 +91,15 @@ describe(`Supported file formats (e2e)`, () => { orientation: '1', }, }, - { - format: 'nef (Nikon D700)', - path: 'raw/Nikon/D700', - runTest: true, - expectedAsset: { - type: AssetType.IMAGE, - originalFileName: 'philadelphia', - resized: true, - fileCreatedAt: '2016-09-22T22:10:29.060Z', - }, - expectedExif: { + }, + { + input: 'formats/raw/Nikon/D700/philadelphia.nef', + expected: { + type: AssetType.IMAGE, + originalFileName: 'philadelphia', + resized: true, + fileCreatedAt: '2016-09-22T22:10:29.060Z', + exifInfo: { make: 'NIKON CORPORATION', model: 'NIKON D700', exposureTime: '1/400', @@ -161,41 +114,45 @@ describe(`Supported file formats (e2e)`, () => { timeZone: 'UTC-5', }, }, - ]; + }, +]; - // Only run tests with runTest = true - const testsToRun = formatTests.filter((formatTest) => formatTest.runTest); +describe(`Format (e2e)`, () => { + let server: any; + let admin: LoginResponseDto; beforeAll(async () => { - server = (await testApp.create({ jobs: true })).getHttpServer(); - }); - - afterAll(async () => { - await testApp.teardown(); + const app = await testApp.create(); + server = app.getHttpServer(); }); beforeEach(async () => { await testApp.reset(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); }); - it.each(testsToRun)('should import file of format $format', async (testedFormat) => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/formats/${testedFormat.path}`], + afterAll(async () => { + await testApp.teardown(); + }); + + for (const { input, expected } of tests) { + it(`should generate a thumbnail for ${input}`, async () => { + const filepath = join(IMMICH_TEST_ASSET_PATH, input); + const content = await readFile(filepath); + await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { + content, + filename: basename(filepath), + }); + const assets = await api.assetApi.getAllAssets(server, admin.accessToken); + + expect(assets).toHaveLength(1); + + const asset = assets[0]; + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo).toMatchObject(expected.exifInfo); + expect(asset).toMatchObject(expected); }); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {}); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual([ - expect.objectContaining({ - ...testedFormat.expectedAsset, - exifInfo: expect.objectContaining(testedFormat.expectedExif), - }), - ]); - }); + } }); diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index 1ed1355dfc..21bf994239 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -5,7 +5,7 @@ import { errorStub, uuidStub } from '@test/fixtures'; import * as fs from 'fs'; import request from 'supertest'; import { utimes } from 'utimes'; -import { api } from '../client'; +import { api } from '../../client'; import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '../utils'; describe(`${LibraryController.name} (e2e)`, () => { @@ -13,7 +13,7 @@ describe(`${LibraryController.name} (e2e)`, () => { let admin: LoginResponseDto; beforeAll(async () => { - server = (await testApp.create({ jobs: true })).getHttpServer(); + server = (await testApp.create()).getHttpServer(); }); afterAll(async () => { @@ -148,48 +148,6 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); - it('should scan external library with import paths', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - }); - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - libraryId: library.id, - resized: true, - exifInfo: expect.objectContaining({ - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - }), - }), - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'silver_fir', - libraryId: library.id, - resized: true, - thumbhash: expect.any(String), - exifInfo: expect.objectContaining({ - exifImageWidth: 511, - exifImageHeight: 323, - latitude: null, - longitude: null, - }), - }), - ]), - ); - }); - it('should offline missing files', async () => { await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, { recursive: true, diff --git a/server/e2e/jobs/specs/metadata.e2e-spec.ts b/server/e2e/jobs/specs/metadata.e2e-spec.ts index 541e2d5505..4a534b7ef9 100644 --- a/server/e2e/jobs/specs/metadata.e2e-spec.ts +++ b/server/e2e/jobs/specs/metadata.e2e-spec.ts @@ -2,17 +2,9 @@ import { AssetResponseDto, LoginResponseDto } from '@app/domain'; import { AssetController } from '@app/immich'; import { INestApplication } from '@nestjs/common'; import { exiftool } from 'exiftool-vendored'; -import * as fs from 'fs'; -import { api } from '../client'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - db, - itif, - restoreTempFolder, - runAllTests, - testApp, -} from '../utils'; +import { readFile, writeFile } from 'fs/promises'; +import { api } from '../../client'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, db, restoreTempFolder, testApp } from '../utils'; describe(`${AssetController.name} (e2e)`, () => { let app: INestApplication; @@ -20,7 +12,7 @@ describe(`${AssetController.name} (e2e)`, () => { let admin: LoginResponseDto; beforeAll(async () => { - app = await testApp.create({ jobs: true }); + app = await testApp.create(); server = app.getHttpServer(); }); @@ -40,9 +32,7 @@ describe(`${AssetController.name} (e2e)`, () => { let assetWithLocation: AssetResponseDto; beforeEach(async () => { - const fileContent = await fs.promises.readFile( - `${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`, - ); + const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`); await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent }); @@ -58,12 +48,12 @@ describe(`${AssetController.name} (e2e)`, () => { ); }); - itif(runAllTests)('small webp thumbnails', async () => { + it('small webp thumbnails', async () => { const assetId = assetWithLocation.id; const thumbnail = await api.assetApi.getWebpThumbnail(server, admin.accessToken, assetId); - await fs.promises.writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail); + await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail); const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`); @@ -71,12 +61,12 @@ describe(`${AssetController.name} (e2e)`, () => { expect(exifData).not.toHaveProperty('GPSLatitude'); }); - itif(runAllTests)('large jpeg thumbnails', async () => { + it('large jpeg thumbnails', async () => { const assetId = assetWithLocation.id; const thumbnail = await api.assetApi.getJpegThumbnail(server, admin.accessToken, assetId); - await fs.promises.writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail); + await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail); const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`); @@ -95,8 +85,8 @@ describe(`${AssetController.name} (e2e)`, () => { ['Samsung One UI 6.jpg', 'lT9Uviw/FFJYCjfIxAGPTjzAmmw='], ['Samsung One UI 6.heic', '/ejgzywvgvzvVhUYVfvkLzFBAF0='], ])('should extract motionphoto video', (file, checksum) => { - itif(runAllTests)(`with checksum ${checksum} from ${file}`, async () => { - const fileContent = await fs.promises.readFile(`${IMMICH_TEST_ASSET_PATH}/formats/motionphoto/${file}`); + it(`with checksum ${checksum} from ${file}`, async () => { + const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/formats/motionphoto/${file}`); const response = await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent }); const asset = await api.assetApi.get(server, admin.accessToken, response.id); diff --git a/server/e2e/jobs/utils.ts b/server/e2e/jobs/utils.ts index c0ca511d55..8a098d90d7 100644 --- a/server/e2e/jobs/utils.ts +++ b/server/e2e/jobs/utils.ts @@ -12,7 +12,7 @@ import { Server } from 'tls'; import { EntityTarget, ObjectLiteral } from 'typeorm'; import { AppService } from '../../src/microservices/app.service'; -export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH; +export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string; export const IMMICH_TEST_ASSET_TEMP_PATH = path.normalize(`${IMMICH_TEST_ASSET_PATH}/temp/`); export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 }); @@ -57,16 +57,10 @@ export const db = { let _handler: JobItemHandler = () => Promise.resolve(); -interface TestAppOptions { - jobs: boolean; -} - let app: INestApplication; export const testApp = { - create: async (options?: TestAppOptions): Promise => { - const { jobs } = options || { jobs: false }; - + create: async (): Promise => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], providers: [AppService] }) .overrideModule(InfraModule) .useModule(InfraTestModule) @@ -77,8 +71,8 @@ export const testApp = { updateCronJob: jest.fn(), deleteCronJob: jest.fn(), validateCronExpression: jest.fn(), - queue: (item: JobItem) => jobs && _handler(item), - queueAll: (items: JobItem[]) => jobs && Promise.all(items.map(_handler)).then(() => Promise.resolve()), + queue: (item: JobItem) => _handler(item), + queueAll: (items: JobItem[]) => Promise.all(items.map(_handler)).then(() => Promise.resolve()), resume: jest.fn(), empty: jest.fn(), setConcurrency: jest.fn(), @@ -113,10 +107,6 @@ export const testApp = { }, }; -export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true'; - -export const itif = (condition: boolean) => (condition ? it : it.skip); - const directoryExists = async (dirPath: string) => await fs.promises .access(dirPath)