refactor: album e2e (#7320)

* refactor: album e2e

* refactor: user e2e
This commit is contained in:
Jason Rasmussen 2024-02-21 16:52:13 -05:00 committed by GitHub
parent 173b47033a
commit 546edc2e91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 390 additions and 296 deletions

View File

@ -1,11 +1,15 @@
import { AlbumResponseDto, LoginResponseDto } from '@app/domain'; import {
import { AlbumController } from '@app/immich'; AlbumResponseDto,
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; AssetResponseDto,
import { SharedLinkType } from '@app/infra/entities'; LoginResponseDto,
import { errorStub, userDto, uuidStub } from '@test/fixtures'; SharedLinkType,
deleteUser,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { api } from '../../client'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { testApp } from '../utils';
const user1SharedUser = 'user1SharedUser'; const user1SharedUser = 'user1SharedUser';
const user1SharedLink = 'user1SharedLink'; const user1SharedLink = 'user1SharedLink';
@ -14,193 +18,327 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink'; const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared'; const user2NotShared = 'user2NotShared';
describe(`${AlbumController.name} (e2e)`, () => { describe('/album', () => {
let server: any;
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user1Asset: AssetFileUploadResponseDto; let user1Asset1: AssetResponseDto;
let user1Asset2: AssetResponseDto;
let user1Albums: AlbumResponseDto[]; let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto; let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[]; let user2Albums: AlbumResponseDto[];
let user3: LoginResponseDto; // deleted
beforeAll(async () => { beforeAll(async () => {
server = (await testApp.create()).getHttpServer(); apiUtils.setup();
}); await dbUtils.reset();
afterAll(async () => { admin = await apiUtils.adminSetup();
await testApp.teardown();
});
beforeEach(async () => { [user1, user2, user3] = await Promise.all([
await testApp.reset(); apiUtils.userSetup(admin.accessToken, createUserDto.user1),
await api.authApi.adminSignUp(server); apiUtils.userSetup(admin.accessToken, createUserDto.user2),
admin = await api.authApi.adminLogin(server); apiUtils.userSetup(admin.accessToken, createUserDto.user3),
await Promise.all([
api.userApi.create(server, admin.accessToken, userDto.user1),
api.userApi.create(server, admin.accessToken, userDto.user2),
]); ]);
[user1, user2] = await Promise.all([ [user1Asset1, user1Asset2] = await Promise.all([
api.authApi.login(server, userDto.user1), apiUtils.createAsset(user1.accessToken),
api.authApi.login(server, userDto.user2), apiUtils.createAsset(user1.accessToken),
]); ]);
user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example');
const albums = await Promise.all([ const albums = await Promise.all([
// user 1 // user 1
api.albumApi.create(server, user1.accessToken, { apiUtils.createAlbum(user1.accessToken, {
albumName: user1SharedUser, albumName: user1SharedUser,
sharedWithUserIds: [user2.userId], sharedWithUserIds: [user2.userId],
assetIds: [user1Asset.id], assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}), }),
api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset.id] }),
api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }),
// user 2 // user 2
api.albumApi.create(server, user2.accessToken, { apiUtils.createAlbum(user2.accessToken, {
albumName: user2SharedUser, albumName: user2SharedUser,
sharedWithUserIds: [user1.userId], sharedWithUserIds: [user1.userId],
assetIds: [user1Asset.id], assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
// user 3
apiUtils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
sharedWithUserIds: [user1.userId],
}), }),
api.albumApi.create(server, user2.accessToken, { albumName: user2SharedLink }),
api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }),
]); ]);
user1Albums = albums.slice(0, 3); user1Albums = albums.slice(0, 3);
user2Albums = albums.slice(3); user2Albums = albums.slice(3, 6);
await Promise.all([ await Promise.all([
// add shared link to user1SharedLink album // add shared link to user1SharedLink album
api.sharedLinkApi.create(server, user1.accessToken, { apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.ALBUM, type: SharedLinkType.Album,
albumId: user1Albums[1].id, albumId: user1Albums[1].id,
}), }),
// add shared link to user2SharedLink album // add shared link to user2SharedLink album
api.sharedLinkApi.create(server, user2.accessToken, { apiUtils.createSharedLink(user2.accessToken, {
type: SharedLinkType.ALBUM, type: SharedLinkType.Album,
albumId: user2Albums[1].id, albumId: user2Albums[1].id,
}), }),
]); ]);
await deleteUser(
{ id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
}); });
describe('GET /album', () => { describe('GET /album', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/album'); const { status, body } = await request(app).get('/album');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should reject an invalid shared param', async () => { it('should reject an invalid shared param', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get('/album?shared=invalid') .get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(['shared must be a boolean value'])); expect(body).toEqual(
errorDto.badRequest(['shared must be a boolean value'])
);
}); });
it('should reject an invalid assetId param', async () => { it('should reject an invalid assetId param', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get('/album?assetId=invalid') .get('/album?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(['assetId must be a UUID'])); expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
}); });
it('should not return shared albums with a deleted owner', async () => { it('should not return shared albums with a deleted owner', async () => {
await api.userApi.delete(server, admin.accessToken, user1.userId); const { status, body } = await request(app)
const { status, body } = await request(server)
.get('/album?shared=true') .get('/album?shared=true')
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(3);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedLink, shared: true }), expect.objectContaining({
]), ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedUser,
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
shared: true,
}),
])
); );
}); });
it('should return the album collection including owned and shared', async () => { it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(server).get('/album').set('Authorization', `Bearer ${user1.accessToken}`); const { status, body } = await request(app)
.get('/album')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(3); expect(body).toHaveLength(3);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), expect.objectContaining({
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), ownerId: user1.userId,
expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), albumName: user1SharedUser,
]), shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
shared: false,
}),
])
); );
}); });
it('should return the album collection filtered by shared', async () => { it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get('/album?shared=true') .get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(3); expect(body).toHaveLength(3);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), expect.objectContaining({
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), ownerId: user1.userId,
expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedUser, shared: true }), albumName: user1SharedUser,
]), shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
shared: true,
}),
])
); );
}); });
it('should return the album collection filtered by NOT shared', async () => { it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get('/album?shared=false') .get('/album?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), expect.objectContaining({
]), ownerId: user1.userId,
albumName: user1NotShared,
shared: false,
}),
])
); );
}); });
it('should return the album collection filtered by assetId', async () => { it('should return the album collection filtered by assetId', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example2'); const { status, body } = await request(app)
await api.albumApi.addAssets(server, user1.accessToken, user1Albums[0].id, { ids: [asset.id] }); .get(`/album?assetId=${user1Asset2.id}`)
const { status, body } = await request(server)
.get(`/album?assetId=${asset.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
}); });
it('should return the album collection filtered by assetId and ignores shared=true', async () => { it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get(`/album?shared=true&assetId=${user1Asset.id}`) .get(`/album?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
}); });
it('should return the album collection filtered by assetId and ignores shared=false', async () => { it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.get(`/album?shared=false&assetId=${user1Asset.id}`) .get(`/album?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
}); });
}); });
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/album/${user1Albums[0].id}`
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
});
});
it('should return album info for shared album', async () => {
const { status, body } = await request(app)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
assets: [expect.objectContaining(user2Albums[0].assets[0])],
});
});
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
});
});
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [],
assetCount: 1,
});
});
});
describe('GET /album/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/album/count');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app)
.get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 });
});
});
describe('POST /album', () => { describe('POST /album', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).post('/album').send({ albumName: 'New album' }); const { status, body } = await request(app)
.post('/album')
.send({ albumName: 'New album' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should create an album', async () => { it('should create an album', async () => {
const body = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' }); const { status, body } = await request(app)
.post('/album')
.send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201);
expect(body).toEqual({ expect(body).toEqual({
id: expect.any(String), id: expect.any(String),
createdAt: expect.any(String), createdAt: expect.any(String),
@ -220,113 +358,56 @@ describe(`${AlbumController.name} (e2e)`, () => {
}); });
}); });
describe('GET /album/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/album/count');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(server)
.get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 });
});
});
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/album/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info for shared album', async () => {
const { status, body } = await request(server)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] });
});
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [],
assetCount: 1,
});
});
});
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(server).put(`/album/${user1Albums[0].id}/assets`); const { status, body } = await request(app).put(
`/album/${user1Albums[0].id}/assets`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should be able to add own asset to own album', async () => { it('should be able to add own asset to own album', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example1'); const asset = await apiUtils.createAsset(user1.accessToken);
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`) .put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]); expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
}); });
it('should be able to add own asset to shared album', async () => { it('should be able to add own asset to shared album', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example1'); const asset = await apiUtils.createAsset(user1.accessToken);
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`) .put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]); expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
}); });
}); });
describe('PATCH /album/:id', () => { describe('PATCH /album/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.patch(`/album/${uuidStub.notFound}`) .patch(`/album/${uuidDto.notFound}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should update an album', async () => { it('should update an album', async () => {
const album = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' }); const album = await apiUtils.createAlbum(user1.accessToken, {
const { status, body } = await request(server) albumName: 'New album',
});
const { status, body } = await request(app)
.patch(`/album/${album.id}`) .patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ .send({
@ -345,52 +426,68 @@ describe(`${AlbumController.name} (e2e)`, () => {
describe('DELETE /album/:id/assets', () => { describe('DELETE /album/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`) .delete(`/album/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(server)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]);
});
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(server)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]);
}); });
it('should not be able to remove foreign asset from own album', async () => { it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`) .delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]); expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
}),
]);
}); });
it('should not be able to remove foreign asset from foreign album', async () => { it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`) .delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]); expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
}),
]);
});
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
});
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
}); });
}); });
@ -398,51 +495,57 @@ describe(`${AlbumController.name} (e2e)`, () => {
let album: AlbumResponseDto; let album: AlbumResponseDto;
beforeEach(async () => { beforeEach(async () => {
album = await api.albumApi.create(server, user1.accessToken, { albumName: 'testAlbum' }); album = await apiUtils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
});
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/users`) .put(`/album/${user1Albums[0].id}/users`)
.send({ sharedUserIds: [] }); .send({ sharedUserIds: [] });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should be able to add user to own album', async () => { it('should be able to add user to own album', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] }); .send({ sharedUserIds: [user2.userId] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ sharedUsers: [expect.objectContaining({ id: user2.userId })] })); expect(body).toEqual(
expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })],
})
);
}); });
it('should not be able to share album with owner', async () => { it('should not be able to share album with owner', async () => {
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user1.userId] }); .send({ sharedUserIds: [user1.userId] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Cannot be shared with owner')); expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
}); });
it('should not be able to add existing user to shared album', async () => { it('should not be able to add existing user to shared album', async () => {
await request(server) await request(app)
.put(`/album/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] }); .send({ sharedUserIds: [user2.userId] });
const { status, body } = await request(server) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] }); .send({ sharedUserIds: [user2.userId] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('User already added')); expect(body).toEqual(errorDto.badRequest('User already added'));
}); });
}); });
}); });

