From dba365634a5a54d312f3521a220ae9a02b0051bb Mon Sep 17 00:00:00 2001 From: Jason Rasmussen <jrasm91@gmail.com> Date: Mon, 15 Apr 2024 23:05:08 -0400 Subject: [PATCH] chore(server): cleanup library watching (#8835) chore: clean up library watching --- .github/workflows/test.yml | 13 - Makefile | 3 - docs/docs/developer/testing.md | 23 +- e2e/src/api/specs/library.e2e-spec.ts | 39 +-- e2e/src/utils.ts | 22 +- server/e2e/client/asset-api.ts | 10 - server/e2e/client/auth-api.ts | 23 -- server/e2e/client/index.ts | 9 - server/e2e/client/library-api.ts | 33 -- server/e2e/docker-compose.server-e2e.yml | 33 -- .../config/library-watcher-e2e-config.json | 17 - server/e2e/jobs/immich-e2e-config.json | 12 - server/e2e/jobs/jest-e2e.json | 22 -- server/e2e/jobs/setup.ts | 42 --- .../jobs/specs/library-watcher.e2e-spec.ts | 231 ------------- server/package-lock.json | 322 ------------------ server/package.json | 3 - server/src/interfaces/storage.interface.ts | 8 - server/src/repositories/storage.repository.ts | 11 +- server/src/services/library.service.spec.ts | 71 +--- server/src/services/library.service.ts | 19 +- .../repositories/storage.repository.mock.ts | 10 +- server/test/utils.ts | 168 --------- 23 files changed, 56 insertions(+), 1088 deletions(-) delete mode 100644 server/e2e/client/asset-api.ts delete mode 100644 server/e2e/client/auth-api.ts delete mode 100644 server/e2e/client/index.ts delete mode 100644 server/e2e/client/library-api.ts delete mode 100644 server/e2e/docker-compose.server-e2e.yml delete mode 100644 server/e2e/jobs/config/library-watcher-e2e-config.json delete mode 100644 server/e2e/jobs/immich-e2e-config.json delete mode 100644 server/e2e/jobs/jest-e2e.json delete mode 100644 server/e2e/jobs/setup.ts delete mode 100644 server/e2e/jobs/specs/library-watcher.e2e-spec.ts delete mode 100644 server/test/utils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a5df111d7..d74c961393 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,19 +10,6 @@ concurrency: cancel-in-progress: true jobs: - server-e2e-jobs: - name: Server (e2e-jobs) - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - - name: Run e2e tests - run: make server-e2e-jobs - doc-tests: name: Docs runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 55875e732b..ede70237fe 100644 --- a/Makefile +++ b/Makefile @@ -16,9 +16,6 @@ stage: pull-stage: docker compose -f ./docker/docker-compose.staging.yml pull -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 - .PHONY: e2e e2e: docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans diff --git a/docs/docs/developer/testing.md b/docs/docs/developer/testing.md index de080fbb52..fecb58f592 100644 --- a/docs/docs/developer/testing.md +++ b/docs/docs/developer/testing.md @@ -8,15 +8,24 @@ Unit are run by calling `npm run test` from the `server` directory. ### End to end tests -The backend has two end-to-end test suites that can be called with the following two commands from the project root directory: +The e2e tests can be run by first starting up a test production environment via: -- `make server-e2e-api` -- `make server-e2e-jobs` +```bash +make e2e +``` -#### API (e2e) +Once the test environment is running, the e2e tests can be run via: -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. +```bash +cd e2e/ +npm test +``` -#### Jobs (e2e) +The tests check various things including: -The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated. +- Authentication and authorization +- Query param, body, and url validation +- Response codes +- Thumbnail generation +- Metadata extraction +- Library scanning diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 587f760c20..18becec770 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -27,6 +27,7 @@ describe('/library', () => { beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup(); + await utils.resetAdminConfig(admin.accessToken); user = await utils.userSetup(admin.accessToken, userDto.user1); library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External }); websocket = await utils.connectWebsocket(admin.accessToken); @@ -36,7 +37,7 @@ describe('/library', () => { afterAll(() => { utils.disconnectWebsocket(websocket); - utils.deleteTempFolder(); + utils.resetTempFolder(); }); beforeEach(() => { @@ -816,40 +817,4 @@ describe('/library', () => { expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true); }); }); - - // describe('Watching', () => { - // beforeAll(async () => { - // const config = await getConfigDefaults({ headers: asBearerAuth(admin.accessToken) }); - // await updateConfig( - // { systemConfigDto: { ...config, library: { ...config.library, watch: { enabled: true } } } }, - // { headers: asBearerAuth(admin.accessToken) }, - // ); - // }); - - // afterAll(async () => { - // const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(admin.accessToken) }); - // await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(admin.accessToken) }); - // rmSync(`${testAssetDir}/temp/watch`, { recursive: true }); - // }); - - // describe('Single import path', () => { - // let library: LibraryResponseDto; - // beforeEach(async () => { - // library = await utils.createLibrary(admin.accessToken, { - // ownerId: admin.userId, - // type: LibraryType.External, - // importPaths: [`${testAssetDirInternal}/temp`], - // }); - // }); - - // it('should import a new file', async () => { - // utils.createImageFile(`${testAssetDir}/temp/watch/assetA.png`); - - // await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 }); - - // const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - // expect(assets.count).toEqual(3); - // }); - // }); - // }); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 0495acbbfc..a46653eb11 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -21,10 +21,12 @@ import { getAllAssets, getAllJobsStatus, getAssetInfo, + getConfigDefaults, login, searchMetadata, setAdminOnboarding, signUpAdmin, + updateConfig, validate, } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; @@ -139,6 +141,7 @@ export const utils = { 'user_token', 'users', 'system_metadata', + 'system_config', ]; const sql: string[] = []; @@ -148,7 +151,12 @@ export const utils = { } for (const table of tables) { - sql.push(`DELETE FROM ${table} CASCADE;`); + if (table === 'system_metadata') { + // prevent reverse geocoder from being re-initialized + sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`); + } else { + sql.push(`DELETE FROM ${table} CASCADE;`); + } } await client.query(sql.join('\n')); @@ -310,9 +318,7 @@ export const utils = { if (!existsSync(dirname(path))) { mkdirSync(dirname(path), { recursive: true }); } - if (!existsSync(path)) { - writeFileSync(path, makeRandomImage()); - } + writeFileSync(path, makeRandomImage()); }, removeImageFile: (path: string) => { @@ -407,8 +413,14 @@ export const utils = { }, ]), - deleteTempFolder: () => { + resetTempFolder: () => { rmSync(`${testAssetDir}/temp`, { recursive: true, force: true }); + mkdirSync(`${testAssetDir}/temp`, { recursive: true }); + }, + + resetAdminConfig: async (accessToken: string) => { + const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); + await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); }, isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => { diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts deleted file mode 100644 index f32d586115..0000000000 --- a/server/e2e/client/asset-api.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import request from 'supertest'; - -export const assetApi = { - 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[]; - }, -}; diff --git a/server/e2e/client/auth-api.ts b/server/e2e/client/auth-api.ts deleted file mode 100644 index a8cfe4660a..0000000000 --- a/server/e2e/client/auth-api.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { LoginResponseDto } from 'src/dtos/auth.dto'; -import { UserResponseDto } from 'src/dtos/user.dto'; -import request from 'supertest'; -import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub'; - -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; - }, -}; diff --git a/server/e2e/client/index.ts b/server/e2e/client/index.ts deleted file mode 100644 index 41418ddcc0..0000000000 --- a/server/e2e/client/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assetApi } from 'e2e/client/asset-api'; -import { authApi } from 'e2e/client/auth-api'; -import { libraryApi } from 'e2e/client/library-api'; - -export const api = { - authApi, - assetApi, - libraryApi, -}; diff --git a/server/e2e/client/library-api.ts b/server/e2e/client/library-api.ts deleted file mode 100644 index 70c8c4c360..0000000000 --- a/server/e2e/client/library-api.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto'; -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(204); - }, -}; diff --git a/server/e2e/docker-compose.server-e2e.yml b/server/e2e/docker-compose.server-e2e.yml deleted file mode 100644 index 61b38e1ee0..0000000000 --- a/server/e2e/docker-compose.server-e2e.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: '3.8' - -name: 'immich-test-e2e' - -services: - immich-server: - image: immich-server-dev:latest - build: - context: ../../ - dockerfile: server/Dockerfile - target: dev - command: ['/usr/src/app/bin/immich-test', 'jobs'] - volumes: - - /usr/src/app/node_modules - - ../test/assets:/usr/src/app/test/assets:ro - environment: - - DB_HOSTNAME=database - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - DB_DATABASE_NAME=e2e_test - - IMMICH_METRICS=true - depends_on: - - database - - database: - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 - command: -c fsync=off -c shared_preload_libraries=vectors.so - environment: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - POSTGRES_DB: e2e_test - logging: - driver: none diff --git a/server/e2e/jobs/config/library-watcher-e2e-config.json b/server/e2e/jobs/config/library-watcher-e2e-config.json deleted file mode 100644 index 9f7420ca5a..0000000000 --- a/server/e2e/jobs/config/library-watcher-e2e-config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "reverseGeocoding": { - "enabled": false - }, - "machineLearning": { - "enabled": false - }, - "logging": { - "enabled": false, - "level": "debug" - }, - "library": { - "watch": { - "enabled": true - } - } -} diff --git a/server/e2e/jobs/immich-e2e-config.json b/server/e2e/jobs/immich-e2e-config.json deleted file mode 100644 index 4f018dc164..0000000000 --- a/server/e2e/jobs/immich-e2e-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "reverseGeocoding": { - "enabled": false - }, - "machineLearning": { - "enabled": false - }, - "logging": { - "enabled": false, - "level": "debug" - } -} diff --git a/server/e2e/jobs/jest-e2e.json b/server/e2e/jobs/jest-e2e.json deleted file mode 100644 index b7e62d5f46..0000000000 --- a/server/e2e/jobs/jest-e2e.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "modulePaths": ["<rootDir>"], - "rootDir": "../..", - "globalSetup": "<rootDir>/e2e/jobs/setup.ts", - "testEnvironment": "node", - "testMatch": ["**/e2e/jobs/specs/*.e2e-spec.[tj]s"], - "testTimeout": 10000, - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "<rootDir>/src/**/*.(t|j)s", - "!<rootDir>/src/**/*.spec.(t|s)s", - "!<rootDir>/src/migrations/**" - ], - "coverageDirectory": "./coverage", - "moduleNameMapper": { - "^test(|/.*)$": "<rootDir>/test/$1", - "^src(|/.*)$": "<rootDir>/src/$1" - } -} diff --git a/server/e2e/jobs/setup.ts b/server/e2e/jobs/setup.ts deleted file mode 100644 index d1f566d372..0000000000 --- a/server/e2e/jobs/setup.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PostgreSqlContainer } from '@testcontainers/postgresql'; -import { access } from 'fs/promises'; -import path from 'path'; - -export default async () => { - let IMMICH_TEST_ASSET_PATH: string = ''; - - if (process.env.IMMICH_TEST_ASSET_PATH === undefined) { - IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../test/assets/`); - process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH; - } else { - IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH; - } - - const directoryExists = async (dirPath: string) => - await access(dirPath) - .then(() => true) - .catch(() => false); - - if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) { - throw new Error( - `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`, - ); - } - - if (process.env.DB_HOSTNAME === undefined) { - // DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container. - const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0') - .withExposedPorts(5432) - .withDatabase('immich') - .withUsername('postgres') - .withPassword('postgres') - .withReuse() - .start(); - - process.env.DB_URL = pg.getConnectionUri(); - } - - process.env.NODE_ENV = 'development'; - process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/immich-e2e-config.json`); - process.env.TZ = 'Z'; -}; diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts deleted file mode 100644 index 20a9d32020..0000000000 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { api } from 'e2e/client'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { LoginResponseDto } from 'src/dtos/auth.dto'; -import { LibraryResponseDto } from 'src/dtos/library.dto'; -import { AssetType } from 'src/entities/asset.entity'; -import { LibraryType } from 'src/entities/library.entity'; -import { StorageEventType } from 'src/interfaces/storage.interface'; -import { LibraryService } from 'src/services/library.service'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - restoreTempFolder, - testApp, - waitForEvent, -} from 'test/utils'; - -describe(`Library watcher (e2e)`, () => { - let server: any; - let admin: LoginResponseDto; - let libraryService: LibraryService; - const configFilePath = process.env.IMMICH_CONFIG_FILE; - - beforeAll(async () => { - process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../config/library-watcher-e2e-config.json`); - - const app = await testApp.create(); - server = app.getHttpServer(); - libraryService = testApp.get(LibraryService); - }); - - beforeEach(async () => { - await testApp.reset(); - await restoreTempFolder(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - }); - - afterEach(async () => { - await libraryService.teardown(); - }); - - afterAll(async () => { - await testApp.teardown(); - await restoreTempFolder(); - process.env.IMMICH_CONFIG_FILE = configFilePath; - }); - - describe('Event handling', () => { - describe('Single import path', () => { - beforeEach(async () => { - await api.libraryApi.create(server, admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], - }); - }); - - it('should import a new file', async () => { - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD); - - const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(afterAssets.length).toEqual(1); - }); - - it('should import new files with case insensitive extensions', async () => { - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file2.JPG`, - ); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file3.Jpg`, - ); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file4.jpG`, - ); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD, 4); - - const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(afterAssets.length).toEqual(4); - }); - - it('should update a changed file', async () => { - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD); - - const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(originalAssets.length).toEqual(1); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/prairie_falcon.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.CHANGE); - - const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(afterAssets).toEqual([ - expect.objectContaining({ - // Make sure we keep the original asset id - id: originalAssets[0].id, - type: AssetType.IMAGE, - exifInfo: expect.objectContaining({ - make: 'Canon', - model: 'Canon EOS R5', - exifImageWidth: 800, - exifImageHeight: 533, - exposureTime: '1/4000', - }), - }), - ]); - }); - }); - - describe('Multiple import paths', () => { - beforeEach(async () => { - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true }); - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true }); - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); - - await api.libraryApi.create(server, admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.EXTERNAL, - importPaths: [ - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, - ], - }); - }); - - it('should add new files in multiple import paths', async () => { - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file2.jpg`, - ); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2/file3.jpg`, - ); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD, 3); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(assets.length).toEqual(3); - }); - - it('should offline a removed file', async () => { - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD); - - const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(addedAssets.length).toEqual(1); - - await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`); - - await waitForEvent(libraryService, StorageEventType.UNLINK); - - const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(afterAssets[0].isOffline).toEqual(true); - }); - }); - }); - - describe('Configuration', () => { - let library: LibraryResponseDto; - - beforeEach(async () => { - library = await api.libraryApi.create(server, admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.EXTERNAL, - importPaths: [ - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, - ], - }); - - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true }); - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true }); - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); - }); - - it('should use an updated import path', async () => { - await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true }); - - await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [ - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, - ]); - - await fs.copyFile( - `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/dir4/file.jpg`, - ); - - await waitForEvent(libraryService, StorageEventType.ADD); - - const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(afterAssets.length).toEqual(1); - }); - }); -}); diff --git a/server/package-lock.json b/server/package-lock.json index e473a94d0b..a0dc11ad22 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -81,7 +81,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", @@ -98,8 +97,6 @@ "rimraf": "^5.0.1", "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", - "supertest": "^6.3.3", - "testcontainers": "^10.2.1", "ts-jest": "^29.1.1", "ts-loader": "^9.4.4", "ts-node": "^10.9.1", @@ -4390,12 +4387,6 @@ "@types/express": "*" } }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true - }, "node_modules/@types/cookies": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", @@ -4685,12 +4676,6 @@ "@types/node": "*" } }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true - }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -4890,27 +4875,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "node_modules/@types/superagent": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.1.tgz", - "integrity": "sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA==", - "dev": true, - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, "node_modules/@types/tedious": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", @@ -5709,12 +5673,6 @@ "node": ">=8" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -5734,12 +5692,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -6587,18 +6539,6 @@ "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -6624,12 +6564,6 @@ "node": ">= 6" } }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "node_modules/compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -6762,12 +6696,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "node_modules/core-js-compat": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", @@ -7037,15 +6965,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -7093,16 +7012,6 @@ "node": ">=8" } }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "node_modules/diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -8172,35 +8081,6 @@ "webpack": "^5.11.0" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "dev": true, - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8711,15 +8591,6 @@ "he": "bin/he" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -12922,52 +12793,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17435,12 +17260,6 @@ "@types/express": "*" } }, - "@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true - }, "@types/cookies": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", @@ -17729,12 +17548,6 @@ "@types/node": "*" } }, - "@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true - }, "@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -17921,27 +17734,6 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "@types/superagent": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.1.tgz", - "integrity": "sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA==", - "dev": true, - "requires": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*" - } - }, - "@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "requires": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, "@types/tedious": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", @@ -18536,12 +18328,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -18561,12 +18347,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -19182,15 +18962,6 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -19210,12 +18981,6 @@ "repeat-string": "^1.6.1" } }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -19315,12 +19080,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "core-js-compat": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", @@ -19502,12 +19261,6 @@ "has-property-descriptors": "^1.0.1" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -19539,16 +19292,6 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, - "dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -20373,29 +20116,6 @@ "tapable": "^2.2.1" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "dev": true, - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -20758,12 +20478,6 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true - }, "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -23956,42 +23670,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "dependencies": { - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true - } - } - }, - "supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/server/package.json b/server/package.json index 94b21d922f..5e2a453d0b 100644 --- a/server/package.json +++ b/server/package.json @@ -105,7 +105,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", @@ -122,8 +121,6 @@ "rimraf": "^5.0.1", "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", - "supertest": "^6.3.3", - "testcontainers": "^10.2.1", "ts-jest": "^29.1.1", "ts-loader": "^9.4.4", "ts-node": "^10.9.1", diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index e78bb0195d..1bd49a3f20 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -31,14 +31,6 @@ export interface WatchEvents { onError(error: Error): void; } -export enum StorageEventType { - READY = 'ready', - ADD = 'add', - CHANGE = 'change', - UNLINK = 'unlink', - ERROR = 'error', -} - export interface IStorageRepository { createZipStream(): ImmichZipStream; createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>; diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 7e1c8d59e2..8f0dfd3ff9 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -10,7 +10,6 @@ import { IStorageRepository, ImmichReadStream, ImmichZipStream, - StorageEventType, WatchEvents, } from 'src/interfaces/storage.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -173,11 +172,11 @@ export class StorageRepository implements IStorageRepository { watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) { const watcher = chokidar.watch(paths, options); - watcher.on(StorageEventType.READY, () => events.onReady?.()); - watcher.on(StorageEventType.ADD, (path) => events.onAdd?.(path)); - watcher.on(StorageEventType.CHANGE, (path) => events.onChange?.(path)); - watcher.on(StorageEventType.UNLINK, (path) => events.onUnlink?.(path)); - watcher.on(StorageEventType.ERROR, (error) => events.onError?.(error)); + watcher.on('ready', () => events.onReady?.()); + watcher.on('add', (path) => events.onAdd?.(path)); + watcher.on('change', (path) => events.onChange?.(path)); + watcher.on('unlink', (path) => events.onUnlink?.(path)); + watcher.on('error', (error) => events.onError?.(error)); return () => watcher.close(); } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 5f3dc45de1..0a6026b252 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -13,7 +13,7 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; -import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { LibraryService } from 'src/services/library.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -933,12 +933,6 @@ describe(LibraryService.name, () => { type: LibraryType.EXTERNAL, importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths, }); - - expect(storageMock.watch).toHaveBeenCalledWith( - libraryStub.externalLibraryWithImportPaths1.importPaths, - expect.anything(), - expect.anything(), - ); }); it('should create with exclusion patterns', async () => { @@ -1087,45 +1081,6 @@ describe(LibraryService.name, () => { await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1)); expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' })); }); - - it('should re-watch library when updating import paths', async () => { - libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - - storageMock.stat.mockResolvedValue({ - isDirectory: () => true, - } as Stats); - - storageMock.checkFileExists.mockResolvedValue(true); - - await expect(sut.update('library-id', { importPaths: ['/data/user1/foo'] })).resolves.toEqual( - mapLibrary(libraryStub.externalLibraryWithImportPaths1), - ); - - expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' })); - expect(storageMock.watch).toHaveBeenCalledWith( - libraryStub.externalLibraryWithImportPaths1.importPaths, - expect.anything(), - expect.anything(), - ); - }); - - it('should re-watch library when updating exclusion patterns', async () => { - libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); - libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - - await expect(sut.update('library-id', { exclusionPatterns: ['bar'] })).resolves.toEqual( - mapLibrary(libraryStub.externalLibraryWithImportPaths1), - ); - - expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' })); - expect(storageMock.watch).toHaveBeenCalledWith( - expect.arrayContaining([expect.any(String)]), - expect.anything(), - expect.anything(), - ); - }); }); describe('watchAll', () => { @@ -1198,9 +1153,7 @@ describe(LibraryService.name, () => { it('should handle a new file event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }), - ); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -1221,7 +1174,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }), + makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), ); await sut.watchAll(); @@ -1244,7 +1197,7 @@ describe(LibraryService.name, () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }), + makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }), ); await sut.watchAll(); @@ -1258,19 +1211,17 @@ describe(LibraryService.name, () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); storageMock.watch.mockImplementation( makeMockWatcher({ - items: [{ event: StorageEventType.ERROR, value: 'Error!' }], + items: [{ event: 'error', value: 'Error!' }], }), ); - await expect(sut.watchAll()).rejects.toThrow('Error!'); + await expect(sut.watchAll()).resolves.toBeUndefined(); }); it('should ignore unknown extensions', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }), - ); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -1280,9 +1231,7 @@ describe(LibraryService.name, () => { it('should ignore excluded paths', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }), - ); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] })); await sut.watchAll(); @@ -1292,9 +1241,7 @@ describe(LibraryService.name, () => { it('should ignore excluded paths without case sensitivity', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }), - ); + storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] })); await sut.watchAll(); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 4dd47f9ad5..848c40f78e 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { Trie } from 'mnemonist'; import { R_OK } from 'node:constants'; -import { EventEmitter } from 'node:events'; import { Stats } from 'node:fs'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; @@ -37,7 +36,7 @@ import { JobStatus, } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; -import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ImmichLogger } from 'src/utils/logger'; import { mimeTypes } from 'src/utils/mime-types'; @@ -48,7 +47,7 @@ import { validateCronExpression } from 'src/validation'; const LIBRARY_SCAN_BATCH_SIZE = 5000; @Injectable() -export class LibraryService extends EventEmitter { +export class LibraryService { readonly logger = new ImmichLogger(LibraryService.name); private configCore: SystemConfigCore; private watchLibraries = false; @@ -64,7 +63,6 @@ export class LibraryService extends EventEmitter { @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, ) { - super(); this.configCore = SystemConfigCore.create(configRepository); } @@ -152,7 +150,6 @@ export class LibraryService extends EventEmitter { if (matcher(path)) { await this.scanAssets(library.id, [path], library.ownerId, false); } - this.emit(StorageEventType.ADD, path); }; return handlePromiseError(handler(), this.logger); }, @@ -163,7 +160,6 @@ export class LibraryService extends EventEmitter { // Note: if the changed file was not previously imported, it will be imported now. await this.scanAssets(library.id, [path], library.ownerId, false); } - this.emit(StorageEventType.CHANGE, path); }; return handlePromiseError(handler(), this.logger); }, @@ -174,13 +170,11 @@ export class LibraryService extends EventEmitter { if (asset && matcher(path)) { await this.assetRepository.update({ id: asset.id, isOffline: true }); } - this.emit(StorageEventType.UNLINK, path); }; return handlePromiseError(handler(), this.logger); }, onError: (error) => { this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); - this.emit(StorageEventType.ERROR, error); }, }, ); @@ -281,10 +275,6 @@ export class LibraryService extends EventEmitter { this.logger.log(`Creating ${dto.type} library for ${dto.ownerId}}`); - if (dto.type === LibraryType.EXTERNAL) { - await this.watch(library.id); - } - return mapLibrary(library); } @@ -368,11 +358,6 @@ export class LibraryService extends EventEmitter { } } - if (dto.importPaths || dto.exclusionPatterns) { - // Re-watch library to use new paths and/or exclusion patterns - await this.watch(id); - } - return mapLibrary(library); } diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index d5049999c7..e88657684b 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -1,6 +1,6 @@ import { WatchOptions } from 'chokidar'; import { StorageCore } from 'src/cores/storage.core'; -import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.interface'; +import { IStorageRepository, WatchEvents } from 'src/interfaces/storage.interface'; interface MockWatcherOptions { items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; @@ -13,19 +13,19 @@ export const makeMockWatcher = events.onReady?.(); for (const item of items || []) { switch (item.event) { - case StorageEventType.ADD: { + case 'add': { events.onAdd?.(item.value); break; } - case StorageEventType.CHANGE: { + case 'change': { events.onChange?.(item.value); break; } - case StorageEventType.UNLINK: { + case 'unlink': { events.onUnlink?.(item.value); break; } - case StorageEventType.ERROR: { + case 'error': { events.onError?.(new Error(item.value)); } } diff --git a/server/test/utils.ts b/server/test/utils.ts deleted file mode 100644 index c7732eabc1..0000000000 --- a/server/test/utils.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { DateTime } from 'luxon'; -import fs from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { EventEmitter } from 'node:stream'; -import { AppTestModule } from 'src/app.module'; -import { dataSource } from 'src/database.config'; -import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; -import { StorageEventType } from 'src/interfaces/storage.interface'; -import { MediaRepository } from 'src/repositories/media.repository'; -import { ApiService } from 'src/services/api.service'; -import { MicroservicesService } from 'src/services/microservices.service'; -import { EntityTarget, ObjectLiteral } from 'typeorm'; - -export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string; -export const IMMICH_TEST_ASSET_TEMP_PATH = join(tmpdir(), 'immich'); - -export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 }); -export const yesterday = today.minus({ days: 1 }); - -export interface ResetOptions { - entities?: EntityTarget<ObjectLiteral>[]; -} -export const db = { - reset: async (options?: ResetOptions) => { - if (!dataSource.isInitialized) { - await dataSource.initialize(); - } - await dataSource.transaction(async (em) => { - const entities = options?.entities || []; - const tableNames = - entities.length > 0 - ? entities.map((entity) => em.getRepository(entity).metadata.tableName) - : dataSource.entityMetadatas - .map((entity) => entity.tableName) - .filter((tableName) => !tableName.startsWith('geodata')); - - let deleteUsers = false; - for (const tableName of tableNames) { - if (tableName === 'users') { - deleteUsers = true; - continue; - } - await em.query(`DELETE FROM ${tableName} CASCADE;`); - } - if (deleteUsers) { - await em.query(`DELETE FROM "users" CASCADE;`); - } - - // Release all locks - await em.query('SELECT pg_advisory_unlock_all()'); - }); - }, - disconnect: async () => { - if (dataSource.isInitialized) { - await dataSource.destroy(); - } - }, -}; - -class JobMock implements IJobRepository { - private _handler: JobItemHandler = () => Promise.resolve(); - addHandler(_queueName: QueueName, _concurrency: number, handler: JobItemHandler) { - this._handler = handler; - } - addCronJob() {} - updateCronJob() {} - deleteCronJob() {} - validateCronExpression() {} - queue(item: JobItem) { - return this._handler(item); - } - queueAll(items: JobItem[]) { - return Promise.all(items.map((arg) => this._handler(arg))).then(() => {}); - } - async resume() {} - async empty() {} - async setConcurrency() {} - getQueueStatus() { - return Promise.resolve(null) as any; - } - getJobCounts() { - return Promise.resolve(null) as any; - } - async pause() {} - clear() { - return Promise.resolve([]); - } - async waitForQueueCompletion() {} -} - -class MediaMockRepository extends MediaRepository { - generateThumbhash() { - return Promise.resolve(Buffer.from('mock-thumbhash')); - } -} - -let app: INestApplication; - -export const testApp = { - create: async (): Promise<INestApplication> => { - const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] }) - .overrideProvider(IJobRepository) - .useClass(JobMock) - .overrideProvider(IMediaRepository) - .useClass(MediaMockRepository) - .compile(); - - app = await moduleFixture.createNestApplication().init(); - await app.get(ApiService).init(); - await db.reset(); - await app.get(ApiService).init(); - await app.get(MicroservicesService).init(); - - return app; - }, - reset: async (options?: ResetOptions) => { - await db.reset(options); - }, - get: (member: any) => app.get(member), - teardown: async () => { - if (app) { - await app.get(MicroservicesService).teardown(); - await app.close(); - } - await db.disconnect(); - }, -}; - -export function waitForEvent(emitter: EventEmitter, event: string, times = 1): Promise<void[]> { - const promises: Promise<void>[] = []; - - for (let i = 1; i <= times; i++) { - promises.push( - new Promise((resolve, reject) => { - const success = (value: any) => { - emitter.off(StorageEventType.ERROR, fail); - resolve(value); - }; - const fail = (error: Error) => { - emitter.off(event, success); - reject(error); - }; - emitter.once(event, success); - emitter.once(StorageEventType.ERROR, fail); - }), - ); - } - return Promise.all(promises); -} - -const directoryExists = async (dirPath: string) => - await fs.promises - .access(dirPath) - .then(() => true) - .catch(() => false); - -export async function restoreTempFolder(): Promise<void> { - if (await directoryExists(`${IMMICH_TEST_ASSET_TEMP_PATH}`)) { - // Temp directory exists, delete all files inside it - await fs.promises.rm(IMMICH_TEST_ASSET_TEMP_PATH, { recursive: true }); - } - // Create temp folder - await fs.promises.mkdir(IMMICH_TEST_ASSET_TEMP_PATH); -}