mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 02:13:51 -04:00
refactor(server): e2e (#7462)
* refactor: trash e2e * refactor: asset e2e
This commit is contained in:
parent
dc0f8756f5
commit
807cd245f4
@ -4,7 +4,6 @@ name: immich-e2e
|
|||||||
|
|
||||||
x-server-build: &server-common
|
x-server-build: &server-common
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
container_name: immich-e2e-server
|
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
@ -23,14 +22,16 @@ x-server-build: &server-common
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
|
container_name: immich-e2e-server
|
||||||
command: [ "./start.sh", "immich" ]
|
command: [ "./start.sh", "immich" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
|
|
||||||
# immich-microservices:
|
immich-microservices:
|
||||||
# command: [ "./start.sh", "microservices" ]
|
container_name: immich-e2e-microservices
|
||||||
# <<: *server-common
|
command: [ "./start.sh", "microservices" ]
|
||||||
|
<<: *server-common
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
|
104
e2e/package-lock.json
generated
104
e2e/package-lock.json
generated
@ -12,11 +12,14 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vitest": "^1.3.0"
|
"vitest": "^1.3.0"
|
||||||
@ -781,6 +784,12 @@
|
|||||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/cookiejar": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
"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==",
|
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/methods": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
"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": "^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": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
@ -1704,6 +1741,15 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.7",
|
"version": "0.30.7",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
||||||
@ -2346,6 +2392,34 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@ -2743,6 +2817,36 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
@ -16,11 +16,14 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vitest": "^1.3.0"
|
"vitest": "^1.3.0"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActivityCreateDto,
|
ActivityCreateDto,
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity as create,
|
createActivity as create,
|
||||||
@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|||||||
describe('/activity', () => {
|
describe('/activity', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonOwner: LoginResponseDto;
|
let nonOwner: LoginResponseDto;
|
||||||
let asset: AssetResponseDto;
|
let asset: AssetFileUploadResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||||
create(
|
create(
|
||||||
{ activityCreateDto: dto },
|
{ activityCreateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken || admin.accessToken) }
|
{ headers: asBearerAuth(accessToken || admin.accessToken) },
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -40,7 +40,7 @@ describe('/activity', () => {
|
|||||||
sharedWithUserIds: [nonOwner.userId],
|
sharedWithUserIds: [nonOwner.userId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
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}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
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}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
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],
|
assetIds: [asset.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [reaction] = await Promise.all([
|
const [reaction] = await Promise.all([
|
||||||
@ -216,7 +216,7 @@ describe('/activity', () => {
|
|||||||
.send({ albumId: uuidDto.invalid });
|
.send({ albumId: uuidDto.invalid });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
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([
|
errorDto.badRequest([
|
||||||
'comment must be a string',
|
'comment must be a string',
|
||||||
'comment should not be empty',
|
'comment should not be empty',
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -357,7 +357,7 @@ describe('/activity', () => {
|
|||||||
describe('DELETE /activity/:id', () => {
|
describe('DELETE /activity/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(
|
||||||
`/activity/${uuidDto.notFound}`
|
`/activity/${uuidDto.notFound}`,
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -421,7 +421,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
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,
|
type: ReactionType.Comment,
|
||||||
comment: 'This is a test comment',
|
comment: 'This is a test comment',
|
||||||
},
|
},
|
||||||
nonOwner.accessToken
|
nonOwner.accessToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
|
|||||||
describe('/album', () => {
|
describe('/album', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user1Asset1: AssetResponseDto;
|
let user1Asset1: AssetFileUploadResponseDto;
|
||||||
let user1Asset2: AssetResponseDto;
|
let user1Asset2: AssetFileUploadResponseDto;
|
||||||
let user1Albums: AlbumResponseDto[];
|
let user1Albums: AlbumResponseDto[];
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let user2Albums: AlbumResponseDto[];
|
let user2Albums: AlbumResponseDto[];
|
||||||
@ -95,7 +95,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
await deleteUser(
|
await deleteUser(
|
||||||
{ id: user3.userId },
|
{ id: user3.userId },
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ describe('/album', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
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,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -202,7 +202,7 @@ describe('/album', () => {
|
|||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -251,7 +251,7 @@ describe('/album', () => {
|
|||||||
describe('GET /album/:id', () => {
|
describe('GET /album/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(
|
||||||
`/album/${user1Albums[0].id}`
|
`/album/${user1Albums[0].id}`,
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -361,7 +361,7 @@ describe('/album', () => {
|
|||||||
describe('PUT /album/:id/assets', () => {
|
describe('PUT /album/:id/assets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(
|
||||||
`/album/${user1Albums[0].id}/assets`
|
`/album/${user1Albums[0].id}/assets`,
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -519,7 +519,7 @@ describe('/album', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
481
e2e/src/api/specs/asset.e2e-spec.ts
Normal file
481
e2e/src/api/specs/asset.e2e-spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
|
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
describe('/download', () => {
|
describe('/download', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetResponseDto;
|
let asset1: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
@ -35,7 +35,7 @@ describe('/download', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -43,7 +43,7 @@ describe('/download', () => {
|
|||||||
describe('POST /download/asset/:id', () => {
|
describe('POST /download/asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(
|
const { status, body } = await request(app).post(
|
||||||
`/download/asset/${asset1.id}`
|
`/download/asset/${asset1.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkCreateDto,
|
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
createSharedLink as create,
|
|
||||||
createAlbum,
|
createAlbum,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@ -17,8 +15,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
describe('/shared-link', () => {
|
describe('/shared-link', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetResponseDto;
|
let asset1: AssetFileUploadResponseDto;
|
||||||
let asset2: AssetResponseDto;
|
let asset2: AssetFileUploadResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
@ -50,11 +48,11 @@ describe('/shared-link', () => {
|
|||||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: { albumName: 'album' } },
|
{ createAlbumDto: { albumName: 'album' } },
|
||||||
{ headers: asBearerAuth(user1.accessToken) }
|
{ headers: asBearerAuth(user1.accessToken) },
|
||||||
),
|
),
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: { albumName: 'deleted album' } },
|
{ createAlbumDto: { albumName: 'deleted album' } },
|
||||||
{ headers: asBearerAuth(user2.accessToken) }
|
{ headers: asBearerAuth(user2.accessToken) },
|
||||||
),
|
),
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{
|
{
|
||||||
@ -63,7 +61,7 @@ describe('/shared-link', () => {
|
|||||||
assetIds: [asset1.id],
|
assetIds: [asset1.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(user1.accessToken) }
|
{ headers: asBearerAuth(user1.accessToken) },
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -106,7 +104,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
await deleteUser(
|
await deleteUser(
|
||||||
{ id: user2.userId },
|
{ 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: linkWithPassword.id }),
|
||||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,7 +164,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -208,7 +206,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -225,7 +223,7 @@ describe('/shared-link', () => {
|
|||||||
localDateTime: expect.any(String),
|
localDateTime: expect.any(String),
|
||||||
fileCreatedAt: expect.any(String),
|
fileCreatedAt: expect.any(String),
|
||||||
exifInfo: expect.any(Object),
|
exifInfo: expect.any(Object),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
expect(body.album).toBeDefined();
|
expect(body.album).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -250,7 +248,7 @@ describe('/shared-link', () => {
|
|||||||
describe('GET /shared-link/:id', () => {
|
describe('GET /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(
|
||||||
`/shared-link/${linkWithAlbum.id}`
|
`/shared-link/${linkWithAlbum.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@ -268,7 +266,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -279,7 +277,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
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(status).toBe(400);
|
||||||
expect(body).toEqual(
|
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(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({ message: 'Invalid assetIds' })
|
expect.objectContaining({ message: 'Invalid assetIds' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -338,7 +336,7 @@ describe('/shared-link', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -375,7 +373,7 @@ describe('/shared-link', () => {
|
|||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
description: 'foo',
|
description: 'foo',
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -427,7 +425,7 @@ describe('/shared-link', () => {
|
|||||||
describe('DELETE /shared-link/:id', () => {
|
describe('DELETE /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(
|
||||||
`/shared-link/${linkWithAlbum.id}`
|
`/shared-link/${linkWithAlbum.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
|
107
e2e/src/api/specs/trash.e2e-spec.ts
Normal file
107
e2e/src/api/specs/trash.e2e-spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,9 @@
|
|||||||
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
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`, () => {
|
describe(`immich server-info`, () => {
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await dbUtils.reset();
|
await dbUtils.reset();
|
||||||
await cliUtils.login();
|
await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||||
|
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||||
import {
|
import {
|
||||||
apiUtils,
|
apiUtils,
|
||||||
asKeyAuth,
|
asKeyAuth,
|
||||||
@ -8,18 +9,18 @@ import {
|
|||||||
testAssetDir,
|
testAssetDir,
|
||||||
} from 'src/utils';
|
} from 'src/utils';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
|
||||||
|
|
||||||
describe(`immich upload`, () => {
|
describe(`immich upload`, () => {
|
||||||
let key: string;
|
let key: string;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
key = await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await dbUtils.reset();
|
await dbUtils.reset(['assets', 'albums']);
|
||||||
key = await cliUtils.login();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('immich upload --recursive', () => {
|
describe('immich upload --recursive', () => {
|
||||||
@ -33,7 +34,7 @@ describe(`immich upload`, () => {
|
|||||||
expect(stdout.split('\n')).toEqual(
|
expect(stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@ -77,7 +78,7 @@ describe(`immich upload`, () => {
|
|||||||
expect(response1.stdout.split('\n')).toEqual(
|
expect(response1.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(response1.stderr).toBe('');
|
expect(response1.stderr).toBe('');
|
||||||
expect(response1.exitCode).toBe(0);
|
expect(response1.exitCode).toBe(0);
|
||||||
@ -97,10 +98,10 @@ describe(`immich upload`, () => {
|
|||||||
expect(response2.stdout.split('\n')).toEqual(
|
expect(response2.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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.stringContaining('Successfully updated 9 assets'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(response2.stderr).toBe('');
|
expect(response2.stderr).toBe('');
|
||||||
expect(response2.exitCode).toBe(0);
|
expect(response2.exitCode).toBe(0);
|
||||||
@ -127,7 +128,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@ -148,7 +149,7 @@ describe(`immich upload`, () => {
|
|||||||
for (const file of filesToLink) {
|
for (const file of filesToLink) {
|
||||||
await symlink(
|
await symlink(
|
||||||
`${testAssetDir}/albums/nature/${file}`,
|
`${testAssetDir}/albums/nature/${file}`,
|
||||||
`/tmp/albums/nature/${file}`
|
`/tmp/albums/nature/${file}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
119
e2e/src/utils.ts
119
e2e/src/utils.ts
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
AssetResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
CreateUserDto,
|
CreateUserDto,
|
||||||
@ -11,6 +11,8 @@ import {
|
|||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUser,
|
||||||
defaults,
|
defaults,
|
||||||
|
deleteAssets,
|
||||||
|
getAssetInfo,
|
||||||
login,
|
login,
|
||||||
setAdminOnboarding,
|
setAdminOnboarding,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
@ -23,6 +25,7 @@ import { access } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
|
import { io, type Socket } from 'socket.io-client';
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
@ -39,15 +42,19 @@ const directoryExists = (directory: string) =>
|
|||||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||||
|
|
||||||
const serverContainerName = 'immich-e2e-server';
|
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`))) {
|
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||||
throw new Error(
|
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) => ({
|
export const asBearerAuth = (accessToken: string) => ({
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
});
|
});
|
||||||
@ -59,7 +66,7 @@ let client: pg.Client | null = null;
|
|||||||
export const fileUtils = {
|
export const fileUtils = {
|
||||||
reset: async () => {
|
reset: async () => {
|
||||||
await execPromise(
|
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(
|
await client.query(
|
||||||
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
||||||
[assetId, personId, embedding]
|
[assetId, personId, embedding],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
@ -91,14 +98,14 @@ export const dbUtils = {
|
|||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
||||||
[personId]
|
[personId],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
reset: async (tables?: string[]) => {
|
reset: async (tables?: string[]) => {
|
||||||
try {
|
try {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new pg.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();
|
await client.connect();
|
||||||
}
|
}
|
||||||
@ -170,10 +177,42 @@ export interface AdminSetupOptions {
|
|||||||
onboarding?: boolean;
|
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 = {
|
export const apiUtils = {
|
||||||
setup: () => {
|
setup: () => {
|
||||||
setBaseUrl();
|
defaults.baseUrl = app;
|
||||||
},
|
},
|
||||||
|
|
||||||
adminSetup: async (options?: AdminSetupOptions) => {
|
adminSetup: async (options?: AdminSetupOptions) => {
|
||||||
options = options || { onboarding: true };
|
options = options || { onboarding: true };
|
||||||
|
|
||||||
@ -187,7 +226,7 @@ export const apiUtils = {
|
|||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||||
await createUser(
|
await createUser(
|
||||||
{ createUserDto: dto },
|
{ createUserDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) }
|
{ headers: asBearerAuth(accessToken) },
|
||||||
);
|
);
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
@ -196,48 +235,74 @@ export const apiUtils = {
|
|||||||
createApiKey: (accessToken: string) => {
|
createApiKey: (accessToken: string) => {
|
||||||
return createApiKey(
|
return createApiKey(
|
||||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||||
{ headers: asBearerAuth(accessToken) }
|
{ headers: asBearerAuth(accessToken) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: dto },
|
{ createAlbumDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) }
|
{ headers: asBearerAuth(accessToken) },
|
||||||
),
|
),
|
||||||
createAsset: async (
|
createAsset: async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
dto?: Omit<CreateAssetDto, 'assetData'>
|
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
|
||||||
|
data?: {
|
||||||
|
bytes?: Buffer;
|
||||||
|
filename?: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
dto = dto || {
|
const _dto = {
|
||||||
deviceAssetId: 'test-1',
|
deviceAssetId: 'test-1',
|
||||||
deviceId: 'test',
|
deviceId: 'test',
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: 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`)
|
.post(`/asset/upload`)
|
||||||
.field('deviceAssetId', dto.deviceAssetId)
|
.attach('assetData', _assetData.bytes, _assetData.filename)
|
||||||
.field('deviceId', dto.deviceId)
|
|
||||||
.field('fileCreatedAt', dto.fileCreatedAt)
|
|
||||||
.field('fileModifiedAt', dto.fileModifiedAt)
|
|
||||||
.attach('assetData', randomBytes(32), 'example.jpg')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
.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
|
// TODO fix createPerson to accept a body
|
||||||
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
|
let person = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||||
await dbUtils.setPersonThumbnail(id);
|
await dbUtils.setPersonThumbnail(person.id);
|
||||||
|
|
||||||
|
if (!dto) {
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
return updatePerson(
|
return updatePerson(
|
||||||
{ id, personUpdateDto: dto },
|
{ id: person.id, personUpdateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) }
|
{ headers: asBearerAuth(accessToken) },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||||
createSharedLink(
|
createSharedLink(
|
||||||
{ sharedLinkCreateDto: dto },
|
{ sharedLinkCreateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) }
|
{ headers: asBearerAuth(accessToken) },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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', () => {
|
describe('POST /asset/upload', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(server)
|
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', () => {
|
describe('GET /asset/time-buckets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
|
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
|
||||||
|
@ -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 { 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 { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
@ -74,8 +74,4 @@ export const assetApi = {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
return body;
|
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);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user