diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index fb02aff2ff..8228ff2893 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -4,7 +4,6 @@ name: immich-e2e
 
 x-server-build: &server-common
   image: immich-server:latest
-  container_name: immich-e2e-server
   build:
     context: ../
     dockerfile: server/Dockerfile
@@ -23,14 +22,16 @@ x-server-build: &server-common
 
 services:
   immich-server:
+    container_name: immich-e2e-server
     command: [ "./start.sh", "immich" ]
     <<: *server-common
     ports:
       - 2283:3001
 
-  # immich-microservices:
-  #   command: [ "./start.sh", "microservices" ]
-  #   <<: *server-common
+  immich-microservices:
+    container_name: immich-e2e-microservices
+    command: [ "./start.sh", "microservices" ]
+    <<: *server-common
 
   redis:
     image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 44155ac834..954d1cc3fc 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -12,11 +12,14 @@
         "@immich/cli": "file:../cli",
         "@immich/sdk": "file:../open-api/typescript-sdk",
         "@playwright/test": "^1.41.2",
+        "@types/luxon": "^3.4.2",
         "@types/node": "^20.11.17",
         "@types/pg": "^8.11.0",
         "@types/supertest": "^6.0.2",
         "@vitest/coverage-v8": "^1.3.0",
+        "luxon": "^3.4.4",
         "pg": "^8.11.3",
+        "socket.io-client": "^4.7.4",
         "supertest": "^6.3.4",
         "typescript": "^5.3.3",
         "vitest": "^1.3.0"
@@ -781,6 +784,12 @@
       "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
       "dev": true
     },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
+      "dev": true
+    },
     "node_modules/@types/cookiejar": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -799,6 +808,12 @@
       "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
       "dev": true
     },
+    "node_modules/@types/luxon": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
+      "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
+      "dev": true
+    },
     "node_modules/@types/methods": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -1263,6 +1278,28 @@
         "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
+    "node_modules/engine.io-client": {
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
+      "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
+      "dev": true,
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.11.0",
+        "xmlhttprequest-ssl": "~2.0.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
+      "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/es-define-property": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -1704,6 +1741,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/luxon": {
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
+      "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.30.7",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
@@ -2346,6 +2392,34 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/socket.io-client": {
+      "version": "4.7.4",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
+      "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
+      "dev": true,
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.2",
+        "engine.io-client": "~6.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+      "dev": true,
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2743,6 +2817,36 @@
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "dev": true
     },
+    "node_modules/ws": {
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+      "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xmlhttprequest-ssl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
+      "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/e2e/package.json b/e2e/package.json
index ebd5b9aeae..7bbdfd1d9d 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -16,11 +16,14 @@
     "@immich/cli": "file:../cli",
     "@immich/sdk": "file:../open-api/typescript-sdk",
     "@playwright/test": "^1.41.2",
+    "@types/luxon": "^3.4.2",
     "@types/node": "^20.11.17",
     "@types/pg": "^8.11.0",
     "@types/supertest": "^6.0.2",
     "@vitest/coverage-v8": "^1.3.0",
+    "luxon": "^3.4.4",
     "pg": "^8.11.3",
+    "socket.io-client": "^4.7.4",
     "supertest": "^6.3.4",
     "typescript": "^5.3.3",
     "vitest": "^1.3.0"
diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts
index 738411338f..39c075dba2 100644
--- a/e2e/src/api/specs/activity.e2e-spec.ts
+++ b/e2e/src/api/specs/activity.e2e-spec.ts
@@ -1,7 +1,7 @@
 import {
   ActivityCreateDto,
   AlbumResponseDto,
-  AssetResponseDto,
+  AssetFileUploadResponseDto,
   LoginResponseDto,
   ReactionType,
   createActivity as create,
@@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 describe('/activity', () => {
   let admin: LoginResponseDto;
   let nonOwner: LoginResponseDto;
-  let asset: AssetResponseDto;
+  let asset: AssetFileUploadResponseDto;
   let album: AlbumResponseDto;
 
   const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
     create(
       { activityCreateDto: dto },
-      { headers: asBearerAuth(accessToken || admin.accessToken) }
+      { headers: asBearerAuth(accessToken || admin.accessToken) },
     );
 
   beforeAll(async () => {
@@ -40,7 +40,7 @@ describe('/activity', () => {
           sharedWithUserIds: [nonOwner.userId],
         },
       },
-      { headers: asBearerAuth(admin.accessToken) }
+      { headers: asBearerAuth(admin.accessToken) },
     );
   });
 
@@ -61,7 +61,7 @@ describe('/activity', () => {
         .set('Authorization', `Bearer ${admin.accessToken}`);
       expect(status).toEqual(400);
       expect(body).toEqual(
-        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
+        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
       );
     });
 
@@ -72,7 +72,7 @@ describe('/activity', () => {
         .set('Authorization', `Bearer ${admin.accessToken}`);
       expect(status).toEqual(400);
       expect(body).toEqual(
-        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
+        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
       );
     });
 
@@ -83,7 +83,7 @@ describe('/activity', () => {
         .set('Authorization', `Bearer ${admin.accessToken}`);
       expect(status).toEqual(400);
       expect(body).toEqual(
-        errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))
+        errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
       );
     });
 
@@ -104,7 +104,7 @@ describe('/activity', () => {
             assetIds: [asset.id],
           },
         },
-        { headers: asBearerAuth(admin.accessToken) }
+        { headers: asBearerAuth(admin.accessToken) },
       );
 
       const [reaction] = await Promise.all([
@@ -216,7 +216,7 @@ describe('/activity', () => {
         .send({ albumId: uuidDto.invalid });
       expect(status).toEqual(400);
       expect(body).toEqual(
-        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
+        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
       );
     });
 
@@ -230,7 +230,7 @@ describe('/activity', () => {
         errorDto.badRequest([
           'comment must be a string',
           'comment should not be empty',
-        ])
+        ]),
       );
     });
 
