mirror of
https://github.com/immich-app/immich.git
synced 2026-05-21 15:16:31 -04:00
1fcc2b704b
* feat(server)!: add owned filter to albums API
BREAKING CHANGE: GET /albums with no parameters now returns all accessible albums (owned + shared-with-me) instead of only owned albums.
* document tri-state matrix
* web impl
* collapse to single method and handover branching to sql
* dedupe
* verify that owned, shared, and notShared counts are mapped independently from their respective queries
* refactor(server): add select:['id'] overload to albumRepository.getAll
Avoid fetching full album rows (with albumUsers/sharedLinks subqueries) in map.service where only album IDs are needed.
* focus relevant test filters
* fmt
* Revert "verify that owned, shared, and notShared counts are mapped independently from their respective queries"
This reverts commit 47aab458192c766de4662aada5a6841b091d2a80.
* sync sql
* Revert "document tri-state matrix"
This reverts commit a5b2355d0c.
* address review comments
* inline shared condition and return as ternary
* sync sql
* use [...albums].sort
Array.toSorted() is not supported in Chrome 109
* use isShared and isOwned nomenclature
* fix e2e tests
* add params to sql query
800 lines
29 KiB
TypeScript
800 lines
29 KiB
TypeScript
import {
|
|
addAssetsToAlbum,
|
|
AlbumResponseDto,
|
|
AlbumUserRole,
|
|
AssetMediaResponseDto,
|
|
AssetOrder,
|
|
deleteUserAdmin,
|
|
getAlbumInfo,
|
|
LoginResponseDto,
|
|
SharedLinkType,
|
|
} from '@immich/sdk';
|
|
import { createUserDto } from 'src/fixtures';
|
|
import { errorDto } from 'src/responses';
|
|
import { app, asBearerAuth, utils } from 'src/utils';
|
|
import request from 'supertest';
|
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
const user1SharedEditorUser = 'user1SharedEditorUser';
|
|
const user1SharedViewerUser = 'user1SharedViewerUser';
|
|
const user1SharedLink = 'user1SharedLink';
|
|
const user1NotShared = 'user1NotShared';
|
|
const user2SharedUser = 'user2SharedUser';
|
|
const user2SharedLink = 'user2SharedLink';
|
|
const user2NotShared = 'user2NotShared';
|
|
const user4DeletedAsset = 'user4DeletedAsset';
|
|
const user4Empty = 'user4Empty';
|
|
|
|
describe('/albums', () => {
|
|
let admin: LoginResponseDto;
|
|
let user1: LoginResponseDto;
|
|
let user1Asset1: AssetMediaResponseDto;
|
|
let user1Asset2: AssetMediaResponseDto;
|
|
let user4Asset1: AssetMediaResponseDto;
|
|
let user1Albums: AlbumResponseDto[];
|
|
let user2: LoginResponseDto;
|
|
let user2Albums: AlbumResponseDto[];
|
|
let deletedAssetAlbum: AlbumResponseDto;
|
|
let user3: LoginResponseDto; // deleted
|
|
let user4: LoginResponseDto;
|
|
|
|
beforeAll(async () => {
|
|
await utils.resetDatabase();
|
|
|
|
admin = await utils.adminSetup();
|
|
|
|
[user1, user2, user3, user4] = await Promise.all([
|
|
utils.userSetup(admin.accessToken, createUserDto.user1),
|
|
utils.userSetup(admin.accessToken, createUserDto.user2),
|
|
utils.userSetup(admin.accessToken, createUserDto.user3),
|
|
utils.userSetup(admin.accessToken, createUserDto.user4),
|
|
]);
|
|
|
|
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
|
|
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
|
utils.createAsset(user1.accessToken),
|
|
utils.createAsset(user1.accessToken),
|
|
]);
|
|
|
|
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
|
|
Promise.all([
|
|
utils.createAlbum(user1.accessToken, {
|
|
albumName: user1SharedEditorUser,
|
|
albumUsers: [
|
|
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
|
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
|
],
|
|
assetIds: [user1Asset1.id],
|
|
}),
|
|
utils.createAlbum(user1.accessToken, {
|
|
albumName: user1SharedLink,
|
|
assetIds: [user1Asset1.id],
|
|
}),
|
|
utils.createAlbum(user1.accessToken, {
|
|
albumName: user1NotShared,
|
|
assetIds: [user1Asset1.id, user1Asset2.id],
|
|
}),
|
|
utils.createAlbum(user1.accessToken, {
|
|
albumName: user1SharedViewerUser,
|
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
|
assetIds: [user1Asset1.id],
|
|
}),
|
|
]),
|
|
Promise.all([
|
|
utils.createAlbum(user2.accessToken, {
|
|
albumName: user2SharedUser,
|
|
albumUsers: [
|
|
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
|
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
|
],
|
|
}),
|
|
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
|
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
|
]),
|
|
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
|
|
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
|
|
utils.createAlbum(user3.accessToken, {
|
|
albumName: 'Deleted',
|
|
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
|
}),
|
|
]);
|
|
|
|
await Promise.all([
|
|
addAssetsToAlbum(
|
|
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
|
{ headers: asBearerAuth(user1.accessToken) },
|
|
),
|
|
addAssetsToAlbum(
|
|
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
|
|
{ headers: asBearerAuth(user4.accessToken) },
|
|
),
|
|
// add shared link to user1SharedLink album
|
|
utils.createSharedLink(user1.accessToken, {
|
|
type: SharedLinkType.Album,
|
|
albumId: user1Albums[1].id,
|
|
}),
|
|
// add shared link to user2SharedLink album
|
|
utils.createSharedLink(user2.accessToken, {
|
|
type: SharedLinkType.Album,
|
|
albumId: user2Albums[1].id,
|
|
}),
|
|
]);
|
|
|
|
[user2Albums[0]] = await Promise.all([
|
|
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
|
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
|
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
|
|
]);
|
|
});
|
|
|
|
describe('GET /albums', () => {
|
|
it("should not show other users' favorites", async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user1Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
expect(status).toEqual(200);
|
|
expect(body).toEqual({
|
|
...user1Albums[0],
|
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
|
lastModifiedAssetTimestamp: expect.any(String),
|
|
startDate: expect.any(String),
|
|
endDate: expect.any(String),
|
|
shared: true,
|
|
albumUsers: expect.any(Array),
|
|
});
|
|
});
|
|
|
|
it('should not return shared albums with a deleted owner', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isShared=true')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(4);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user1SharedLink,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedEditorUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedViewerUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user2SharedUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return the album collection including owned and shared', async () => {
|
|
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(5);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user1SharedEditorUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedViewerUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedLink,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1NotShared,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: false,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user2SharedUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return the album collection filtered by isShared', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isShared=true')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(4);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user1SharedEditorUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedViewerUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user1SharedLink,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user2SharedUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
|
]),
|
|
shared: true,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return the album collection filtered by NOT isShared', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isShared=false')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(1);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user1NotShared,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
|
]),
|
|
shared: false,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return only owned albums when filtered by isOwned=true', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isOwned=true')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(4);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ albumName: user1SharedEditorUser }),
|
|
expect.objectContaining({ albumName: user1SharedViewerUser }),
|
|
expect.objectContaining({ albumName: user1SharedLink }),
|
|
expect.objectContaining({ albumName: user1NotShared }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isOwned=false')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(1);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user2SharedUser,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
|
]),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isOwned=true&isShared=true')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(3);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ albumName: user1SharedEditorUser }),
|
|
expect.objectContaining({ albumName: user1SharedViewerUser }),
|
|
expect.objectContaining({ albumName: user1SharedLink }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums?isOwned=false&isShared=false')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(0);
|
|
});
|
|
|
|
it('should return the album collection filtered by assetId', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums?assetId=${user1Asset2.id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(2);
|
|
});
|
|
|
|
it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(5);
|
|
});
|
|
|
|
it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toHaveLength(5);
|
|
});
|
|
|
|
it('should return empty albums and albums where all assets are deleted', async () => {
|
|
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
albumName: user4DeletedAsset,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
|
]),
|
|
shared: false,
|
|
}),
|
|
expect.objectContaining({
|
|
albumName: user4Empty,
|
|
albumUsers: expect.arrayContaining([
|
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
|
]),
|
|
shared: false,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('GET /albums/:id', () => {
|
|
it('should return album info for own album', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user1Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual({
|
|
...user1Albums[0],
|
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
|
lastModifiedAssetTimestamp: expect.any(String),
|
|
startDate: expect.any(String),
|
|
endDate: expect.any(String),
|
|
albumUsers: expect.any(Array),
|
|
shared: true,
|
|
});
|
|
});
|
|
|
|
it('should return album info for shared album (editor)', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user2Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toMatchObject({ id: user2Albums[0].id });
|
|
});
|
|
|
|
it('should return album info for shared album (viewer)', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user1Albums[3].id}`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toMatchObject({ id: user1Albums[3].id });
|
|
});
|
|
|
|
it('should return album info', async () => {
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user1Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual({
|
|
...user1Albums[0],
|
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
|
assetCount: 1,
|
|
lastModifiedAssetTimestamp: expect.any(String),
|
|
endDate: expect.any(String),
|
|
startDate: expect.any(String),
|
|
albumUsers: expect.any(Array),
|
|
shared: true,
|
|
});
|
|
});
|
|
|
|
it('should not count trashed assets', async () => {
|
|
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
|
|
|
const { status, body } = await request(app)
|
|
.get(`/albums/${user2Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual(
|
|
expect.objectContaining({
|
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
|
assetCount: 1,
|
|
lastModifiedAssetTimestamp: expect.any(String),
|
|
endDate: expect.any(String),
|
|
startDate: expect.any(String),
|
|
albumUsers: expect.any(Array),
|
|
shared: true,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('GET /albums/statistics', () => {
|
|
it('should return total count of albums the user has access to', async () => {
|
|
const { status, body } = await request(app)
|
|
.get('/albums/statistics')
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual({ owned: 4, shared: 4, notShared: 1 });
|
|
});
|
|
});
|
|
|
|
describe('POST /albums', () => {
|
|
it('should create an album', async () => {
|
|
const { status, body } = await request(app)
|
|
.post('/albums')
|
|
.send({ albumName: 'New album' })
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(201);
|
|
expect(body).toEqual({
|
|
id: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
updatedAt: expect.any(String),
|
|
albumName: 'New album',
|
|
description: '',
|
|
albumThumbnailAssetId: null,
|
|
shared: false,
|
|
albumUsers: [{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) }],
|
|
hasSharedLink: false,
|
|
assetCount: 0,
|
|
isActivityEnabled: true,
|
|
order: AssetOrder.Desc,
|
|
});
|
|
});
|
|
|
|
it('should not be able to share album with owner', async () => {
|
|
const { status, body } = await request(app)
|
|
.post('/albums')
|
|
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
|
|
});
|
|
});
|
|
|
|
describe('PUT /albums/:id/assets', () => {
|
|
it('should be able to add own asset to own album', async () => {
|
|
const asset = await utils.createAsset(user1.accessToken);
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${user1Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ ids: [asset.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
|
});
|
|
|
|
it('should be able to add own asset to shared album', async () => {
|
|
const asset = await utils.createAsset(user1.accessToken);
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${user2Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ ids: [asset.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
|
});
|
|
|
|
it('should not be able to add assets to album as a viewer', async () => {
|
|
const asset = await utils.createAsset(user2.accessToken);
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${user1Albums[3].id}/assets`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ ids: [asset.id] });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.create access'));
|
|
});
|
|
|
|
it('should add duplicate assets only once', async () => {
|
|
const asset = await utils.createAsset(user1.accessToken);
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${user1Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ ids: [asset.id, asset.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([
|
|
expect.objectContaining({ id: asset.id, success: true }),
|
|
expect.objectContaining({ id: asset.id, success: false, error: 'duplicate' }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /albums/:id', () => {
|
|
it('should update an album', async () => {
|
|
const album = await utils.createAlbum(user1.accessToken, {
|
|
albumName: 'New album',
|
|
});
|
|
const { status, body } = await request(app)
|
|
.patch(`/albums/${album.id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({
|
|
albumName: 'New album name',
|
|
description: 'An album description',
|
|
});
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual({
|
|
...album,
|
|
updatedAt: expect.any(String),
|
|
albumName: 'New album name',
|
|
description: 'An album description',
|
|
});
|
|
});
|
|
|
|
it('should not be able to update as a viewer', async () => {
|
|
const { status, body } = await request(app)
|
|
.patch(`/albums/${user1Albums[3].id}`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ albumName: 'New album name' });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('Not found or no album.update access'));
|
|
});
|
|
|
|
it('should be able to update as an editor', async () => {
|
|
const { status, body } = await request(app)
|
|
.patch(`/albums/${user1Albums[0].id}`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ albumName: 'New album name' });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual(
|
|
expect.objectContaining({
|
|
id: user1Albums[0].id,
|
|
albumName: 'New album name',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /albums/:id/assets', () => {
|
|
it('should require authorization', async () => {
|
|
const { status, body } = await request(app)
|
|
.delete(`/albums/${user1Albums[1].id}/assets`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ ids: [user1Asset1.id] });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.noPermission);
|
|
});
|
|
|
|
it('should be able to remove foreign asset from owned album', async () => {
|
|
const { status, body } = await request(app)
|
|
.delete(`/albums/${user2Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ ids: [user1Asset1.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([
|
|
expect.objectContaining({
|
|
id: user1Asset1.id,
|
|
success: true,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should not be able to remove foreign asset from foreign album', async () => {
|
|
const { status, body } = await request(app)
|
|
.delete(`/albums/${user1Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ ids: [user1Asset1.id] });
|
|
|
|
expect(status).toBe(200);
|
|
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(`/albums/${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(`/albums/${user2Albums[0].id}/assets`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ ids: [user1Asset2.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([expect.objectContaining({ id: user1Asset2.id, success: true })]);
|
|
});
|
|
|
|
it('should not be able to remove assets from album as a viewer', async () => {
|
|
const { status, body } = await request(app)
|
|
.delete(`/albums/${user1Albums[3].id}/assets`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ ids: [user1Asset1.id] });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('Not found or no albumAsset.delete access'));
|
|
});
|
|
|
|
it('should remove duplicate assets only once', async () => {
|
|
const { status, body } = await request(app)
|
|
.delete(`/albums/${user1Albums[1].id}/assets`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ ids: [user1Asset1.id, user1Asset1.id] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual([
|
|
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
|
expect.objectContaining({ id: user1Asset1.id, success: false, error: 'not_found' }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('PUT :id/users', () => {
|
|
let album: AlbumResponseDto;
|
|
|
|
beforeEach(async () => {
|
|
album = await utils.createAlbum(user1.accessToken, {
|
|
albumName: 'testAlbum',
|
|
});
|
|
});
|
|
|
|
it('should be able to add user to own album', async () => {
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${album.id}/users`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
|
|
|
|
expect(status).toBe(200);
|
|
expect(body).toEqual(
|
|
expect.objectContaining({
|
|
albumUsers: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
user: expect.objectContaining({ id: user2.userId }),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not be able to share album with owner', async () => {
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${album.id}/users`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('User already added'));
|
|
});
|
|
|
|
it('should not be able to add existing user to shared album', async () => {
|
|
await request(app)
|
|
.put(`/albums/${album.id}/users`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
|
|
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${album.id}/users`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('User already added'));
|
|
});
|
|
});
|
|
|
|
describe('PUT :id/user/:userId', () => {
|
|
it('should allow the album owner to change the role of a shared user', async () => {
|
|
const album = await utils.createAlbum(user1.accessToken, {
|
|
albumName: 'testAlbum',
|
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
|
});
|
|
|
|
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
|
|
|
const { status } = await request(app)
|
|
.put(`/albums/${album.id}/user/${user2.userId}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
.send({ role: AlbumUserRole.Editor });
|
|
|
|
expect(status).toBe(204);
|
|
|
|
// Get album to verify the role change
|
|
const { body } = await request(app)
|
|
.get(`/albums/${album.id}`)
|
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
expect(body).toEqual(
|
|
expect.objectContaining({
|
|
albumUsers: [
|
|
expect.objectContaining({ role: AlbumUserRole.Owner }),
|
|
expect.objectContaining({ role: AlbumUserRole.Editor }),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not allow a shared user to change the role of another shared user', async () => {
|
|
const album = await utils.createAlbum(user1.accessToken, {
|
|
albumName: 'testAlbum',
|
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
|
});
|
|
|
|
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
|
|
|
const { status, body } = await request(app)
|
|
.put(`/albums/${album.id}/user/${user2.userId}`)
|
|
.set('Authorization', `Bearer ${user2.accessToken}`)
|
|
.send({ role: AlbumUserRole.Editor });
|
|
|
|
expect(status).toBe(400);
|
|
expect(body).toEqual(errorDto.badRequest('Not found or no album.share access'));
|
|
});
|
|
});
|
|
});
|