From 331dd058be5569b60f79a1b9255368eaef04d006 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler Date: Sat, 6 Apr 2024 00:39:13 +0200 Subject: [PATCH] WIP --- e2e/src/api/specs/library.e2e-spec.ts | 233 +++++++++++++--------- e2e/src/utils.ts | 32 +++ e2e/vitest.config.ts | 2 +- server/e2e/jobs/specs/library.e2e-spec.ts | 36 ---- 4 files changed, 171 insertions(+), 132 deletions(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 3e4f971cf..37c16b7fa 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -6,7 +6,7 @@ import { getAllLibraries, scanLibrary, } from '@immich/sdk'; -import { existsSync, rmdirSync } from 'node:fs'; +import { cpSync, existsSync, readdirSync } from 'node:fs'; import { Socket } from 'socket.io-client'; import { userDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; @@ -33,14 +33,12 @@ describe('/library', () => { afterAll(() => { utils.disconnectWebsocket(websocket); + utils.deleteTempFolder(); }); - beforeEach(() => { + beforeEach(async () => { utils.resetEvents(); - const tempDir = `${testAssetDir}/temp`; - if (existsSync(tempDir)) { - rmdirSync(tempDir, { recursive: true }); - } + utils.deleteTempFolder(); utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`); utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`); }); @@ -357,95 +355,6 @@ describe('/library', () => { }); }); - describe('DELETE /library/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should not delete the last upload library', async () => { - const libraries = await getAllLibraries( - { $type: LibraryType.Upload }, - { headers: asBearerAuth(admin.accessToken) }, - ); - - const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId); - expect(adminLibraries.length).toBeGreaterThanOrEqual(1); - const lastLibrary = adminLibraries.pop() as LibraryResponseDto; - - // delete all but the last upload library - for (const library of adminLibraries) { - const { status } = await request(app) - .delete(`/library/${library.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(204); - } - - const { status, body } = await request(app) - .delete(`/library/${lastLibrary.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(body).toEqual(errorDto.noDeleteUploadLibrary); - expect(status).toBe(400); - }); - - it('should delete an external library', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.External, - }); - - const { status, body } = await request(app) - .delete(`/library/${library.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(204); - expect(body).toEqual({}); - - const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); - expect(libraries).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: library.id, - }), - ]), - ); - }); - - it('should delete an external library with assets', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.External, - importPaths: [`${testAssetDirInternal}/temp`], - }); - - await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); - - const { status, body } = await request(app) - .delete(`/library/${library.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(204); - expect(body).toEqual({}); - - const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); - expect(libraries).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: library.id, - }), - ]), - ); - - // ensure no files get deleted - expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true); - expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true); - }); - }); - describe('GET /library/:id/statistics', () => { it('should require authentication', async () => { const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`); @@ -550,6 +459,51 @@ describe('/library', () => { expect(newAssets.count).toBe(3); }); + + it('should offline missing files', async () => { + cpSync(`${testAssetDir}/albums/nature`, `${testAssetDir}/temp`, { + recursive: true, + }); + console.log(readdirSync(`${testAssetDir}/temp`)); + + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id, { refreshAllFiles: true }); + // await setTimeout(8000); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + // await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + // await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); + + const onlineAssets = await utils.getAllAssets(admin.accessToken); + console.log(onlineAssets); + // expect(onlineAssets.length).toBeGreaterThan(1); + // console.log(onlineAssets) + + utils.deleteTempFolder(); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const assets = await utils.getAllAssets(admin.accessToken); + console.log(assets); + + expect(assets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + isOffline: true, + originalFileName: 'el_torcal_rocks.jpg', + }), + expect.objectContaining({ + isOffline: true, + originalFileName: 'tanners_ridge.jpg', + }), + ]), + ); + }); }); describe('POST /library/:id/removeOffline', () => { @@ -608,4 +562,93 @@ describe('/library', () => { }); }); }); + + describe('DELETE /library/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should not delete the last upload library', async () => { + const libraries = await getAllLibraries( + { $type: LibraryType.Upload }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId); + expect(adminLibraries.length).toBeGreaterThanOrEqual(1); + const lastLibrary = adminLibraries.pop() as LibraryResponseDto; + + // delete all but the last upload library + for (const library of adminLibraries) { + const { status } = await request(app) + .delete(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + } + + const { status, body } = await request(app) + .delete(`/library/${lastLibrary.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(body).toEqual(errorDto.noDeleteUploadLibrary); + expect(status).toBe(400); + }); + + it('should delete an external library', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + type: LibraryType.External, + }); + + const { status, body } = await request(app) + .delete(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(204); + expect(body).toEqual({}); + + const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); + expect(libraries).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: library.id, + }), + ]), + ); + }); + + it('should delete an external library with assets', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + + const { status, body } = await request(app) + .delete(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(204); + expect(body).toEqual({}); + + const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); + expect(libraries).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: library.id, + }), + ]), + ); + + // ensure no files get deleted + expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true); + expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true); + }); + }); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index d8302a9e3..5b0deec05 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -1,4 +1,5 @@ import { + AllJobStatusResponseDto, AssetFileUploadResponseDto, AssetResponseDto, CreateAlbumDto, @@ -18,6 +19,7 @@ import { defaults, deleteAssets, getAllAssets, + getAllJobsStatus, getAssetInfo, login, searchMetadata, @@ -31,6 +33,7 @@ import { createHash } from 'node:crypto'; import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path, { dirname } from 'node:path'; +import { setTimeout as setAsyncTimeout } from 'node:timers/promises'; import { promisify } from 'node:util'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; @@ -406,6 +409,35 @@ export const utils = { }, ]), + deleteTempFolder: () => { + rmSync(`${testAssetDir}/temp`, { recursive: true, force: true }); + }, + + isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => { + const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) }); + const jobCounts = queues[queue].jobCounts; + console.log(jobCounts); + return !jobCounts.active && !jobCounts.waiting; + }, + + waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000); + + while (true) { + const done = await utils.isQueueEmpty(accessToken, queue); + if (done) { + break; + } + await setAsyncTimeout(300); + } + + clearTimeout(timeout); + // await setAsyncTimeout(5000); + resolve(); + }); + }, + cliLogin: async (accessToken: string) => { const key = await utils.createApiKey(accessToken); await immichCli(['login', app, `${key.secret}`]); diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index d7dcde4c3..9b9670c04 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ test: { include: ['src/{api,cli}/specs/*.e2e-spec.ts'], globalSetup, - testTimeout: 10_000, + testTimeout: 15_000, poolOptions: { threads: { singleThread: true, diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index 3ae27e631..b3a064eda 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -31,42 +31,6 @@ describe(`${LibraryController.name} (e2e)`, () => { }); describe('POST /library/:id/scan', () => { - 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, - }); - - const library = await api.libraryApi.create(server, admin.accessToken, { - ownerId: admin.userId, - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], - }); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const onlineAssets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(onlineAssets.length).toBeGreaterThan(1); - - await restoreTempFolder(); - - 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({ - isOffline: true, - originalFileName: 'el_torcal_rocks.jpg', - }), - expect.objectContaining({ - isOffline: true, - originalFileName: 'tanners_ridge.jpg', - }), - ]), - ); - }); - it('should scan new files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { ownerId: admin.userId,