mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat: manual stack assets (#4198)
This commit is contained in:
@@ -626,4 +626,167 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||
expect(body).toEqual([expect.objectContaining({ id: asset2.id })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /asset', () => {
|
||||
beforeEach(async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: asset1.id, ids: [asset2.id, asset3.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).put('/asset');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid parent id', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: uuidStub.invalid, ids: [asset1.id] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest(['stackParentId must be a UUID']));
|
||||
});
|
||||
|
||||
it('should require access to the parent', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: asset4.id, ids: [asset1.id] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.noPermission);
|
||||
});
|
||||
|
||||
it('should add stack children', async () => {
|
||||
const parent = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01'));
|
||||
const child = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01'));
|
||||
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: parent.id, ids: [child.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, parent.id);
|
||||
expect(asset.stack).not.toBeUndefined();
|
||||
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: child.id })]));
|
||||
});
|
||||
|
||||
it('should remove stack children', async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ removeParent: true, ids: [asset2.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, asset1.id);
|
||||
expect(asset.stack).not.toBeUndefined();
|
||||
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset3.id })]));
|
||||
});
|
||||
|
||||
it('should remove all stack children', async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ removeParent: true, ids: [asset2.id, asset3.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, asset1.id);
|
||||
expect(asset.stack).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should merge stack children', async () => {
|
||||
const newParent = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01'));
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: newParent.id, ids: [asset1.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, newParent.id);
|
||||
expect(asset.stack).not.toBeUndefined();
|
||||
expect(asset.stack).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: asset1.id }),
|
||||
expect.objectContaining({ id: asset2.id }),
|
||||
expect.objectContaining({ id: asset3.id }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /asset/stack/parent', () => {
|
||||
beforeEach(async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ stackParentId: asset1.id, ids: [asset2.id, asset3.id] });
|
||||
|
||||
expect(status).toBe(204);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).put('/asset/stack/parent');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
});
|
||||
|
||||
it('should require a valid id', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.put('/asset/stack/parent')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ oldParentId: uuidStub.invalid, newParentId: uuidStub.invalid });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest());
|
||||
});
|
||||
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(server)
|
||||
.put('/asset/stack/parent')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ oldParentId: asset4.id, newParentId: asset1.id });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.noPermission);
|
||||
});
|
||||
|
||||
it('should make old parent child of new parent', async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset/stack/parent')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ oldParentId: asset1.id, newParentId: asset2.id });
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, asset2.id);
|
||||
expect(asset.stack).not.toBeUndefined();
|
||||
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset1.id })]));
|
||||
});
|
||||
|
||||
it('should make all childrens of old parent, a child of new parent', async () => {
|
||||
const { status } = await request(server)
|
||||
.put('/asset/stack/parent')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ oldParentId: asset1.id, newParentId: asset2.id });
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const asset = await api.assetApi.get(server, user1.accessToken, asset2.id);
|
||||
expect(asset.stack).not.toBeUndefined();
|
||||
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset3.id })]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
44
server/test/fixtures/asset.stub.ts
vendored
44
server/test/fixtures/asset.stub.ts
vendored
@@ -41,6 +41,7 @@ export const assetStub = {
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.uploadLibrary1,
|
||||
}),
|
||||
|
||||
noWebpPath: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -80,6 +81,7 @@ export const assetStub = {
|
||||
} as ExifEntity,
|
||||
deletedAt: null,
|
||||
}),
|
||||
|
||||
noThumbhash: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -116,6 +118,7 @@ export const assetStub = {
|
||||
sidecarPath: null,
|
||||
deletedAt: null,
|
||||
}),
|
||||
|
||||
primaryImage: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -154,7 +157,9 @@ export const assetStub = {
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5_000,
|
||||
} as ExifEntity,
|
||||
stack: [{ id: 'stack-child-asset-1' } as AssetEntity, { id: 'stack-child-asset-2' } as AssetEntity],
|
||||
}),
|
||||
|
||||
image: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -194,6 +199,7 @@ export const assetStub = {
|
||||
fileSizeInByte: 5_000,
|
||||
} as ExifEntity,
|
||||
}),
|
||||
|
||||
external: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -233,6 +239,7 @@ export const assetStub = {
|
||||
fileSizeInByte: 5_000,
|
||||
} as ExifEntity,
|
||||
}),
|
||||
|
||||
offline: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -272,6 +279,7 @@ export const assetStub = {
|
||||
} as ExifEntity,
|
||||
deletedAt: null,
|
||||
}),
|
||||
|
||||
image1: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id-1',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -311,6 +319,7 @@ export const assetStub = {
|
||||
fileSizeInByte: 5_000,
|
||||
} as ExifEntity,
|
||||
}),
|
||||
|
||||
imageFrom2015: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id-1',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
@@ -350,6 +359,7 @@ export const assetStub = {
|
||||
} as ExifEntity,
|
||||
deletedAt: null,
|
||||
}),
|
||||
|
||||
video: Object.freeze<AssetEntity>({
|
||||
id: 'asset-id',
|
||||
originalFileName: 'asset-id.ext',
|
||||
@@ -389,6 +399,7 @@ export const assetStub = {
|
||||
} as ExifEntity,
|
||||
deletedAt: null,
|
||||
}),
|
||||
|
||||
livePhotoMotionAsset: Object.freeze({
|
||||
id: 'live-photo-motion-asset',
|
||||
originalPath: fileStub.livePhotoMotion.originalPath,
|
||||
@@ -497,10 +508,41 @@ export const assetStub = {
|
||||
sidecarPath: '/original/path.ext.xmp',
|
||||
deletedAt: null,
|
||||
}),
|
||||
readOnly: Object.freeze({
|
||||
|
||||
readOnly: Object.freeze<AssetEntity>({
|
||||
id: 'read-only-asset',
|
||||
deviceAssetId: 'device-asset-id',
|
||||
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
owner: userStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.ext',
|
||||
resizePath: '/uploads/user-id/thumbs/path.ext',
|
||||
thumbhash: null,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.IMAGE,
|
||||
webpPath: null,
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: true,
|
||||
isArchived: false,
|
||||
isReadOnly: true,
|
||||
isExternal: false,
|
||||
isOffline: false,
|
||||
libraryId: 'library-id',
|
||||
library: libraryStub.uploadLibrary1,
|
||||
duration: null,
|
||||
isVisible: true,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.ext',
|
||||
faces: [],
|
||||
sidecarPath: '/original/path.ext.xmp',
|
||||
deletedAt: null,
|
||||
}),
|
||||
};
|
||||
|
||||
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
@@ -72,6 +72,7 @@ const assetResponse: AssetResponseDto = {
|
||||
isTrashed: false,
|
||||
libraryId: 'library-id',
|
||||
hasMetadata: true,
|
||||
stackCount: 0,
|
||||
};
|
||||
|
||||
const assetResponseWithoutMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user