diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 2873bb0c3e..2bb0e7c4d1 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -7,13 +7,12 @@ import { } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; -import { createHash } from 'node:crypto'; import { readFile, writeFile } from 'node:fs/promises'; 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, tempDir, testAssetDir, wsUtils } from 'src/utils'; +import { apiUtils, app, dbUtils, fileUtils, tempDir, testAssetDir, wsUtils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -21,8 +20,6 @@ const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; -const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'); - const readTags = async (bytes: Buffer, filename: string) => { const filepath = join(tempDir, filename); await writeFile(filepath, bytes); @@ -739,8 +736,8 @@ describe('/asset', () => { const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id); const original = await readFile(locationAssetFilepath); - const originalChecksum = sha1(original); - const downloadChecksum = sha1(body); + const originalChecksum = fileUtils.sha1(original); + const downloadChecksum = fileUtils.sha1(body); expect(originalChecksum).toBe(downloadChecksum); expect(downloadChecksum).toBe(asset.checksum); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts index cf4aae3e0a..af328934b4 100644 --- a/e2e/src/api/specs/download.e2e-spec.ts +++ b/e2e/src/api/specs/download.e2e-spec.ts @@ -1,18 +1,23 @@ import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; +import { readFile, writeFile } from 'node:fs/promises'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { apiUtils, app, dbUtils, fileUtils, tempDir } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; describe('/download', () => { let admin: LoginResponseDto; let asset1: AssetFileUploadResponseDto; + let asset2: AssetFileUploadResponseDto; beforeAll(async () => { apiUtils.setup(); await dbUtils.reset(); admin = await apiUtils.adminSetup(); - asset1 = await apiUtils.createAsset(admin.accessToken); + [asset1, asset2] = await Promise.all([ + apiUtils.createAsset(admin.accessToken), + apiUtils.createAsset(admin.accessToken), + ]); }); describe('POST /download/info', () => { @@ -40,6 +45,39 @@ describe('/download', () => { }); }); + describe('POST /download/archive', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .post(`/download/archive`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should download an archive', async () => { + const { status, body } = await request(app) + .post('/download/archive') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(status).toBe(200); + expect(body instanceof Buffer).toBe(true); + + await writeFile(`${tempDir}/archive.zip`, body); + await fileUtils.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); + } + }); + }); + describe('POST /download/asset/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).post(`/download/asset/${asset1.id}`); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 34f25e396d..9be730c7e1 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -25,6 +25,7 @@ import { } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; import { access } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -76,6 +77,10 @@ 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'), }; export const dbUtils = { @@ -150,7 +155,7 @@ export interface CliResponse { export const immichCli = async (args: string[]) => { let _resolve: (value: CliResponse) => void; const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve)); - const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args]; + const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; const child = spawn('node', _args, { stdio: 'pipe', });