@@ -357,7 +357,7 @@ describe('/activity', () => {
   describe('DELETE /activity/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).delete(
-        `/activity/${uuidDto.notFound}`
+        `/activity/${uuidDto.notFound}`,
       );
       expect(status).toBe(401);
       expect(body).toEqual(errorDto.unauthorized);
@@ -421,7 +421,7 @@ describe('/activity', () => {
 
       expect(status).toBe(400);
       expect(body).toEqual(
-        errorDto.badRequest('Not found or no activity.delete access')
+        errorDto.badRequest('Not found or no activity.delete access'),
       );
     });
 
@@ -432,7 +432,7 @@ describe('/activity', () => {
           type: ReactionType.Comment,
           comment: 'This is a test comment',
         },
-        nonOwner.accessToken
+        nonOwner.accessToken,
       );
 
       const { status } = await request(app)
diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts
index c131edc49c..3385e50f4d 100644
--- a/e2e/src/api/specs/album.e2e-spec.ts
+++ b/e2e/src/api/specs/album.e2e-spec.ts
@@ -1,6 +1,6 @@
 import {
   AlbumResponseDto,
-  AssetResponseDto,
+  AssetFileUploadResponseDto,
   LoginResponseDto,
   SharedLinkType,
   deleteUser,
@@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
 describe('/album', () => {
   let admin: LoginResponseDto;
   let user1: LoginResponseDto;
-  let user1Asset1: AssetResponseDto;
-  let user1Asset2: AssetResponseDto;
+  let user1Asset1: AssetFileUploadResponseDto;
+  let user1Asset2: AssetFileUploadResponseDto;
   let user1Albums: AlbumResponseDto[];
   let user2: LoginResponseDto;
   let user2Albums: AlbumResponseDto[];
@@ -95,7 +95,7 @@ describe('/album', () => {
 
     await deleteUser(
       { id: user3.userId },
-      { headers: asBearerAuth(admin.accessToken) }
+      { headers: asBearerAuth(admin.accessToken) },
     );
   });
 
@@ -112,7 +112,7 @@ describe('/album', () => {
         .set('Authorization', `Bearer ${user1.accessToken}`);
       expect(status).toEqual(400);
       expect(body).toEqual(
-        errorDto.badRequest(['shared must be a boolean value'])
+        errorDto.badRequest(['shared must be a boolean value']),
       );
     });
 
@@ -148,7 +148,7 @@ describe('/album', () => {
             albumName: user2SharedUser,
             shared: true,
           }),
-        ])
+        ]),
       );
     });
 
@@ -175,7 +175,7 @@ describe('/album', () => {
             albumName: user1NotShared,
             shared: false,
           }),
-        ])
+        ]),
       );
     });
 
@@ -202,7 +202,7 @@ describe('/album', () => {
             albumName: user2SharedUser,
             shared: true,
           }),
-        ])
+        ]),
       );
     });
 
@@ -219,7 +219,7 @@ describe('/album', () => {
             albumName: user1NotShared,
             shared: false,
           }),
-        ])
+        ]),
       );
     });
 
