diff --git a/Makefile b/Makefile index 39141ef4b..e66714a20 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 6146c0987..aee8fb458 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 cd41cf729..30e12616f 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 ab5506491..ed2e8d048 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 cd80b14b2..aef6a6f7f 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 b570c1157..085c7596e 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 27cbf71bb..9a17345a4 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 000000000..e469e845b --- /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 03c6799b5..47d2d7a19 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 0df753a84..312816035 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 bd3d0a8a3..4fc9a0e16 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 179fef795..eeb4723fa 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 114c0a5bb..0ce192e47 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 75bde1085..b6136971d 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 60c62a2af..b254aa61f 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 f8b8691d7..51c870428 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 3b73b2952..8d17ee930 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 015fd53dc..d14270434 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 9d4bdae04..034b2f263 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 fe1470f7f..a6b738709 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 35a9a56e2..cd7c8147a 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 e40a8c150..b9ae897b3 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 854f59235..c9d656ced 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 e40a8c150..000000000 --- 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 3043c941f..000000000 --- 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 6040d026d..000000000 --- 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 d70e7bd62..000000000 --- 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 5ed0838f7..000000000 --- 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 e3f9ef17f..601d99cc2 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 eb7f623c9..6f862c050 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 1ed1355df..21bf99423 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 541e2d550..4a534b7ef 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 c0ca511d5..8a098d90d 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)