0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-08 03:01:32 -05:00

refactor(server): e2e (#7462)

* refactor: trash e2e

* refactor: asset e2e
This commit is contained in:
Jason Rasmussen 2024-02-27 14:04:38 -05:00 committed by GitHub
parent dc0f8756f5
commit 807cd245f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 856 additions and 547 deletions

View file

@ -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

104
e2e/package-lock.json generated
View file

@ -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",

View file

@ -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"

View file

@ -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)

View file

@ -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 })],
})
}),
);
});

View 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);
});
});
});

View file

@ -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);

View file

@ -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);

View 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);
});
});
});

View file

@ -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();
});

View file

@ -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);

View file

@ -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) },
),
};

View file

@ -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 });

View file

@ -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);
},
};

View file

@ -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);
});
});