diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts index 8d2a1b79bcc67..7dd47e06c6d75 100644 --- a/server/e2e/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto } from '@app/domain'; +import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain'; import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { randomBytes } from 'node:crypto'; @@ -74,4 +74,8 @@ export const assetApi = { expect(status).toBe(200); return body; }, + delete: async (server: any, accessToken: string, dto: AssetBulkDeleteDto) => { + const { status } = await request(server).delete('/asset').set('Authorization', `Bearer ${accessToken}`).send(dto); + expect(status).toBe(204); + }, }; diff --git a/server/e2e/client/index.ts b/server/e2e/client/index.ts index d13f2425e8052..b9c0f2ff380a6 100644 --- a/server/e2e/client/index.ts +++ b/server/e2e/client/index.ts @@ -7,6 +7,7 @@ import { libraryApi } from './library-api'; import { partnerApi } from './partner-api'; import { serverInfoApi } from './server-info-api'; import { sharedLinkApi } from './shared-link-api'; +import { trashApi } from './trash-api'; import { userApi } from './user-api'; export const api = { @@ -17,6 +18,7 @@ export const api = { libraryApi, serverInfoApi, sharedLinkApi, + trashApi, albumApi, userApi, partnerApi, diff --git a/server/e2e/client/trash-api.ts b/server/e2e/client/trash-api.ts new file mode 100644 index 0000000000000..a381253f50be2 --- /dev/null +++ b/server/e2e/client/trash-api.ts @@ -0,0 +1,13 @@ +import request from 'supertest'; +import type { App } from 'supertest/types'; + +export const trashApi = { + async empty(server: App, accessToken: string) { + const { status } = await request(server).post('/trash/empty').set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(204); + }, + async restore(server: App, accessToken: string) { + const { status } = await request(server).post('/trash/restore').set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(204); + }, +}; diff --git a/server/e2e/jobs/specs/trash.e2e-spec.ts b/server/e2e/jobs/specs/trash.e2e-spec.ts new file mode 100644 index 0000000000000..5c4b3e9051d24 --- /dev/null +++ b/server/e2e/jobs/specs/trash.e2e-spec.ts @@ -0,0 +1,80 @@ +import { LoginResponseDto } from '@app/domain'; +import { api } from 'e2e/client'; +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; +import type { App } from 'supertest/types'; +import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils'; + +const assetFilePath = join(IMMICH_TEST_ASSET_PATH, 'formats/png/density_plot.png'); + +describe(`Trash (e2e)`, () => { + let server: App; + let admin: LoginResponseDto; + + beforeAll(async () => { + const app = await testApp.create(); + server = app.getHttpServer(); + }); + + beforeEach(async () => { + await testApp.reset(); + await api.authApi.adminSignUp(server); + admin = await api.authApi.adminLogin(server); + }); + + afterAll(async () => { + await testApp.teardown(); + }); + + it('should move an asset to trash', async () => { + const content = await readFile(assetFilePath); + const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { + content, + filename: basename(assetFilePath), + }); + + const uploadedAsset = await api.assetApi.get(server, admin.accessToken, assetId); + expect(uploadedAsset.isTrashed).toBe(false); + + await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); + + const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId); + expect(deletedAsset.isTrashed).toBe(true); + }); + + it('should delete all trashed assets', async () => { + const content = await readFile(assetFilePath); + const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { + content, + filename: basename(assetFilePath), + }); + + await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); + + const assetsBeforeEmpty = await api.assetApi.getAllAssets(server, admin.accessToken); + expect(assetsBeforeEmpty.length).toBe(1); + + await api.trashApi.empty(server, admin.accessToken); + + const assetsAfterEmpty = await api.assetApi.getAllAssets(server, admin.accessToken); + expect(assetsAfterEmpty.length).toBe(0); + }); + + it('should restore all trashed assets', async () => { + const content = await readFile(assetFilePath); + const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { + content, + filename: basename(assetFilePath), + }); + + await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); + + const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId); + expect(deletedAsset.isTrashed).toBe(true); + + await api.trashApi.restore(server, admin.accessToken); + + const restoredAsset = await api.assetApi.get(server, admin.accessToken, assetId); + expect(restoredAsset.isTrashed).toBe(false); + }); +}); diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 0931fd08a19df..b60f7285716ec 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -176,7 +176,7 @@ export class AssetRepository implements IAssetRepository { } getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated { - return this.getAll(pagination, { ...options, id: userId }); + return this.getAll(pagination, { ...options, ownerId: userId }); } @GenerateSql({ params: [[DummyValue.UUID]] })