View File

@ -15,9 +15,6 @@ import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
const createSharedLink = (dto: SharedLinkCreateDto, accessToken: string) =>
create({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/shared-link', () => { describe('/shared-link', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetResponseDto; let asset1: AssetResponseDto;
@ -78,38 +75,33 @@ describe('/shared-link', () => {
linkWithMetadata, linkWithMetadata,
linkWithoutMetadata, linkWithoutMetadata,
] = await Promise.all([ ] = await Promise.all([
createSharedLink( apiUtils.createSharedLink(user2.accessToken, {
{ type: SharedLinkType.Album, albumId: deletedAlbum.id }, type: SharedLinkType.Album,
user2.accessToken albumId: deletedAlbum.id,
), }),
createSharedLink( apiUtils.createSharedLink(user1.accessToken, {
{ type: SharedLinkType.Album, albumId: album.id }, type: SharedLinkType.Album,
user1.accessToken albumId: album.id,
), }),
createSharedLink( apiUtils.createSharedLink(user1.accessToken, {
{ type: SharedLinkType.Individual, assetIds: [asset1.id] }, type: SharedLinkType.Individual,
user1.accessToken assetIds: [asset1.id],
), }),
createSharedLink( apiUtils.createSharedLink(user1.accessToken, {
{ type: SharedLinkType.Album, albumId: album.id, password: 'foo' }, type: SharedLinkType.Album,
user1.accessToken albumId: album.id,
), password: 'foo',
createSharedLink( }),
{ apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album, type: SharedLinkType.Album,
albumId: metadataAlbum.id, albumId: metadataAlbum.id,
showMetadata: true, showMetadata: true,
}, }),
user1.accessToken apiUtils.createSharedLink(user1.accessToken, {
), type: SharedLinkType.Album,
createSharedLink( albumId: metadataAlbum.id,
{ showMetadata: false,
type: SharedLinkType.Album, }),
albumId: metadataAlbum.id,
showMetadata: false,
},
user1.accessToken
),
]); ]);
await deleteUser( await deleteUser(

View File

@ -1,26 +1,31 @@
import { import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
LoginResponseDto,
UserResponseDto,
createUser,
deleteUser,
getUserById,
} from '@immich/sdk';
import { createUserDto, userDto } from 'src/fixtures'; import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/server-info', () => { describe('/server-info', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
apiUtils.setup(); apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset(); await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false }); admin = await apiUtils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin, userToDelete] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
]);
await deleteUser(
{ id: deletedUser.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
}); });
describe('GET /user', () => { describe('GET /user', () => {
@ -30,60 +35,54 @@ describe('/server-info', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should start with the admin', async () => { it('should get users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/user') .get('/user')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(4);
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' }); expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
);
}); });
it('should hide deleted users', async () => { it('should hide deleted users', async () => {
const user1 = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
await deleteUser(
{ id: user1.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/user`) .get(`/user`)
.query({ isAll: true }) .query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(3);
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' }); expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
);
}); });
it('should include deleted users', async () => { it('should include deleted users', async () => {
const user1 = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
await deleteUser(
{ id: user1.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/user`) .get(`/user`)
.query({ isAll: false }) .query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(2); expect(body).toHaveLength(4);
expect(body[0]).toMatchObject({ expect(body).toEqual(
id: user1.userId, expect.arrayContaining([
email: 'user1@immich.cloud', expect.objectContaining({ email: 'admin@immich.cloud' }),
deletedAt: expect.any(String), expect.objectContaining({ email: 'user1@immich.cloud' }),
}); expect.objectContaining({ email: 'user2@immich.cloud' }),
expect(body[1]).toMatchObject({ expect.objectContaining({ email: 'user3@immich.cloud' }),
id: admin.userId, ])
email: 'admin@immich.cloud', );
});
}); });
}); });
@ -149,13 +148,13 @@ describe('/server-info', () => {
.post(`/user`) .post(`/user`)
.send({ .send({
isAdmin: true, isAdmin: true,
email: 'user1@immich.cloud', email: 'user4@immich.cloud',
password: 'Password123', password: 'password123',
name: 'Immich', name: 'Immich',
}) })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({ expect(body).toMatchObject({
email: 'user1@immich.cloud', email: 'user4@immich.cloud',
isAdmin: false, isAdmin: false,
shouldChangePassword: true, shouldChangePassword: true,
}); });
@ -181,18 +180,9 @@ describe('/server-info', () => {
}); });
describe('DELETE /user/:id', () => { describe('DELETE /user/:id', () => {
let userToDelete: UserResponseDto;
beforeEach(async () => {
userToDelete = await createUser(
{ createUserDto: createUserDto.user1 },
{ headers: asBearerAuth(admin.accessToken) }
);
});
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(
`/user/${userToDelete.id}` `/user/${userToDelete.userId}`
); );
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -200,12 +190,12 @@ describe('/server-info', () => {
it('should delete user', async () => { it('should delete user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/user/${userToDelete.id}`) .delete(`/user/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toMatchObject({
...userToDelete, id: userToDelete.userId,
updatedAt: expect.any(String), updatedAt: expect.any(String),
deletedAt: expect.any(String), deletedAt: expect.any(String),
}); });
@ -231,14 +221,9 @@ describe('/server-info', () => {
} }
it('should not allow a non-admin to become an admin', async () => { it('should not allow a non-admin to become an admin', async () => {
const user = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/user`)
.send({ isAdmin: true, id: user.userId }) .send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);

View File

@ -1,10 +1,14 @@
import { import {
AssetResponseDto, AssetResponseDto,
CreateAlbumDto,
CreateAssetDto, CreateAssetDto,
CreateUserDto, CreateUserDto,
PersonUpdateDto, PersonUpdateDto,
SharedLinkCreateDto,
createAlbum,
createApiKey, createApiKey,
createPerson, createPerson,
createSharedLink,
createUser, createUser,
defaults, defaults,
login, login,
@ -181,6 +185,11 @@ export const apiUtils = {
{ headers: asBearerAuth(accessToken) } { headers: asBearerAuth(accessToken) }
); );
}, },
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum(
{ createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) }
),
createAsset: async ( createAsset: async (
accessToken: string, accessToken: string,
dto?: Omit<CreateAssetDto, 'assetData'> dto?: Omit<CreateAssetDto, 'assetData'>
@ -211,6 +220,11 @@ export const apiUtils = {
{ headers: asBearerAuth(accessToken) } { headers: asBearerAuth(accessToken) }
); );
}, },
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink(
{ sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) }
),
}; };
export const cliUtils = { export const cliUtils = {