diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index e3e7381466..555d891ef2 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -6,7 +6,9 @@ import { CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, + JobCreateDto, MaintenanceAction, + ManualJobName, MetadataSearchDto, Permission, PersonCreateDto, @@ -21,6 +23,7 @@ import { checkExistingAssets, createAlbum, createApiKey, + createJob, createLibrary, createPartner, createPerson, @@ -28,10 +31,12 @@ import { createStack, createUserAdmin, deleteAssets, + deleteBackup, getAssetInfo, getConfig, getConfigDefaults, getQueuesLegacy, + listBackups, login, runQueueCommandLegacy, scanLibrary, @@ -82,8 +87,9 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $ export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const immichCli = (args: string[]) => executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise; -export const immichAdmin = (args: string[]) => - executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); +export const dockerExec = (args: string[]) => + executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]); +export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]); export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -147,12 +153,26 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => { }; export const utils = { + connectDatabase: async () => { + if (!client) { + client = new pg.Client(dbUrl); + client.on('end', () => (client = null)); + client.on('error', () => (client = null)); + await client.connect(); + } + + return client; + }, + + disconnectDatabase: async () => { + if (client) { + await client.end(); + } + }, + resetDatabase: async (tables?: string[]) => { try { - if (!client) { - client = new pg.Client(dbUrl); - await client.connect(); - } + client = await utils.connectDatabase(); tables = tables || [ // TODO e2e test for deleting a stack, since it is quite complex @@ -479,6 +499,9 @@ export const utils = { tagAssets: (accessToken: string, tagId: string, assetIds: string[]) => tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }), + createJob: async (accessToken: string, jobCreateDto: JobCreateDto) => + createJob({ jobCreateDto }, { headers: asBearerAuth(accessToken) }), + queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) => runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }), @@ -557,6 +580,31 @@ export const utils = { mkdirSync(`${testAssetDir}/temp`, { recursive: true }); }, + createBackup: async (accessToken: string) => { + await utils.createJob(accessToken, { + name: ManualJobName.BackupDatabase, + }); + + return await utils.poll( + () => request(app).get('/admin/maintenance/backups/list').set('Authorization', `Bearer ${accessToken}`), + ({ status, body }) => status === 200 && body.backups.length === 1, + ({ body }) => body.backups[0], + ); + }, + + resetBackups: async (accessToken: string) => { + const { backups, failedBackups } = await listBackups({ headers: asBearerAuth(accessToken) }); + for (const filename of [...backups, ...failedBackups]) { + await deleteBackup({ filename }, { headers: asBearerAuth(accessToken) }); + } + }, + + prepareTestBackup: async (testBackup: 'corrupted.sql') => { + await dockerExec(['cp', `${testAssetDirInternal}/backups/${testBackup}`, `/data/backups/development-${testBackup}`]) + .promise; + await dockerExec(['gzip', `/data/backups/development-${testBackup}`]).promise; + }, + resetAdminConfig: async (accessToken: string) => { const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); @@ -599,6 +647,25 @@ export const utils = { await utils.waitForQueueFinish(accessToken, 'sidecar'); await utils.waitForQueueFinish(accessToken, 'metadataExtraction'); }, + + async poll(cb: () => Promise, validate: (value: T) => boolean, map?: (value: T) => any) { + let timeout = 0; + while (true) { + try { + const data = await cb(); + if (validate(data)) { + return map ? map(data) : data; + } + timeout++; + if (timeout >= 10) { + throw 'Could not clean up test.'; + } + await new Promise((resolve) => setTimeout(resolve, 5e2)); + } catch { + // no-op + } + } + }, }; utils.initSdk();