@@ -251,7 +251,7 @@ describe('/album', () => {
   describe('GET /album/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).get(
-        `/album/${user1Albums[0].id}`
+        `/album/${user1Albums[0].id}`,
       );
       expect(status).toBe(401);
       expect(body).toEqual(errorDto.unauthorized);
@@ -361,7 +361,7 @@ describe('/album', () => {
   describe('PUT /album/:id/assets', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).put(
-        `/album/${user1Albums[0].id}/assets`
+        `/album/${user1Albums[0].id}/assets`,
       );
       expect(status).toBe(401);
       expect(body).toEqual(errorDto.unauthorized);
@@ -519,7 +519,7 @@ describe('/album', () => {
       expect(body).toEqual(
         expect.objectContaining({
           sharedUsers: [expect.objectContaining({ id: user2.userId })],
-        })
+        }),
       );
     });
 
diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
new file mode 100644
index 0000000000..db1821260b
--- /dev/null
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -0,0 +1,481 @@
+import {
+  AssetFileUploadResponseDto,
+  AssetResponseDto,
+  LoginResponseDto,
+  SharedLinkType,
+} from '@immich/sdk';
+import { DateTime } from 'luxon';
+import { Socket } from 'socket.io-client';
+import { createUserDto, uuidDto } from 'src/fixtures';
+import { errorDto } from 'src/responses';
+import { apiUtils, app, dbUtils } from 'src/utils';
+import request from 'supertest';
+import { beforeAll, describe, expect, it } from 'vitest';
+
+const today = DateTime.fromObject({
+  year: 2023,
+  month: 11,
+  day: 3,
+}) as DateTime<true>;
+const yesterday = today.minus({ days: 1 });
+
+describe('/asset', () => {
+  let admin: LoginResponseDto;
+  let user1: LoginResponseDto;
+  let user2: LoginResponseDto;
+  let userStats: LoginResponseDto;
+  let asset1: AssetFileUploadResponseDto;
+  let asset2: AssetFileUploadResponseDto;
+  let asset3: AssetFileUploadResponseDto;
+  let asset4: AssetFileUploadResponseDto; // user2 asset
+  let asset5: AssetFileUploadResponseDto;
+  let asset6: AssetFileUploadResponseDto;
+  let ws: Socket;
+
+  beforeAll(async () => {
+    apiUtils.setup();
+    await dbUtils.reset();
+    admin = await apiUtils.adminSetup({ onboarding: false });
+    [user1, user2, userStats] = await Promise.all([
+      apiUtils.userSetup(admin.accessToken, createUserDto.user1),
+      apiUtils.userSetup(admin.accessToken, createUserDto.user2),
+      apiUtils.userSetup(admin.accessToken, createUserDto.user3),
+    ]);
+
+    [asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([
+      apiUtils.createAsset(user1.accessToken),
+      apiUtils.createAsset(user1.accessToken),
+      apiUtils.createAsset(
+        user1.accessToken,
+        {
+          isFavorite: true,
+          isExternal: true,
+          isReadOnly: true,
+          fileCreatedAt: yesterday.toISO(),
+          fileModifiedAt: yesterday.toISO(),
+        },
+        { filename: 'example.mp4' },
+      ),
+      apiUtils.createAsset(user2.accessToken),
+      apiUtils.createAsset(user1.accessToken),
+      apiUtils.createAsset(user1.accessToken),
+
+      // stats
+      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' },
+      ),
+    ]);
+
+    const person1 = await apiUtils.createPerson(user1.accessToken, {
+      name: 'Test Person',
+    });
+    await dbUtils.createFace({ assetId: asset1.id, personId: person1.id });
+  });
+
+  describe('GET /asset/:id', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get(
+        `/asset/${uuidDto.notFound}`,
+      );
+      expect(body).toEqual(errorDto.unauthorized);
+      expect(status).toBe(401);
+    });
+
+    it('should require a valid id', async () => {
+      const { status, body } = await request(app)
+        .get(`/asset/${uuidDto.invalid}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
+    });
+
+    it('should require access', async () => {
+      const { status, body } = await request(app)
+        .get(`/asset/${asset4.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.noPermission);
+    });
+
+    it('should get the asset info', async () => {
+      const { status, body } = await request(app)
+        .get(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+      expect(status).toBe(200);
+      expect(body).toMatchObject({ id: asset1.id });
+    });
+
+    it('should work with a shared link', async () => {
+      const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
+        type: SharedLinkType.Individual,
+        assetIds: [asset1.id],
+      });
+
+      const { status, body } = await request(app).get(
+        `/asset/${asset1.id}?key=${sharedLink.key}`,
+      );
+      expect(status).toBe(200);
+      expect(body).toMatchObject({ id: asset1.id });
+    });
+
+    it('should not send people data for shared links for un-authenticated users', async () => {
+      const { status, body } = await request(app)
+        .get(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+
+      expect(status).toEqual(200);
+      expect(body).toMatchObject({
+        id: asset1.id,
+        isFavorite: false,
+        people: [
+          {
+            birthDate: null,
+            id: expect.any(String),
+            isHidden: false,
+            name: 'Test Person',
+            thumbnailPath: '/my/awesome/thumbnail.jpg',
+          },
+        ],
+      });
+
+      const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
+        type: SharedLinkType.Individual,
+        assetIds: [asset1.id],
+      });
+
+      const data = await request(app).get(
+        `/asset/${asset1.id}?key=${sharedLink.key}`,
+      );
+      expect(data.status).toBe(200);
+      expect(data.body).toMatchObject({ people: [] });
+    });
+  });
+
+  describe('GET /asset/statistics', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/asset/statistics');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should return stats of all assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/statistics')
+        .set('Authorization', `Bearer ${userStats.accessToken}`);
+
+      expect(body).toEqual({ images: 3, videos: 1, total: 4 });
+      expect(status).toBe(200);
+    });
+
+    it('should return stats of all favored assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/statistics')
+        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .query({ isFavorite: true });
+
+      expect(status).toBe(200);
+      expect(body).toEqual({ images: 1, videos: 1, total: 2 });
+    });
+
+    it('should return stats of all archived assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/statistics')
+        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .query({ isArchived: true });
+
+      expect(status).toBe(200);
+      expect(body).toEqual({ images: 1, videos: 1, total: 2 });
+    });
+
+    it('should return stats of all favored and archived assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/statistics')
+        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .query({ isFavorite: true, isArchived: true });
+
+      expect(status).toBe(200);
+      expect(body).toEqual({ images: 0, videos: 1, total: 1 });
+    });
+
+    it('should return stats of all assets neither favored nor archived', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/statistics')
+        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .query({ isFavorite: false, isArchived: false });
+
+      expect(status).toBe(200);
+      expect(body).toEqual({ images: 1, videos: 0, total: 1 });
+    });
+  });
+
+  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),
+      ]);
+    });
+
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/asset/random');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it.each(Array(10))('should return 1 random assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/random')
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+
+      expect(status).toBe(200);
+
+      const assets: AssetResponseDto[] = body;
+      expect(assets.length).toBe(1);
+      expect(assets[0].ownerId).toBe(user1.userId);
+      //
+      // assets owned by user2
+      expect(assets[0].id).not.toBe(asset4.id);
+      // assets owned by user1
+      expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
+    });
+
+    it.each(Array(10))('should return 2 random assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/random?count=2')
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+
+      expect(status).toBe(200);
+
+      const assets: AssetResponseDto[] = body;
+      expect(assets.length).toBe(2);
+
+      for (const asset of assets) {
+        expect(asset.ownerId).toBe(user1.userId);
+        // assets owned by user1
+        expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
+        // assets owned by user2
+        expect(asset.id).not.toBe(asset4.id);
+      }
+    });
+
+    it.each(Array(10))(
+      'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
+      async () => {
+        const { status, body } = await request(app)
+          .get('/[]asset/random')
+          .set('Authorization', `Bearer ${user2.accessToken}`);
+
+        expect(status).toBe(200);
+        expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
+      },
+    );
+
+    it('should return error', async () => {
+      const { status } = await request(app)
+        .get('/asset/random?count=ABC')
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+
+      expect(status).toBe(400);
+    });
+  });
+
+  describe('PUT /asset/:id', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).put(
+        `/asset/:${uuidDto.notFound}`,
+      );
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should require a valid id', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${uuidDto.invalid}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
+    });
+
+    it('should require access', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${asset4.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`);
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.noPermission);
+    });
+
+    it('should favorite an asset', async () => {
+      const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
+      expect(before.isFavorite).toBe(false);
+
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ isFavorite: true });
+      expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
+      expect(status).toEqual(200);
+    });
+
+    it('should archive an asset', async () => {
+      const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
+      expect(before.isArchived).toBe(false);
+
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ isArchived: true });
+      expect(body).toMatchObject({ id: asset1.id, isArchived: true });
+      expect(status).toEqual(200);
+    });
+
+    it('should update date time original', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
+
+      expect(body).toMatchObject({
+        id: asset1.id,
+        exifInfo: expect.objectContaining({
+          dateTimeOriginal: '2023-11-20T01:11:00.000Z',
+        }),
+      });
+      expect(status).toEqual(200);
+    });
+
+    it('should reject invalid gps coordinates', async () => {
+      for (const test of [
+        { latitude: 12 },
+        { longitude: 12 },
+        { latitude: 12, longitude: 'abc' },
+        { latitude: 'abc', longitude: 12 },
+        { latitude: null, longitude: 12 },
+        { latitude: 12, longitude: null },
+        { latitude: 91, longitude: 12 },
+        { latitude: -91, longitude: 12 },
+        { latitude: 12, longitude: -181 },
+        { latitude: 12, longitude: 181 },
+      ]) {
+        const { status, body } = await request(app)
+          .put(`/asset/${asset1.id}`)
+          .send(test)
+          .set('Authorization', `Bearer ${user1.accessToken}`);
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest());
+      }
+    });
+
+    it('should update gps data', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ latitude: 12, longitude: 12 });
+
+      expect(body).toMatchObject({
+        id: asset1.id,
+        exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
+      });
+      expect(status).toEqual(200);
+    });
+
+    it('should set the description', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ description: 'Test asset description' });
+      expect(body).toMatchObject({
+        id: asset1.id,
+        exifInfo: expect.objectContaining({
+          description: 'Test asset description',
+        }),
+      });
+      expect(status).toEqual(200);
+    });
+
+    it('should return tagged people', async () => {
+      const { status, body } = await request(app)
+        .put(`/asset/${asset1.id}`)
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ isFavorite: true });
+      expect(status).toEqual(200);
+      expect(body).toMatchObject({
+        id: asset1.id,
+        isFavorite: true,
+        people: [
+          {
+            birthDate: null,
+            id: expect.any(String),
+            isHidden: false,
+            name: 'Test Person',
+            thumbnailPath: '/my/awesome/thumbnail.jpg',
+          },
+        ],
+      });
+    });
+  });
+
+  describe('DELETE /asset', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app)
+        .delete(`/asset`)
+        .send({ ids: [uuidDto.notFound] });
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should require a valid uuid', async () => {
+      const { status, body } = await request(app)
+        .delete(`/asset`)
+        .send({ ids: [uuidDto.invalid] })
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+
+      expect(status).toBe(400);
+      expect(body).toEqual(
+        errorDto.badRequest(['each value in ids must be a UUID']),
+      );
+    });
+
+    it('should throw an error when the id is not found', async () => {
+      const { status, body } = await request(app)
+        .delete(`/asset`)
+        .send({ ids: [uuidDto.notFound] })
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+
+      expect(status).toBe(400);
+      expect(body).toEqual(
+        errorDto.badRequest('Not found or no asset.delete access'),
+      );
+    });
+
+    it('should move an asset to the trash', async () => {
+      const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
+
+      const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
+      expect(before.isTrashed).toBe(false);
+
+      const { status } = await request(app)
+        .delete('/asset')
+        .send({ ids: [assetId] })
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(204);
+
+      const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
+      expect(after.isTrashed).toBe(true);
+    });
+  });
+});
diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts
index f1d7bd1123..22d66baf05 100644
--- a/e2e/src/api/specs/download.e2e-spec.ts
+++ b/e2e/src/api/specs/download.e2e-spec.ts
@@ -1,4 +1,4 @@
-import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
+import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
 import { errorDto } from 'src/responses';
 import { apiUtils, app, dbUtils } from 'src/utils';
 import request from 'supertest';
@@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
 
 describe('/download', () => {
   let admin: LoginResponseDto;
-  let asset1: AssetResponseDto;
+  let asset1: AssetFileUploadResponseDto;
 
   beforeAll(async () => {
     apiUtils.setup();
@@ -35,7 +35,7 @@ describe('/download', () => {
       expect(body).toEqual(
         expect.objectContaining({
           archives: [expect.objectContaining({ assetIds: [asset1.id] })],
-        })
+        }),
       );
     });
   });
@@ -43,7 +43,7 @@ describe('/download', () => {
   describe('POST /download/asset/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).post(
-        `/download/asset/${asset1.id}`
+        `/download/asset/${asset1.id}`,
       );
 
       expect(status).toBe(401);
diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts
index e791c447ac..0bb760fbc5 100644
--- a/e2e/src/api/specs/shared-link.e2e-spec.ts
+++ b/e2e/src/api/specs/shared-link.e2e-spec.ts
@@ -1,11 +1,9 @@
 import {
   AlbumResponseDto,
-  AssetResponseDto,
+  AssetFileUploadResponseDto,
   LoginResponseDto,
-  SharedLinkCreateDto,
   SharedLinkResponseDto,
   SharedLinkType,
-  createSharedLink as create,
   createAlbum,
   deleteUser,
 } from '@immich/sdk';
@@ -17,8 +15,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
 
 describe('/shared-link', () => {
   let admin: LoginResponseDto;
-  let asset1: AssetResponseDto;
-  let asset2: AssetResponseDto;
+  let asset1: AssetFileUploadResponseDto;
+  let asset2: AssetFileUploadResponseDto;
   let user1: LoginResponseDto;
   let user2: LoginResponseDto;
   let album: AlbumResponseDto;
@@ -50,11 +48,11 @@ describe('/shared-link', () => {
     [album, deletedAlbum, metadataAlbum] = await Promise.all([
       createAlbum(
         { createAlbumDto: { albumName: 'album' } },
-        { headers: asBearerAuth(user1.accessToken) }
+        { headers: asBearerAuth(user1.accessToken) },
       ),
       createAlbum(
         { createAlbumDto: { albumName: 'deleted album' } },
-        { headers: asBearerAuth(user2.accessToken) }
+        { headers: asBearerAuth(user2.accessToken) },
       ),
       createAlbum(
         {
@@ -63,7 +61,7 @@ describe('/shared-link', () => {
             assetIds: [asset1.id],
           },
         },
-        { headers: asBearerAuth(user1.accessToken) }
+        { headers: asBearerAuth(user1.accessToken) },
       ),
     ]);
 
@@ -106,7 +104,7 @@ describe('/shared-link', () => {
 
     await deleteUser(
       { id: user2.userId },
-      { headers: asBearerAuth(admin.accessToken) }
+      { headers: asBearerAuth(admin.accessToken) },
     );
   });
 
@@ -132,7 +130,7 @@ describe('/shared-link', () => {
           expect.objectContaining({ id: linkWithPassword.id }),
           expect.objectContaining({ id: linkWithMetadata.id }),
           expect.objectContaining({ id: linkWithoutMetadata.id }),
-        ])
+        ]),
       );
     });
 
@@ -166,7 +164,7 @@ describe('/shared-link', () => {
           album,
           userId: user1.userId,
           type: SharedLinkType.Album,
-        })
+        }),
       );
     });
 
@@ -208,7 +206,7 @@ describe('/shared-link', () => {
           album,
           userId: user1.userId,
           type: SharedLinkType.Album,
-        })
+        }),
       );
     });
 
@@ -225,7 +223,7 @@ describe('/shared-link', () => {
           localDateTime: expect.any(String),
           fileCreatedAt: expect.any(String),
           exifInfo: expect.any(Object),
-        })
+        }),
       );
       expect(body.album).toBeDefined();
     });
@@ -250,7 +248,7 @@ describe('/shared-link', () => {
   describe('GET /shared-link/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).get(
-        `/shared-link/${linkWithAlbum.id}`
+        `/shared-link/${linkWithAlbum.id}`,
       );
 
       expect(status).toBe(401);
@@ -268,7 +266,7 @@ describe('/shared-link', () => {
           album,
           userId: user1.userId,
           type: SharedLinkType.Album,
-        })
+        }),
       );
     });
 
@@ -279,7 +277,7 @@ describe('/shared-link', () => {
 
       expect(status).toBe(400);
       expect(body).toEqual(
-        expect.objectContaining({ message: 'Shared link not found' })
+        expect.objectContaining({ message: 'Shared link not found' }),
       );
     });
   });
@@ -311,7 +309,7 @@ describe('/shared-link', () => {
 
       expect(status).toBe(400);
       expect(body).toEqual(
-        expect.objectContaining({ message: 'Invalid albumId' })
+        expect.objectContaining({ message: 'Invalid albumId' }),
       );
     });
 
@@ -323,7 +321,7 @@ describe('/shared-link', () => {
 
       expect(status).toBe(400);
       expect(body).toEqual(
-        expect.objectContaining({ message: 'Invalid assetIds' })
+        expect.objectContaining({ message: 'Invalid assetIds' }),
       );
     });
 
@@ -338,7 +336,7 @@ describe('/shared-link', () => {
         expect.objectContaining({
           type: SharedLinkType.Album,
           userId: user1.userId,
-        })
+        }),
       );
     });
   });
@@ -375,7 +373,7 @@ describe('/shared-link', () => {
           type: SharedLinkType.Album,
           userId: user1.userId,
           description: 'foo',
-        })
+        }),
       );
     });
   });
@@ -427,7 +425,7 @@ describe('/shared-link', () => {
   describe('DELETE /shared-link/:id', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(app).delete(
-        `/shared-link/${linkWithAlbum.id}`
+        `/shared-link/${linkWithAlbum.id}`,
       );
 
       expect(status).toBe(401);
diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts
new file mode 100644
index 0000000000..2de838f981
--- /dev/null
+++ b/e2e/src/api/specs/trash.e2e-spec.ts
@@ -0,0 +1,107 @@
+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 request from 'supertest';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+describe('/trash', () => {
+  let admin: LoginResponseDto;
+  let ws: Socket;
+
+  beforeAll(async () => {
+    apiUtils.setup();
+    await dbUtils.reset();
+    admin = await apiUtils.adminSetup({ onboarding: false });
+    ws = await wsUtils.connect(admin.accessToken);
+  });
+
+  afterAll(() => {
+    wsUtils.disconnect(ws);
+  });
+
+  describe('POST /trash/empty', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).post('/trash/empty');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should empty the trash', async () => {
+      const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
+      await apiUtils.deleteAssets(admin.accessToken, [assetId]);
+
+      const before = await getAllAssets(
+        {},
+        { headers: asBearerAuth(admin.accessToken) },
+      );
+
+      expect(before.length).toBeGreaterThanOrEqual(1);
+
+      const { status } = await request(app)
+        .post('/trash/empty')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+      expect(status).toBe(204);
+
+      await wsUtils.once(ws, 'on_asset_delete');
+
+      const after = await getAllAssets(
+        {},
+        { headers: asBearerAuth(admin.accessToken) },
+      );
+      expect(after.length).toBe(0);
+    });
+  });
+
+  describe('POST /trash/restore', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).post('/trash/restore');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should restore all trashed assets', async () => {
+      const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
+      await apiUtils.deleteAssets(admin.accessToken, [assetId]);
+
+      const before = await apiUtils.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);
+      expect(after.isTrashed).toBe(false);
+    });
+  });
+
+  describe('POST /trash/restore/assets', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).post('/trash/restore/assets');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should restore a trashed asset by id', async () => {
+      const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
+      await apiUtils.deleteAssets(admin.accessToken, [assetId]);
+
+      const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
+      expect(before.isTrashed).toBe(true);
+
+      const { status } = await request(app)
+        .post('/trash/restore/assets')
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({ ids: [assetId] });
+      expect(status).toBe(204);
+
+      const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
+      expect(after.isTrashed).toBe(false);
+    });
+  });
+});
diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts
index e9e89befd1..038a2c2ca0 100644
--- a/e2e/src/cli/specs/server-info.e2e-spec.ts
+++ b/e2e/src/cli/specs/server-info.e2e-spec.ts
@@ -1,12 +1,9 @@
 import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
-import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
+import { beforeAll, describe, expect, it } from 'vitest';
 
 describe(`immich server-info`, () => {
-  beforeAll(() => {
+  beforeAll(async () => {
     apiUtils.setup();
-  });
-
-  beforeEach(async () => {
     await dbUtils.reset();
     await cliUtils.login();
   });
diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts
index 6dd664e1e2..908118d77c 100644
--- a/e2e/src/cli/specs/upload.e2e-spec.ts
+++ b/e2e/src/cli/specs/upload.e2e-spec.ts
@@ -1,4 +1,5 @@
 import { getAllAlbums, getAllAssets } from '@immich/sdk';
+import { mkdir, readdir, rm, symlink } from 'fs/promises';
 import {
   apiUtils,
   asKeyAuth,
@@ -8,18 +9,18 @@ import {
   testAssetDir,
 } from 'src/utils';
 import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
-import { mkdir, readdir, rm, symlink } from 'fs/promises';
 
 describe(`immich upload`, () => {
   let key: string;
 
-  beforeAll(() => {
+  beforeAll(async () => {
     apiUtils.setup();
+    await dbUtils.reset();
+    key = await cliUtils.login();
   });
 
   beforeEach(async () => {
-    await dbUtils.reset();
-    key = await cliUtils.login();
+    await dbUtils.reset(['assets', 'albums']);
   });
 
   describe('immich upload --recursive', () => {
@@ -33,7 +34,7 @@ describe(`immich upload`, () => {
       expect(stdout.split('\n')).toEqual(
         expect.arrayContaining([
           expect.stringContaining('Successfully uploaded 9 assets'),
-        ])
+        ]),
       );
       expect(exitCode).toBe(0);
 
@@ -55,7 +56,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Successfully uploaded 9 assets'),
           expect.stringContaining('Successfully created 1 new album'),
           expect.stringContaining('Successfully updated 9 assets'),
-        ])
+        ]),
       );
       expect(stderr).toBe('');
       expect(exitCode).toBe(0);
@@ -77,7 +78,7 @@ describe(`immich upload`, () => {
       expect(response1.stdout.split('\n')).toEqual(
         expect.arrayContaining([
           expect.stringContaining('Successfully uploaded 9 assets'),
-        ])
+        ]),
       );
       expect(response1.stderr).toBe('');
       expect(response1.exitCode).toBe(0);
@@ -97,10 +98,10 @@ describe(`immich upload`, () => {
       expect(response2.stdout.split('\n')).toEqual(
         expect.arrayContaining([
           expect.stringContaining(
-            'All assets were already uploaded, nothing to do.'
+            'All assets were already uploaded, nothing to do.',
           ),
           expect.stringContaining('Successfully updated 9 assets'),
-        ])
+        ]),
       );
       expect(response2.stderr).toBe('');
       expect(response2.exitCode).toBe(0);
@@ -127,7 +128,7 @@ describe(`immich upload`, () => {
           expect.stringContaining('Successfully uploaded 9 assets'),
           expect.stringContaining('Successfully created 1 new album'),
           expect.stringContaining('Successfully updated 9 assets'),
-        ])
+        ]),
       );
       expect(stderr).toBe('');
       expect(exitCode).toBe(0);
@@ -148,7 +149,7 @@ describe(`immich upload`, () => {
       for (const file of filesToLink) {
         await symlink(
           `${testAssetDir}/albums/nature/${file}`,
-          `/tmp/albums/nature/${file}`
+          `/tmp/albums/nature/${file}`,
         );
       }
 
@@ -166,7 +167,7 @@ describe(`immich upload`, () => {
         expect.arrayContaining([
           expect.stringContaining('Successfully uploaded 9 assets'),
           expect.stringContaining('Deleting assets that have been uploaded'),
-        ])
+        ]),
       );
       expect(stderr).toBe('');
       expect(exitCode).toBe(0);
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index fbc0b43b31..428c88b454 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -1,5 +1,5 @@
 import {
-  AssetResponseDto,
+  AssetFileUploadResponseDto,
   CreateAlbumDto,
   CreateAssetDto,
   CreateUserDto,
@@ -11,6 +11,8 @@ import {
   createSharedLink,
   createUser,
   defaults,
+  deleteAssets,
+  getAssetInfo,
   login,
   setAdminOnboarding,
   signUpAdmin,
@@ -23,6 +25,7 @@ import { access } from 'node:fs/promises';
 import path from 'node:path';
 import { promisify } from 'node:util';
 import pg from 'pg';
+import { io, type Socket } from 'socket.io-client';
 import { loginDto, signupDto } from 'src/fixtures';
 import request from 'supertest';
 
@@ -39,15 +42,19 @@ const directoryExists = (directory: string) =>
 export const testAssetDir = path.resolve(`./../server/test/assets/`);
 
 const serverContainerName = 'immich-e2e-server';
-const uploadMediaDir = '/usr/src/app/upload/upload';
+const mediaDir = '/usr/src/app/upload';
+const dirs = [
+  `"${mediaDir}/thumbs"`,
+  `"${mediaDir}/upload"`,
+  `"${mediaDir}/library"`,
+].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`
+    `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
   );
 }
 
-const setBaseUrl = () => (defaults.baseUrl = app);
 export const asBearerAuth = (accessToken: string) => ({
   Authorization: `Bearer ${accessToken}`,
 });
@@ -59,7 +66,7 @@ let client: pg.Client | null = null;
 export const fileUtils = {
   reset: async () => {
     await execPromise(
-      `docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
+      `docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
     );
   },
 };
@@ -81,7 +88,7 @@ export const dbUtils = {
 
     await client.query(
       'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
-      [assetId, personId, embedding]
+      [assetId, personId, embedding],
     );
   },
   setPersonThumbnail: async (personId: string) => {
@@ -91,14 +98,14 @@ export const dbUtils = {
 
     await client.query(
       `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
-      [personId]
+      [personId],
     );
   },
   reset: async (tables?: string[]) => {
     try {
       if (!client) {
         client = new pg.Client(
-          'postgres://postgres:postgres@127.0.0.1:5433/immich'
+          'postgres://postgres:postgres@127.0.0.1:5433/immich',
         );
         await client.connect();
       }
@@ -170,10 +177,42 @@ export interface AdminSetupOptions {
   onboarding?: boolean;
 }
 
+export const wsUtils = {
+  connect: async (accessToken: string) => {
+    const websocket = io('http://127.0.0.1:2283', {
+      path: '/api/socket.io',
+      transports: ['websocket'],
+      extraHeaders: { Authorization: `Bearer ${accessToken}` },
+      autoConnect: false,
+      forceNew: true,
+    });
+
+    return new Promise<Socket>((resolve) => {
+      websocket.on('connect', () => resolve(websocket));
+      websocket.connect();
+    });
+  },
+  disconnect: (ws: Socket) => {
+    if (ws?.connected) {
+      ws.disconnect();
+    }
+  },
+  once: <T = any>(ws: Socket, event: string): Promise<T> => {
+    return new Promise<T>((resolve, reject) => {
+      const timeout = setTimeout(() => reject(new Error('Timeout')), 4000);
+      ws.once(event, (data: T) => {
+        clearTimeout(timeout);
+        resolve(data);
+      });
+    });
+  },
+};
+
 export const apiUtils = {
   setup: () => {
-    setBaseUrl();
+    defaults.baseUrl = app;
   },
+
   adminSetup: async (options?: AdminSetupOptions) => {
     options = options || { onboarding: true };
 
@@ -187,7 +226,7 @@ export const apiUtils = {
   userSetup: async (accessToken: string, dto: CreateUserDto) => {
     await createUser(
       { createUserDto: dto },
-      { headers: asBearerAuth(accessToken) }
+      { headers: asBearerAuth(accessToken) },
     );
     return login({
       loginCredentialDto: { email: dto.email, password: dto.password },
@@ -196,48 +235,74 @@ export const apiUtils = {
   createApiKey: (accessToken: string) => {
     return createApiKey(
       { apiKeyCreateDto: { name: 'e2e' } },
-      { headers: asBearerAuth(accessToken) }
+      { headers: asBearerAuth(accessToken) },
     );
   },
   createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
     createAlbum(
       { createAlbumDto: dto },
-      { headers: asBearerAuth(accessToken) }
+      { headers: asBearerAuth(accessToken) },
     ),
   createAsset: async (
     accessToken: string,
-    dto?: Omit<CreateAssetDto, 'assetData'>
+    dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
+    data?: {
+      bytes?: Buffer;
+      filename?: string;
+    },
   ) => {
-    dto = dto || {
+    const _dto = {
       deviceAssetId: 'test-1',
       deviceId: 'test',
       fileCreatedAt: new Date().toISOString(),
       fileModifiedAt: new Date().toISOString(),
+      ...(dto || {}),
     };
-    const { body } = await request(app)
+
+    const _assetData = {
+      bytes: randomBytes(32),
+      filename: 'example.jpg',
+      ...(data || {}),
+    };
+
+    const builder = request(app)
       .post(`/asset/upload`)
-      .field('deviceAssetId', dto.deviceAssetId)
-      .field('deviceId', dto.deviceId)
-      .field('fileCreatedAt', dto.fileCreatedAt)
-      .field('fileModifiedAt', dto.fileModifiedAt)
-      .attach('assetData', randomBytes(32), 'example.jpg')
+      .attach('assetData', _assetData.bytes, _assetData.filename)
       .set('Authorization', `Bearer ${accessToken}`);
 
-    return body as AssetResponseDto;
+    for (const [key, value] of Object.entries(_dto)) {
+      builder.field(key, String(value));
+    }
+
+    const { body } = await builder;
+
+    return body as AssetFileUploadResponseDto;
   },
-  createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
+  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 { id } = await createPerson({ headers: asBearerAuth(accessToken) });
-    await dbUtils.setPersonThumbnail(id);
+    let person = await createPerson({ headers: asBearerAuth(accessToken) });
+    await dbUtils.setPersonThumbnail(person.id);
+
+    if (!dto) {
+      return person;
+    }
+
     return updatePerson(
-      { id, personUpdateDto: dto },
-      { headers: asBearerAuth(accessToken) }
+      { id: person.id, personUpdateDto: dto },
+      { headers: asBearerAuth(accessToken) },
     );
   },
   createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
     createSharedLink(
       { sharedLinkCreateDto: dto },
-      { headers: asBearerAuth(accessToken) }
+      { headers: asBearerAuth(accessToken) },
     ),
 };
 
diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts
index 0e09a68be5..d869775c98 100644
--- a/server/e2e/api/specs/asset.e2e-spec.ts
+++ b/server/e2e/api/specs/asset.e2e-spec.ts
@@ -538,90 +538,6 @@ describe(`${AssetController.name} (e2e)`, () => {
     }
   });
 
-  describe('GET /asset/:id', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`);
-      expect(body).toEqual(errorStub.unauthorized);
-      expect(status).toBe(401);
-    });
-
-    it('should require a valid id', async () => {
-      const { status, body } = await request(server)
-        .get(`/asset/${uuidStub.invalid}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
-    });
-
-    it('should require access', async () => {
-      const { status, body } = await request(server)
-        .get(`/asset/${asset4.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.noPermission);
-    });
-
-    it('should get the asset info', async () => {
-      const { status, body } = await request(server)
-        .get(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-      expect(status).toBe(200);
-      expect(body).toMatchObject({ id: asset1.id });
-    });
-
-    it('should work with a shared link', async () => {
-      const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
-        type: SharedLinkType.INDIVIDUAL,
-        assetIds: [asset1.id],
-      });
-
-      const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
-      expect(status).toBe(200);
-      expect(body).toMatchObject({ id: asset1.id });
-    });
-
-    it('should not send people data for shared links for un-authenticated users', async () => {
-      const personRepository = app.get<IPersonRepository>(IPersonRepository);
-      const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
-
-      await personRepository.createFaces([
-        {
-          assetId: asset1.id,
-          personId: person.id,
-          embedding: Array.from({ length: 512 }, Math.random),
-        },
-      ]);
-
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ isFavorite: true });
-      expect(status).toEqual(200);
-      expect(body).toMatchObject({
-        id: asset1.id,
-        isFavorite: true,
-        people: [
-          {
-            birthDate: null,
-            id: expect.any(String),
-            isHidden: false,
-            name: 'Test Person',
-            thumbnailPath: '',
-          },
-        ],
-      });
-
-      const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
-        type: SharedLinkType.INDIVIDUAL,
-        assetIds: [asset1.id],
-      });
-
-      const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
-      expect(data.status).toBe(200);
-      expect(data.body).toMatchObject({ people: [] });
-    });
-  });
-
   describe('POST /asset/upload', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(server)
@@ -759,286 +675,6 @@ describe(`${AssetController.name} (e2e)`, () => {
     });
   });
 
-  describe('PUT /asset/:id', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).put(`/asset/:${uuidStub.notFound}`);
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should require a valid id', async () => {
-      const { status, body } = await request(server)
-        .put(`/asset/${uuidStub.invalid}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
-    });
-
-    it('should require access', async () => {
-      const { status, body } = await request(server)
-        .put(`/asset/${asset4.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.noPermission);
-    });
-
-    it('should favorite an asset', async () => {
-      expect(asset1).toMatchObject({ isFavorite: false });
-
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ isFavorite: true });
-      expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
-      expect(status).toEqual(200);
-    });
-
-    it('should archive an asset', async () => {
-      expect(asset1).toMatchObject({ isArchived: false });
-
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ isArchived: true });
-      expect(body).toMatchObject({ id: asset1.id, isArchived: true });
-      expect(status).toEqual(200);
-    });
-
-    it('should update date time original', async () => {
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
-
-      expect(body).toMatchObject({
-        id: asset1.id,
-        exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }),
-      });
-      expect(status).toEqual(200);
-    });
-
-    it('should reject invalid gps coordinates', async () => {
-      for (const test of [
-        { latitude: 12 },
-        { longitude: 12 },
-        { latitude: 12, longitude: 'abc' },
-        { latitude: 'abc', longitude: 12 },
-        { latitude: null, longitude: 12 },
-        { latitude: 12, longitude: null },
-        { latitude: 91, longitude: 12 },
-        { latitude: -91, longitude: 12 },
-        { latitude: 12, longitude: -181 },
-        { latitude: 12, longitude: 181 },
-      ]) {
-        const { status, body } = await request(server)
-          .put(`/asset/${asset1.id}`)
-          .send(test)
-          .set('Authorization', `Bearer ${user1.accessToken}`);
-        expect(status).toBe(400);
-        expect(body).toEqual(errorStub.badRequest());
-      }
-    });
-
-    it('should update gps data', async () => {
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ latitude: 12, longitude: 12 });
-
-      expect(body).toMatchObject({
-        id: asset1.id,
-        exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
-      });
-      expect(status).toEqual(200);
-    });
-
-    it('should set the description', async () => {
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ description: 'Test asset description' });
-      expect(body).toMatchObject({
-        id: asset1.id,
-        exifInfo: expect.objectContaining({ description: 'Test asset description' }),
-      });
-      expect(status).toEqual(200);
-    });
-
-    it('should return tagged people', async () => {
-      const personRepository = app.get<IPersonRepository>(IPersonRepository);
-      const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
-
-      await personRepository.createFaces([
-        {
-          assetId: asset1.id,
-          personId: person.id,
-          embedding: Array.from({ length: 512 }, Math.random),
-        },
-      ]);
-
-      const { status, body } = await request(server)
-        .put(`/asset/${asset1.id}`)
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ isFavorite: true });
-      expect(status).toEqual(200);
-      expect(body).toMatchObject({
-        id: asset1.id,
-        isFavorite: true,
-        people: [
-          {
-            birthDate: null,
-            id: expect.any(String),
-            isHidden: false,
-            name: 'Test Person',
-            thumbnailPath: '',
-          },
-        ],
-      });
-    });
-  });
-
-  describe('GET /asset/statistics', () => {
-    beforeEach(async () => {
-      await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true });
-      await api.assetApi.upload(server, user1.accessToken, 'archived_asset', { isArchived: true });
-      await api.assetApi.upload(server, user1.accessToken, 'favored_archived_asset', {
-        isFavorite: true,
-        isArchived: true,
-      });
-    });
-
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get('/asset/statistics');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should return stats of all assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/statistics')
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(body).toEqual({ images: 6, videos: 1, total: 7 });
-      expect(status).toBe(200);
-    });
-
-    it('should return stats of all favored assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/statistics')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ isFavorite: true });
-
-      expect(status).toBe(200);
-      expect(body).toEqual({ images: 2, videos: 1, total: 3 });
-    });
-
-    it('should return stats of all archived assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/statistics')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ isArchived: true });
-
-      expect(status).toBe(200);
-      expect(body).toEqual({ images: 3, videos: 0, total: 3 });
-    });
-
-    it('should return stats of all favored and archived assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/statistics')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ isFavorite: true, isArchived: true });
-
-      expect(status).toBe(200);
-      expect(body).toEqual({ images: 1, videos: 0, total: 1 });
-    });
-
-    it('should return stats of all assets neither favored nor archived', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/statistics')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ isFavorite: false, isArchived: false });
-
-      expect(status).toBe(200);
-      expect(body).toEqual({ images: 2, videos: 0, total: 2 });
-    });
-  });
-
-  describe('GET /asset/random', () => {
-    beforeAll(async () => {
-      await Promise.all([
-        createAsset(user1, new Date('1970-02-01')),
-        createAsset(user1, new Date('1970-02-01')),
-        createAsset(user1, new Date('1970-02-01')),
-        createAsset(user1, new Date('1970-02-01')),
-        createAsset(user1, new Date('1970-02-01')),
-        createAsset(user1, new Date('1970-02-01')),
-      ]);
-    });
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get('/asset/random');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it.each(Array(10))('should return 1 random assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/random')
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(status).toBe(200);
-
-      const assets: AssetResponseDto[] = body;
-      expect(assets.length).toBe(1);
-      expect(assets[0].ownerId).toBe(user1.userId);
-      //
-      // assets owned by user2
-      expect(assets[0].id).not.toBe(asset4.id);
-      // assets owned by user1
-      expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
-    });
-
-    it.each(Array(10))('should return 2 random assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/random?count=2')
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(status).toBe(200);
-
-      const assets: AssetResponseDto[] = body;
-      expect(assets.length).toBe(2);
-
-      for (const asset of assets) {
-        expect(asset.ownerId).toBe(user1.userId);
-        // assets owned by user1
-        expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
-        // assets owned by user2
-        expect(asset.id).not.toBe(asset4.id);
-      }
-    });
-
-    it.each(Array(10))(
-      'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
-      async () => {
-        const { status, body } = await request(server)
-          .get('/[]asset/random')
-          .set('Authorization', `Bearer ${user2.accessToken}`);
-
-        expect(status).toBe(200);
-        expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
-      },
-    );
-
-    it('should return error', async () => {
-      const { status } = await request(server)
-        .get('/asset/random?count=ABC')
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(status).toBe(400);
-    });
-  });
-
   describe('GET /asset/time-buckets', () => {
     it('should require authentication', async () => {
       const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts
index 7dd47e06c6..8d2a1b79bc 100644
--- a/server/e2e/client/asset-api.ts
+++ b/server/e2e/client/asset-api.ts
@@ -1,4 +1,4 @@
-import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain';
+import { 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,8 +74,4 @@ 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/jobs/specs/trash.e2e-spec.ts b/server/e2e/jobs/specs/trash.e2e-spec.ts
deleted file mode 100644
index 5c4b3e9051..0000000000
--- a/server/e2e/jobs/specs/trash.e2e-spec.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-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);
-  });
-});