diff --git a/e2e/package.json b/e2e/package.json index 9f231c9ddd..14685df51b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -5,7 +5,8 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest --config vitest.config.ts", + "test": "vitest --run", + "test:watch": "vitest", "test:web": "npx playwright test", "start:web": "npx playwright test --ui", "format": "prettier --check .", diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index 365ad66dc4..5d3cf72209 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -9,7 +9,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -23,12 +23,11 @@ describe('/activity', () => { create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) }); beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); - nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); - asset = await apiUtils.createAsset(admin.accessToken); + admin = await utils.adminSetup(); + nonOwner = await utils.userSetup(admin.accessToken, createUserDto.user1); + asset = await utils.createAsset(admin.accessToken); album = await createAlbum( { createAlbumDto: { @@ -42,7 +41,7 @@ describe('/activity', () => { }); beforeEach(async () => { - await dbUtils.reset(['activity']); + await utils.resetDatabase(['activity']); }); describe('GET /activity', () => { diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 99a50106ed..4faa5eac3d 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -7,7 +7,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -29,49 +29,48 @@ describe('/album', () => { let user3: LoginResponseDto; // deleted beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2, user3] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); [user1Asset1, user1Asset2] = await Promise.all([ - apiUtils.createAsset(user1.accessToken, { isFavorite: true }), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken, { isFavorite: true }), + utils.createAsset(user1.accessToken), ]); const albums = await Promise.all([ // user 1 - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1SharedUser, sharedWithUserIds: [user2.userId], assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset1.id, user1Asset2.id], }), // user 2 - apiUtils.createAlbum(user2.accessToken, { + utils.createAlbum(user2.accessToken, { albumName: user2SharedUser, sharedWithUserIds: [user1.userId], assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), - apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), // user 3 - apiUtils.createAlbum(user3.accessToken, { + utils.createAlbum(user3.accessToken, { albumName: 'Deleted', sharedWithUserIds: [user1.userId], }), @@ -82,12 +81,12 @@ describe('/album', () => { await Promise.all([ // add shared link to user1SharedLink album - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: user1Albums[1].id, }), // add shared link to user2SharedLink album - apiUtils.createSharedLink(user2.accessToken, { + utils.createSharedLink(user2.accessToken, { type: SharedLinkType.Album, albumId: user2Albums[1].id, }), @@ -366,7 +365,7 @@ describe('/album', () => { }); it('should be able to add own asset to own album', async () => { - const asset = await apiUtils.createAsset(user1.accessToken); + const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) .put(`/album/${user1Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -377,7 +376,7 @@ describe('/album', () => { }); it('should be able to add own asset to shared album', async () => { - const asset = await apiUtils.createAsset(user1.accessToken); + const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) .put(`/album/${user2Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -398,7 +397,7 @@ describe('/album', () => { }); it('should update an album', async () => { - const album = await apiUtils.createAlbum(user1.accessToken, { + const album = await utils.createAlbum(user1.accessToken, { albumName: 'New album', }); const { status, body } = await request(app) @@ -485,7 +484,7 @@ describe('/album', () => { let album: AlbumResponseDto; beforeEach(async () => { - album = await apiUtils.createAlbum(user1.accessToken, { + album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', }); }); diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 30dfa5d643..f1bb355315 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -12,7 +12,7 @@ import { basename, join } from 'node:path'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils, fileUtils, tempDir, testAssetDir, wsUtils } from 'src/utils'; +import { app, tempDir, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -44,42 +44,41 @@ describe('/asset', () => { let ws: Socket; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); [ws, user1, user2, userStats] = await Promise.all([ - wsUtils.connect(admin.accessToken), - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.connectWebsocket(admin.accessToken), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); // asset location - assetLocation = await apiUtils.createAsset(admin.accessToken, { + assetLocation = await utils.createAsset(admin.accessToken, { assetData: { filename: 'thompson-springs.jpg', bytes: await readFile(locationAssetFilepath), }, }); - await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id }); user1Assets = await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken, { + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken, { isFavorite: true, isReadOnly: true, fileCreatedAt: yesterday.toISO(), fileModifiedAt: yesterday.toISO(), assetData: { filename: 'example.mp4' }, }), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), ]); - user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]); + user2Assets = await Promise.all([utils.createAsset(user2.accessToken)]); for (const asset of [...user1Assets, ...user2Assets]) { expect(asset.duplicate).toBe(false); @@ -87,27 +86,27 @@ describe('/asset', () => { await Promise.all([ // stats - apiUtils.createAsset(userStats.accessToken), - apiUtils.createAsset(userStats.accessToken, { isFavorite: true }), - apiUtils.createAsset(userStats.accessToken, { isArchived: true }), - apiUtils.createAsset(userStats.accessToken, { + utils.createAsset(userStats.accessToken), + utils.createAsset(userStats.accessToken, { isFavorite: true }), + utils.createAsset(userStats.accessToken, { isArchived: true }), + utils.createAsset(userStats.accessToken, { isArchived: true, isFavorite: true, assetData: { filename: 'example.mp4' }, }), ]); - const person1 = await apiUtils.createPerson(user1.accessToken, { + const person1 = await utils.createPerson(user1.accessToken, { name: 'Test Person', }); - await dbUtils.createFace({ + await utils.createFace({ assetId: user1Assets[0].id, personId: person1.id, }); }, 30_000); afterAll(() => { - wsUtils.disconnect(ws); + utils.disconnectWebsocket(ws); }); describe('GET /asset/:id', () => { @@ -142,7 +141,7 @@ describe('/asset', () => { }); it('should work with a shared link', async () => { - const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [user1Assets[0].id], }); @@ -172,7 +171,7 @@ describe('/asset', () => { ], }); - const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [user1Assets[0].id], }); @@ -244,12 +243,12 @@ describe('/asset', () => { describe('GET /asset/random', () => { beforeAll(async () => { await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), ]); }); @@ -332,7 +331,7 @@ describe('/asset', () => { }); it('should favorite an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id); + const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id); expect(before.isFavorite).toBe(false); const { status, body } = await request(app) @@ -344,7 +343,7 @@ describe('/asset', () => { }); it('should archive an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id); + const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id); expect(before.isArchived).toBe(false); const { status, body } = await request(app) @@ -472,9 +471,9 @@ describe('/asset', () => { }); it('should move an asset to the trash', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + const { id: assetId } = await utils.createAsset(admin.accessToken); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(false); const { status } = await request(app) @@ -483,7 +482,7 @@ describe('/asset', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(true); }); }); @@ -604,15 +603,15 @@ describe('/asset', () => { for (const { input, expected } of tests) { it(`should generate a thumbnail for ${input}`, async () => { const filepath = join(testAssetDir, input); - const { id, duplicate } = await apiUtils.createAsset(admin.accessToken, { + const { id, duplicate } = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, }); expect(duplicate).toBe(false); - await wsUtils.waitForEvent({ event: 'upload', assetId: id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: id }); - const asset = await apiUtils.getAssetInfo(admin.accessToken, id); + const asset = await utils.getAssetInfo(admin.accessToken, id); expect(asset.exifInfo).toBeDefined(); expect(asset.exifInfo).toMatchObject(expected.exifInfo); @@ -622,7 +621,7 @@ describe('/asset', () => { it('should handle a duplicate', async () => { const filepath = 'formats/jpeg/el_torcal_rocks.jpeg'; - const { duplicate } = await apiUtils.createAsset(admin.accessToken, { + const { duplicate } = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), @@ -654,21 +653,21 @@ describe('/asset', () => { for (const { filepath, checksum } of motionTests) { it(`should extract motionphoto video from ${filepath}`, async () => { - const response = await apiUtils.createAsset(admin.accessToken, { + const response = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), }, }); - await wsUtils.waitForEvent({ event: 'upload', assetId: response.id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: response.id }); expect(response.duplicate).toBe(false); - const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id); + const asset = await utils.getAssetInfo(admin.accessToken, response.id); expect(asset.livePhotoVideoId).toBeDefined(); - const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string); + const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string); expect(video.checksum).toStrictEqual(checksum); }); } @@ -687,7 +686,7 @@ describe('/asset', () => { .get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`) .set('Authorization', `Bearer ${admin.accessToken}`); - await wsUtils.waitForEvent({ + await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id, }); @@ -733,11 +732,11 @@ describe('/asset', () => { expect(body).toBeDefined(); expect(type).toBe('image/jpeg'); - const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id); + const asset = await utils.getAssetInfo(admin.accessToken, assetLocation.id); const original = await readFile(locationAssetFilepath); - const originalChecksum = fileUtils.sha1(original); - const downloadChecksum = fileUtils.sha1(body); + const originalChecksum = utils.sha1(original); + const downloadChecksum = utils.sha1(body); expect(originalChecksum).toBe(downloadChecksum); expect(downloadChecksum).toBe(asset.checksum); diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts index 13c753039d..2b551fd24c 100644 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ b/e2e/src/api/specs/audit.e2e-spec.ts @@ -1,24 +1,23 @@ import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk'; -import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils'; +import { asBearerAuth, utils } from 'src/utils'; import { beforeAll, describe, expect, it } from 'vitest'; describe('/audit', () => { let admin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - await fileUtils.reset(); + await utils.resetDatabase(); + await utils.resetFilesystem(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); }); describe('GET :/file-report', () => { it('excludes assets without issues from report', async () => { const [trashedAsset, archivedAsset] = await Promise.all([ - apiUtils.createAsset(admin.accessToken), - apiUtils.createAsset(admin.accessToken), - apiUtils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), ]); await Promise.all([ diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index a58e215718..28445f79d9 100644 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ b/e2e/src/api/specs/auth.e2e-spec.ts @@ -1,19 +1,15 @@ import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk'; import { loginDto, signupDto, uuidDto } from 'src/fixtures'; import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; const { name, email, password } = signupDto.admin; describe(`/auth/admin-sign-up`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); }); describe('POST /auth/admin-sign-up', () => { @@ -84,7 +80,7 @@ describe('/auth/*', () => { let admin: LoginResponseDto; beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); await signUpAdmin({ signUpDto: signupDto.admin }); admin = await login({ loginCredentialDto: loginDto.admin }); }); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts index af328934b4..ef14778dac 100644 --- a/e2e/src/api/specs/download.e2e-spec.ts +++ b/e2e/src/api/specs/download.e2e-spec.ts @@ -1,7 +1,7 @@ import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; import { readFile, writeFile } from 'node:fs/promises'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils, fileUtils, tempDir } from 'src/utils'; +import { app, tempDir, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -11,13 +11,9 @@ describe('/download', () => { let asset2: AssetFileUploadResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - [asset1, asset2] = await Promise.all([ - apiUtils.createAsset(admin.accessToken), - apiUtils.createAsset(admin.accessToken), - ]); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + [asset1, asset2] = await Promise.all([utils.createAsset(admin.accessToken), utils.createAsset(admin.accessToken)]); }); describe('POST /download/info', () => { @@ -65,15 +61,15 @@ describe('/download', () => { expect(body instanceof Buffer).toBe(true); await writeFile(`${tempDir}/archive.zip`, body); - await fileUtils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`); + await utils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`); const files = [ { filename: 'example.png', id: asset1.id }, { filename: 'example+1.png', id: asset2.id }, ]; for (const { id, filename } of files) { const bytes = await readFile(`${tempDir}/archive/${filename}`); - const asset = await apiUtils.getAssetInfo(admin.accessToken, id); - expect(fileUtils.sha1(bytes)).toBe(asset.checksum); + const asset = await utils.getAssetInfo(admin.accessToken, id); + expect(utils.sha1(bytes)).toBe(asset.checksum); } }); }); diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 8213cc86ea..e8f9a46bb2 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1,7 +1,7 @@ import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk'; import { userDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils, testAssetDirInternal } from 'src/utils'; +import { app, asBearerAuth, testAssetDirInternal, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -11,11 +11,10 @@ describe('/library', () => { let library: LibraryResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - user = await apiUtils.userSetup(admin.accessToken, userDto.user1); - library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, userDto.user1); + library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); }); describe('GET /library', () => { @@ -303,7 +302,7 @@ describe('/library', () => { }); it('should get library by id', async () => { - const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); const { status, body } = await request(app) .get(`/library/${library.id}`) @@ -359,7 +358,7 @@ describe('/library', () => { }); it('should delete an external library', async () => { - const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); const { status, body } = await request(app) .delete(`/library/${library.id}`) @@ -415,14 +414,14 @@ describe('/library', () => { }); it('should pass with no import paths', async () => { - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { importPaths: [] }); + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [] }); expect(response.importPaths).toEqual([]); }); it('should fail if path does not exist', async () => { const pathToTest = `${testAssetDirInternal}/does/not/exist`; - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [pathToTest], }); @@ -439,7 +438,7 @@ describe('/library', () => { it('should fail if path is a file', async () => { const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`; - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [pathToTest], }); diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 1324d3fa7f..81c4b452c1 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -1,16 +1,12 @@ import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest'; describe(`/oauth`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - - beforeEach(async () => { - await dbUtils.reset(); - await apiUtils.adminSetup(); + beforeAll(async () => { + await utils.resetDatabase(); + await utils.adminSetup(); }); describe('POST /oauth/authorize', () => { diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/api/specs/partner.e2e-spec.ts index 2c88391bd4..b2fb7f4101 100644 --- a/e2e/src/api/specs/partner.e2e-spec.ts +++ b/e2e/src/api/specs/partner.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, createPartner } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -12,15 +12,14 @@ describe('/partner', () => { let user3: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2, user3] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); await Promise.all([ diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts index 915c04f867..55cb982f9d 100644 --- a/e2e/src/api/specs/person.e2e-spec.ts +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, PersonResponseDto } from '@immich/sdk'; import { uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -12,36 +12,35 @@ describe('/activity', () => { let multipleAssetsPerson: PersonResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); }); beforeEach(async () => { - await dbUtils.reset(['person']); + await utils.resetDatabase(['person']); [visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([ - apiUtils.createPerson(admin.accessToken, { + utils.createPerson(admin.accessToken, { name: 'visible_person', }), - apiUtils.createPerson(admin.accessToken, { + utils.createPerson(admin.accessToken, { name: 'hidden_person', isHidden: true, }), - apiUtils.createPerson(admin.accessToken, { + utils.createPerson(admin.accessToken, { name: 'multiple_assets_person', }), ]); - const asset1 = await apiUtils.createAsset(admin.accessToken); - const asset2 = await apiUtils.createAsset(admin.accessToken); + const asset1 = await utils.createAsset(admin.accessToken); + const asset2 = await utils.createAsset(admin.accessToken); await Promise.all([ - dbUtils.createFace({ assetId: asset1.id, personId: visiblePerson.id }), - dbUtils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }), - dbUtils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), - dbUtils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), - dbUtils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }), + utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }), + utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }), ]); }); @@ -194,7 +193,7 @@ describe('/activity', () => { it('should clear a date of birth', async () => { // TODO ironically this uses the update endpoint to create the person - const person = await apiUtils.createPerson(admin.accessToken, { + const person = await utils.createPerson(admin.accessToken, { birthDate: new Date('1990-01-01').toISOString(), }); diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index b8262cb68a..5cfd6a8b98 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, getServerConfig } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,10 +10,9 @@ describe('/server-info', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); - nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); }); describe('GET /server-info', () => { diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index 7ff4bb6bf7..8b854eda00 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -9,7 +9,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -30,20 +30,16 @@ describe('/shared-link', () => { let linkWithoutMetadata: SharedLinkResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), ]); - [asset1, asset2] = await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - ]); + [asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]); [album, deletedAlbum, metadataAlbum] = await Promise.all([ createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }), @@ -61,29 +57,29 @@ describe('/shared-link', () => { [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = await Promise.all([ - apiUtils.createSharedLink(user2.accessToken, { + utils.createSharedLink(user2.accessToken, { type: SharedLinkType.Album, albumId: deletedAlbum.id, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: album.id, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [asset1.id], }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: album.id, password: 'foo', }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: true, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: false, diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index 6d8880d3fc..c223df4874 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,10 +10,9 @@ describe('/system-config', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); }); describe('GET /system-config/map/style.json', () => { diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 60ed75f118..3e6c2f1fc6 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, getAllAssets } from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -10,14 +10,13 @@ describe('/trash', () => { let ws: Socket; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); - ws = await wsUtils.connect(admin.accessToken); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + ws = await utils.connectWebsocket(admin.accessToken); }); afterAll(() => { - wsUtils.disconnect(ws); + utils.disconnectWebsocket(ws); }); describe('POST /trash/empty', () => { @@ -29,8 +28,8 @@ describe('/trash', () => { }); it('should empty the trash', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); @@ -39,7 +38,7 @@ describe('/trash', () => { const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - await wsUtils.waitForEvent({ event: 'delete', assetId }); + await utils.waitForWebsocketEvent({ event: 'delete', assetId }); const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); expect(after.length).toBe(0); @@ -55,16 +54,16 @@ describe('/trash', () => { }); it('should restore all trashed assets', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(true); const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(false); }); }); @@ -78,10 +77,10 @@ describe('/trash', () => { }); it('should restore a trashed asset by id', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(true); const { status } = await request(app) @@ -90,7 +89,7 @@ describe('/trash', () => { .send({ ids: [assetId] }); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(false); }); }); diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index e47e1d531c..d448a605cd 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk'; import { createUserDto, userDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -12,14 +12,13 @@ describe('/server-info', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); [deletedUser, nonAdmin, userToDelete] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) }); diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index aa27bec63e..61702769ca 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -1,14 +1,10 @@ import { stat } from 'node:fs/promises'; -import { apiUtils, app, dbUtils, immichCli } from 'src/utils'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { app, immichCli, utils } from 'src/utils'; +import { beforeEach, describe, expect, it } from 'vitest'; describe(`immich login-key`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); }); it('should require a url', async () => { @@ -30,8 +26,8 @@ describe(`immich login-key`, () => { }); it('should login and save auth.yml with 600', async () => { - const admin = await apiUtils.adminSetup(); - const key = await apiUtils.createApiKey(admin.accessToken); + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283/api', @@ -47,8 +43,8 @@ describe(`immich login-key`, () => { }); it('should login without /api in the url', async () => { - const admin = await apiUtils.adminSetup(); - const key = await apiUtils.createApiKey(admin.accessToken); + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283', diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index 038a2c2ca0..6efe002b86 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -1,11 +1,10 @@ -import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils'; +import { immichCli, utils } from 'src/utils'; import { beforeAll, describe, expect, it } from 'vitest'; describe(`immich server-info`, () => { beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - await cliUtils.login(); + await utils.resetDatabase(); + await utils.cliLogin(); }); it('should return the server info', async () => { diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index bda625241e..27362ef237 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -1,19 +1,18 @@ import { getAllAlbums, getAllAssets } from '@immich/sdk'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; -import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils'; +import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; describe(`immich upload`, () => { let key: string; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - key = await cliUtils.login(); + await utils.resetDatabase(); + key = await utils.cliLogin(); }); beforeEach(async () => { - await dbUtils.reset(['assets', 'albums']); + await utils.resetDatabase(['assets', 'albums']); }); describe('immich upload --recursive', () => { diff --git a/e2e/src/cli/specs/version.e2e-spec.ts b/e2e/src/cli/specs/version.e2e-spec.ts index e94ccf214f..56a0d8b0b1 100644 --- a/e2e/src/cli/specs/version.e2e-spec.ts +++ b/e2e/src/cli/specs/version.e2e-spec.ts @@ -1,14 +1,10 @@ import { readFileSync } from 'node:fs'; -import { apiUtils, immichCli } from 'src/utils'; -import { beforeAll, describe, expect, it } from 'vitest'; +import { immichCli } from 'src/utils'; +import { describe, expect, it } from 'vitest'; const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8')); describe(`immich --version`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - describe('immich --version', () => { it('should print the cli version', async () => { const { stdout, stderr, exitCode } = await immichCli(['--version']); diff --git a/e2e/src/setup.ts b/e2e/src/setup.ts index e0ff443566..a3d96ac17f 100644 --- a/e2e/src/setup.ts +++ b/e2e/src/setup.ts @@ -1,8 +1,16 @@ import { exec, spawn } from 'node:child_process'; +import { setTimeout } from 'node:timers'; export default async () => { let _resolve: () => unknown; - const ready = new Promise((resolve) => (_resolve = resolve)); + let _reject: (error: Error) => unknown; + + const ready = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + + const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000); const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' }); @@ -17,6 +25,7 @@ export default async () => { child.stderr.on('data', (data) => console.log(data.toString())); await ready; + clearTimeout(timeout); return async () => { await new Promise((resolve) => exec('docker compose down', () => resolve())); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 9be730c7e1..5547a2c128 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -26,7 +26,7 @@ import { import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { access } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; @@ -36,79 +36,71 @@ import { loginDto, signupDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import request from 'supertest'; -const execPromise = promisify(exec); +type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; +type EventType = 'upload' | 'delete'; +type WaitOptions = { event: EventType; assetId: string; timeout?: number }; +type AdminSetupOptions = { onboarding?: boolean }; +type AssetData = { bytes?: Buffer; filename: string }; -export const app = 'http://127.0.0.1:2283/api'; - -const directoryExists = (directory: string) => - access(directory) - .then(() => true) - .catch(() => false); +const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich'; +const baseUrl = 'http://127.0.0.1:2283'; +export const app = `${baseUrl}/api`; // TODO move test assets into e2e/assets export const testAssetDir = path.resolve(`./../server/test/assets/`); export const testAssetDirInternal = '/data/assets'; export const tempDir = tmpdir(); - -const serverContainerName = 'immich-e2e-server'; -const mediaDir = '/usr/src/app/upload'; -const dirs = [ - `"${mediaDir}/thumbs"`, - `"${mediaDir}/upload"`, - `"${mediaDir}/library"`, - `"${mediaDir}/encoded-video"`, -].join(' '); - -if (!(await directoryExists(`${testAssetDir}/albums`))) { - throw new Error( - `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`, - ); -} - -export const asBearerAuth = (accessToken: string) => ({ - Authorization: `Bearer ${accessToken}`, -}); - +export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); +export const immichCli = async (args: string[]) => { + let _resolve: (value: CliResponse) => void; + const deferred = new Promise((resolve) => (_resolve = resolve)); + const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; + const child = spawn('node', _args, { + stdio: 'pipe', + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => (stdout += data.toString())); + child.stderr.on('data', (data) => (stderr += data.toString())); + child.on('exit', (exitCode) => { + _resolve({ + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode, + }); + }); + + return deferred; +}; let client: pg.Client | null = null; -export const fileUtils = { - reset: async () => { - await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`); - }, - unzip: async (input: string, output: string) => { - await execPromise(`unzip -o -d "${output}" "${input}"`); - }, - sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'), +const events: Record> = { + upload: new Set(), + delete: new Set(), }; -export const dbUtils = { - createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => { - if (!client) { - return; - } +const callbacks: Record void> = {}; - const vector = Array.from({ length: 512 }, Math.random); - const embedding = `[${vector.join(',')}]`; +const execPromise = promisify(exec); - await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [ - assetId, - personId, - embedding, - ]); - }, - setPersonThumbnail: async (personId: string) => { - if (!client) { - return; - } +const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { + events[event].add(assetId); + const callback = callbacks[assetId]; + if (callback) { + callback(); + delete callbacks[assetId]; + } +}; - await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]); - }, - reset: async (tables?: string[]) => { +export const utils = { + resetDatabase: async (tables?: string[]) => { try { if (!client) { - client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich'); + client = new pg.Client(dbUrl); await client.connect(); } @@ -134,83 +126,27 @@ export const dbUtils = { throw error; } }, - teardown: async () => { - try { - if (client) { - await client.end(); - client = null; - } - } catch (error) { - console.error('Failed to teardown database', error); - throw error; - } + + resetFilesystem: async () => { + const mediaInternal = '/usr/src/app/upload'; + const dirs = [ + `"${mediaInternal}/thumbs"`, + `"${mediaInternal}/upload"`, + `"${mediaInternal}/library"`, + `"${mediaInternal}/encoded-video"`, + ].join(' '); + + await execPromise(`docker exec -i "immich-e2e-server" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`); }, -}; -export interface CliResponse { - stdout: string; - stderr: string; - exitCode: number | null; -} -export const immichCli = async (args: string[]) => { - let _resolve: (value: CliResponse) => void; - const deferred = new Promise((resolve) => (_resolve = resolve)); - const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; - const child = spawn('node', _args, { - stdio: 'pipe', - }); + unzip: async (input: string, output: string) => { + await execPromise(`unzip -o -d "${output}" "${input}"`); + }, - let stdout = ''; - let stderr = ''; + sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'), - child.stdout.on('data', (data) => (stdout += data.toString())); - child.stderr.on('data', (data) => (stderr += data.toString())); - child.on('exit', (exitCode) => { - _resolve({ - stdout: stdout.trim(), - stderr: stderr.trim(), - exitCode, - }); - }); - - return deferred; -}; - -export interface AdminSetupOptions { - onboarding?: boolean; -} - -export enum SocketEvent { - UPLOAD = 'upload', - DELETE = 'delete', -} - -export type EventType = 'upload' | 'delete'; -export interface WaitOptions { - event: EventType; - assetId: string; - timeout?: number; -} - -const events: Record> = { - upload: new Set(), - delete: new Set(), -}; - -const callbacks: Record void> = {}; - -const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { - events[event].add(assetId); - const callback = callbacks[assetId]; - if (callback) { - callback(); - delete callbacks[assetId]; - } -}; - -export const wsUtils = { - connect: async (accessToken: string) => { - const websocket = io('http://127.0.0.1:2283', { + connectWebsocket: async (accessToken: string) => { + const websocket = io(baseUrl, { path: '/api/socket.io', transports: ['websocket'], extraHeaders: { Authorization: `Bearer ${accessToken}` }, @@ -226,7 +162,8 @@ export const wsUtils = { .connect(); }); }, - disconnect: (ws: Socket) => { + + disconnectWebsocket: (ws: Socket) => { if (ws?.connected) { ws.disconnect(); } @@ -235,14 +172,15 @@ export const wsUtils = { set.clear(); } }, - waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise => { + + waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise => { const set = events[event]; if (set.has(assetId)) { return; } return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000); + const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); callbacks[assetId] = () => { clearTimeout(timeout); @@ -250,12 +188,8 @@ export const wsUtils = { }; }); }, -}; -type AssetData = { bytes?: Buffer; filename: string }; - -export const apiUtils = { - setup: () => { + setApiEndpoint: () => { defaults.baseUrl = app; }, @@ -269,17 +203,21 @@ export const apiUtils = { } return response; }, + userSetup: async (accessToken: string, dto: CreateUserDto) => { await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) }); return login({ loginCredentialDto: { email: dto.email, password: dto.password }, }); }, + createApiKey: (accessToken: string) => { return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) }); }, + createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }), + createAsset: async ( accessToken: string, dto?: Partial> & { assetData?: AssetData }, @@ -308,13 +246,16 @@ export const apiUtils = { return body as AssetFileUploadResponseDto; }, + getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), + deleteAssets: (accessToken: string, ids: string[]) => deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), + createPerson: async (accessToken: string, dto?: PersonUpdateDto) => { // TODO fix createPerson to accept a body const person = await createPerson({ headers: asBearerAuth(accessToken) }); - await dbUtils.setPersonThumbnail(person.id); + await utils.setPersonThumbnail(person.id); if (!dto) { return person; @@ -322,24 +263,39 @@ export const apiUtils = { return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) }); }, + + createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => { + if (!client) { + return; + } + + const vector = Array.from({ length: 512 }, Math.random); + const embedding = `[${vector.join(',')}]`; + + await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [ + assetId, + personId, + embedding, + ]); + }, + + setPersonThumbnail: async (personId: string) => { + if (!client) { + return; + } + + await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]); + }, + createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }), + createLibrary: (accessToken: string, dto: CreateLibraryDto) => createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), + validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) => validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), -}; -export const cliUtils = { - login: async () => { - const admin = await apiUtils.adminSetup(); - const key = await apiUtils.createApiKey(admin.accessToken); - await immichCli(['login-key', app, `${key.secret}`]); - return key.secret; - }, -}; - -export const webUtils = { setAuthCookies: async (context: BrowserContext, accessToken: string) => await context.addCookies([ { @@ -373,4 +329,19 @@ export const webUtils = { sameSite: 'Lax', }, ]), + + cliLogin: async () => { + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); + await immichCli(['login-key', app, `${key.secret}`]); + return key.secret; + }, }; + +utils.setApiEndpoint(); + +if (!existsSync(`${testAssetDir}/albums`)) { + throw new Error( + `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`, + ); +} diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index 23210205a3..73d62f1b10 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -1,17 +1,13 @@ import { expect, test } from '@playwright/test'; -import { apiUtils, dbUtils, webUtils } from 'src/utils'; +import { utils } from 'src/utils'; test.describe('Registration', () => { test.beforeAll(() => { - apiUtils.setup(); + utils.setApiEndpoint(); }); test.beforeEach(async () => { - await dbUtils.reset(); - }); - - test.afterAll(async () => { - await dbUtils.teardown(); + await utils.resetDatabase(); }); test('admin registration', async ({ page }) => { @@ -45,8 +41,8 @@ test.describe('Registration', () => { }); test('user registration', async ({ context, page }) => { - const admin = await apiUtils.adminSetup(); - await webUtils.setAuthCookies(context, admin.accessToken); + const admin = await utils.adminSetup(); + await utils.setAuthCookies(context, admin.accessToken); // create user await page.goto('/admin/user-management'); diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index 6b2dbad95c..3540ed72e2 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -7,7 +7,7 @@ import { createAlbum, } from '@immich/sdk'; import { test } from '@playwright/test'; -import { apiUtils, asBearerAuth, dbUtils } from 'src/utils'; +import { asBearerAuth, utils } from 'src/utils'; test.describe('Shared Links', () => { let admin: LoginResponseDto; @@ -17,10 +17,10 @@ test.describe('Shared Links', () => { let sharedLinkPassword: SharedLinkResponseDto; test.beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - asset = await apiUtils.createAsset(admin.accessToken); + utils.setApiEndpoint(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + asset = await utils.createAsset(admin.accessToken); album = await createAlbum( { createAlbumDto: { @@ -30,21 +30,17 @@ test.describe('Shared Links', () => { }, { headers: asBearerAuth(admin.accessToken) }, ); - sharedLink = await apiUtils.createSharedLink(admin.accessToken, { + sharedLink = await utils.createSharedLink(admin.accessToken, { type: SharedLinkType.Album, albumId: album.id, }); - sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, { + sharedLinkPassword = await utils.createSharedLink(admin.accessToken, { type: SharedLinkType.Album, albumId: album.id, password: 'test-password', }); }); - test.afterAll(async () => { - await dbUtils.teardown(); - }); - test('download from a shared link', async ({ page }) => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index b8cc098ddd..d7dcde4c38 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ test: { include: ['src/{api,cli}/specs/*.e2e-spec.ts'], globalSetup, + testTimeout: 10_000, poolOptions: { threads: { singleThread: true,