diff --git a/e2e/package-lock.json b/e2e/package-lock.json index c2cf8cda28..53bfe57a30 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,6 +15,7 @@ "@types/luxon": "^3.4.2", "@types/node": "^20.11.17", "@types/pg": "^8.11.0", + "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", @@ -26,6 +27,7 @@ "exiftool-vendored": "^24.5.0", "luxon": "^3.4.4", "pg": "^8.11.3", + "pngjs": "^7.0.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", "socket.io-client": "^4.7.4", @@ -1236,6 +1238,15 @@ "node": ">=12" } }, + "node_modules/@types/pngjs": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.4.tgz", + "integrity": "sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3897,6 +3908,15 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", diff --git a/e2e/package.json b/e2e/package.json index ec6fd050d2..9f231c9ddd 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -23,6 +23,7 @@ "@types/luxon": "^3.4.2", "@types/node": "^20.11.17", "@types/pg": "^8.11.0", + "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", @@ -34,6 +35,7 @@ "exiftool-vendored": "^24.5.0", "luxon": "^3.4.4", "pg": "^8.11.3", + "pngjs": "^7.0.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", "socket.io-client": "^4.7.4", diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 773f603906..01f23d18db 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -256,7 +256,7 @@ describe('/album', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining(user1Albums[0].assets[0])], + assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], }); }); @@ -268,7 +268,7 @@ describe('/album', () => { expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [expect.objectContaining(user2Albums[0].assets[0])], + assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })], }); }); @@ -280,7 +280,7 @@ describe('/album', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining(user1Albums[0].assets[0])], + assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], }); }); diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 813e5cf888..2873bb0c3e 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -59,30 +59,25 @@ describe('/asset', () => { ]); // asset location - assetLocation = await apiUtils.createAsset( - admin.accessToken, - {}, - { + assetLocation = await apiUtils.createAsset(admin.accessToken, { + assetData: { filename: 'thompson-springs.jpg', bytes: await readFile(locationAssetFilepath), }, - ); + }); await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id }); user1Assets = await Promise.all([ apiUtils.createAsset(user1.accessToken), apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset( - user1.accessToken, - { - isFavorite: true, - isReadOnly: true, - fileCreatedAt: yesterday.toISO(), - fileModifiedAt: yesterday.toISO(), - }, - { filename: 'example.mp4' }, - ), + apiUtils.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), ]); @@ -98,14 +93,11 @@ describe('/asset', () => { apiUtils.createAsset(userStats.accessToken), apiUtils.createAsset(userStats.accessToken, { isFavorite: true }), apiUtils.createAsset(userStats.accessToken, { isArchived: true }), - apiUtils.createAsset( - userStats.accessToken, - { - isArchived: true, - isFavorite: true, - }, - { filename: 'example.mp4' }, - ), + apiUtils.createAsset(userStats.accessToken, { + isArchived: true, + isFavorite: true, + assetData: { filename: 'example.mp4' }, + }), ]); const person1 = await apiUtils.createPerson(user1.accessToken, { @@ -615,11 +607,9 @@ 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, - {}, - { bytes: await readFile(filepath), filename: basename(filepath) }, - ); + const { id, duplicate } = await apiUtils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, + }); expect(duplicate).toBe(false); @@ -635,14 +625,12 @@ 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 apiUtils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), }, - ); + }); expect(duplicate).toBe(true); }); @@ -669,14 +657,12 @@ 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 apiUtils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), }, - ); + }); await wsUtils.waitForEvent({ event: 'upload', assetId: response.id }); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts index 74f89aa26c..cf4aae3e0a 100644 --- a/e2e/src/api/specs/download.e2e-spec.ts +++ b/e2e/src/api/specs/download.e2e-spec.ts @@ -54,7 +54,7 @@ describe('/download', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(response.status).toBe(200); - expect(response.headers['content-type']).toEqual('image/jpeg'); + expect(response.headers['content-type']).toEqual('image/png'); }); }); }); diff --git a/e2e/src/generators.ts b/e2e/src/generators.ts new file mode 100644 index 0000000000..c87427ceab --- /dev/null +++ b/e2e/src/generators.ts @@ -0,0 +1,31 @@ +import { PNG } from 'pngjs'; + +const createPNG = (r: number, g: number, b: number) => { + const image = new PNG({ width: 1, height: 1 }); + image.data[0] = r; + image.data[1] = g; + image.data[2] = b; + image.data[3] = 255; + return PNG.sync.write(image); +}; + +function* newPngFactory() { + for (let r = 0; r < 255; r++) { + for (let g = 0; g < 255; g++) { + for (let b = 0; b < 255; b++) { + yield createPNG(r, g, b); + } + } + } +} + +const pngFactory = newPngFactory(); + +export const makeRandomImage = () => { + const { value } = pngFactory.next(); + if (!value) { + throw new Error('Ran out of random asset data'); + } + + return value; +}; diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 30c7e1f9dc..b02e0053f3 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -21,7 +21,6 @@ import { } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; -import { randomBytes } from 'node:crypto'; import { access } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -29,6 +28,7 @@ import { promisify } from 'node:util'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; import { loginDto, signupDto } from 'src/fixtures'; +import { makeRandomImage } from 'src/generators'; import request from 'supertest'; const execPromise = promisify(exec); @@ -241,6 +241,8 @@ export const wsUtils = { }, }; +type AssetData = { bytes?: Buffer; filename: string }; + export const apiUtils = { setup: () => { defaults.baseUrl = app; @@ -269,11 +271,7 @@ export const apiUtils = { createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }), createAsset: async ( accessToken: string, - dto?: Partial>, - data?: { - bytes?: Buffer; - filename: string; - }, + dto?: Partial> & { assetData?: AssetData }, ) => { const _dto = { deviceAssetId: 'test-1', @@ -283,15 +281,12 @@ export const apiUtils = { ...dto, }; - const _assetData = { - bytes: randomBytes(32), - filename: 'example.jpg', - ...data, - }; + const assetData = dto?.assetData?.bytes || makeRandomImage(); + const filename = dto?.assetData?.filename || 'example.png'; const builder = request(app) .post(`/asset/upload`) - .attach('assetData', _assetData.bytes, _assetData.filename) + .attach('assetData', assetData, filename) .set('Authorization', `Bearer ${accessToken}`); for (const [key, value] of Object.entries(_dto)) {