mirror of
https://github.com/immich-app/immich.git
synced 2026-02-28 17:49:05 +03:00
chore: remove asset stubs (#26187)
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
|
||||
describe('mapAsset', () => {
|
||||
describe('peopleWithFaces', () => {
|
||||
it('should transform all faces when a person has multiple faces in the same image', () => {
|
||||
const person = PersonFactory.create();
|
||||
const face1 = {
|
||||
...faceStub.primaryFace1,
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 200,
|
||||
@@ -18,8 +18,6 @@ describe('mapAsset', () => {
|
||||
};
|
||||
|
||||
const face2 = {
|
||||
...faceStub.primaryFace1,
|
||||
id: 'assetFaceId-second',
|
||||
boundingBoxX1: 300,
|
||||
boundingBoxY1: 400,
|
||||
boundingBoxX2: 400,
|
||||
@@ -28,16 +26,22 @@ describe('mapAsset', () => {
|
||||
imageHeight: 800,
|
||||
};
|
||||
|
||||
const asset = {
|
||||
...assetStub.withCropEdit,
|
||||
faces: [face1, face2],
|
||||
exifInfo: {
|
||||
exifImageWidth: 1000,
|
||||
exifImageHeight: 800,
|
||||
},
|
||||
};
|
||||
const asset = AssetFactory.from()
|
||||
.face(face1, (builder) => builder.person(person))
|
||||
.face(face2, (builder) => builder.person(person))
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.edit({
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: {
|
||||
width: 1512,
|
||||
height: 1152,
|
||||
x: 216,
|
||||
y: 1512,
|
||||
},
|
||||
})
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset as any);
|
||||
const result = mapAsset(asset);
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(1);
|
||||
@@ -61,32 +65,22 @@ describe('mapAsset', () => {
|
||||
});
|
||||
|
||||
it('should transform unassigned faces with edits and dimensions', () => {
|
||||
const unassignedFace = {
|
||||
...faceStub.noPerson1,
|
||||
const unassignedFace = AssetFaceFactory.create({
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 200,
|
||||
boundingBoxY2: 200,
|
||||
imageWidth: 1000,
|
||||
imageHeight: 800,
|
||||
};
|
||||
});
|
||||
|
||||
const asset = {
|
||||
...assetStub.withCropEdit,
|
||||
faces: [unassignedFace],
|
||||
exifInfo: {
|
||||
exifImageWidth: 1000,
|
||||
exifImageHeight: 800,
|
||||
},
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: { x: 50, y: 50, width: 500, height: 400 },
|
||||
},
|
||||
],
|
||||
};
|
||||
const asset = AssetFactory.from()
|
||||
.face(unassignedFace)
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset as any);
|
||||
const result = mapAsset(asset);
|
||||
|
||||
expect(result.unassignedFaces).toBeDefined();
|
||||
expect(result.unassignedFaces).toHaveLength(1);
|
||||
@@ -101,10 +95,6 @@ describe('mapAsset', () => {
|
||||
|
||||
it('should handle multiple people each with multiple faces', () => {
|
||||
const person1Face1 = {
|
||||
...faceStub.primaryFace1,
|
||||
id: 'face-1-1',
|
||||
person: personStub.withName,
|
||||
personId: personStub.withName.id,
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 200,
|
||||
@@ -114,10 +104,6 @@ describe('mapAsset', () => {
|
||||
};
|
||||
|
||||
const person1Face2 = {
|
||||
...faceStub.primaryFace1,
|
||||
id: 'face-1-2',
|
||||
person: personStub.withName,
|
||||
personId: personStub.withName.id,
|
||||
boundingBoxX1: 300,
|
||||
boundingBoxY1: 300,
|
||||
boundingBoxX2: 400,
|
||||
@@ -127,10 +113,6 @@ describe('mapAsset', () => {
|
||||
};
|
||||
|
||||
const person2Face1 = {
|
||||
...faceStub.mergeFace1,
|
||||
id: 'face-2-1',
|
||||
person: personStub.mergePerson,
|
||||
personId: personStub.mergePerson.id,
|
||||
boundingBoxX1: 500,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 600,
|
||||
@@ -139,23 +121,22 @@ describe('mapAsset', () => {
|
||||
imageHeight: 800,
|
||||
};
|
||||
|
||||
const asset = {
|
||||
...assetStub.withCropEdit,
|
||||
faces: [person1Face1, person1Face2, person2Face1],
|
||||
exifInfo: {
|
||||
exifImageWidth: 1000,
|
||||
exifImageHeight: 800,
|
||||
},
|
||||
edits: [],
|
||||
};
|
||||
const person = PersonFactory.create({ id: 'person-1' });
|
||||
|
||||
const result = mapAsset(asset as any);
|
||||
const asset = AssetFactory.from()
|
||||
.face(person1Face1, (builder) => builder.person(person))
|
||||
.face(person1Face2, (builder) => builder.person(person))
|
||||
.face(person2Face1, (builder) => builder.person({ id: 'person-2' }))
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(2);
|
||||
|
||||
const person1 = result.people!.find((p) => p.id === personStub.withName.id);
|
||||
const person2 = result.people!.find((p) => p.id === personStub.mergePerson.id);
|
||||
const person1 = result.people!.find((p) => p.id === 'person-1');
|
||||
const person2 = result.people!.find((p) => p.id === 'person-2');
|
||||
|
||||
expect(person1).toBeDefined();
|
||||
expect(person1!.faces).toHaveLength(2);
|
||||
@@ -173,10 +154,6 @@ describe('mapAsset', () => {
|
||||
|
||||
it('should combine faces of the same person into a single entry', () => {
|
||||
const face1 = {
|
||||
...faceStub.primaryFace1,
|
||||
id: 'face-1',
|
||||
person: personStub.withName,
|
||||
personId: personStub.withName.id,
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 200,
|
||||
@@ -186,10 +163,6 @@ describe('mapAsset', () => {
|
||||
};
|
||||
|
||||
const face2 = {
|
||||
...faceStub.primaryFace1,
|
||||
id: 'face-2',
|
||||
person: personStub.withName,
|
||||
personId: personStub.withName.id,
|
||||
boundingBoxX1: 300,
|
||||
boundingBoxY1: 300,
|
||||
boundingBoxX2: 400,
|
||||
@@ -198,24 +171,21 @@ describe('mapAsset', () => {
|
||||
imageHeight: 800,
|
||||
};
|
||||
|
||||
const asset = {
|
||||
...assetStub.withCropEdit,
|
||||
faces: [face1, face2],
|
||||
exifInfo: {
|
||||
exifImageWidth: 1000,
|
||||
exifImageHeight: 800,
|
||||
},
|
||||
edits: [],
|
||||
};
|
||||
const person = PersonFactory.create();
|
||||
|
||||
const result = mapAsset(asset as any);
|
||||
const asset = AssetFactory.from()
|
||||
.face(face1, (builder) => builder.person(person))
|
||||
.face(face2, (builder) => builder.person(person))
|
||||
.exif({ exifImageWidth: 1000, exifImageHeight: 800 })
|
||||
.build();
|
||||
|
||||
const result = mapAsset(asset);
|
||||
|
||||
expect(result.people).toBeDefined();
|
||||
expect(result.people).toHaveLength(1);
|
||||
|
||||
const person = result.people![0];
|
||||
expect(person.id).toBe(personStub.withName.id);
|
||||
expect(person.faces).toHaveLength(2);
|
||||
expect(result.people![0].id).toBe(person.id);
|
||||
expect(result.people![0].faces).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
@@ -515,28 +515,19 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should download edited file by default when edits exist', async () => {
|
||||
const editedAsset = {
|
||||
...assetStub.withCropEdit,
|
||||
files: [
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({
|
||||
...editedAsset,
|
||||
editedPath: '/uploads/user-id/fullsize/edited.jpg',
|
||||
});
|
||||
const editedAsset = AssetFactory.from()
|
||||
.edit()
|
||||
.files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.file({ type: AssetFileType.FullSize, isEdited: true })
|
||||
.build();
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path });
|
||||
|
||||
await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, {})).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
fileName: 'asset-id.jpg',
|
||||
path: editedAsset.files[3].path,
|
||||
fileName: editedAsset.originalFileName,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
}),
|
||||
@@ -544,28 +535,19 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should download edited file when edited=true', async () => {
|
||||
const editedAsset = {
|
||||
...assetStub.withCropEdit,
|
||||
files: [
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({
|
||||
...editedAsset,
|
||||
editedPath: '/uploads/user-id/fullsize/edited.jpg',
|
||||
});
|
||||
const editedAsset = AssetFactory.from()
|
||||
.edit()
|
||||
.files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.file({ type: AssetFileType.FullSize, isEdited: true })
|
||||
.build();
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual(
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path });
|
||||
|
||||
await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: true })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
fileName: 'asset-id.jpg',
|
||||
path: editedAsset.files[3].path,
|
||||
fileName: editedAsset.originalFileName,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
}),
|
||||
@@ -579,7 +561,9 @@ describe(AssetMediaService.name, () => {
|
||||
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([editedAsset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: fullsizeEdited.path });
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.adminSharedLink, editedAsset.id, { edited: false })).resolves.toEqual(
|
||||
await expect(
|
||||
sut.downloadOriginal(AuthFactory.from().sharedLink().build(), editedAsset.id, { edited: false }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: fullsizeEdited.path,
|
||||
fileName: editedAsset.originalFileName,
|
||||
@@ -590,25 +574,19 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should download original file when edited=false', async () => {
|
||||
const editedAsset = {
|
||||
...assetStub.withCropEdit,
|
||||
files: [
|
||||
...assetStub.withCropEdit.files,
|
||||
{
|
||||
id: 'edited-file',
|
||||
type: AssetFileType.FullSize,
|
||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||
isEdited: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||
const editedAsset = AssetFactory.from()
|
||||
.edit()
|
||||
.files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail])
|
||||
.file({ type: AssetFileType.FullSize, isEdited: true })
|
||||
.build();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id]));
|
||||
mocks.asset.getForOriginal.mockResolvedValue(editedAsset);
|
||||
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: false })).resolves.toEqual(
|
||||
await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: false })).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/original/path.jpg',
|
||||
fileName: 'asset-id.jpg',
|
||||
path: editedAsset.originalPath,
|
||||
fileName: editedAsset.originalFileName,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
}),
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -586,19 +585,19 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
||||
const asset = AssetFactory.from()
|
||||
.stack({}, (builder) => builder.asset())
|
||||
.build();
|
||||
mocks.stack.delete.mockResolvedValue();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
|
||||
...assetStub.primaryImage,
|
||||
stack: {
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.primaryImage.id,
|
||||
assets: [{ id: 'one-asset' }],
|
||||
},
|
||||
...asset,
|
||||
// TODO the specific query filters out the primary asset from `stack.assets`. This should be in a mapper eventually
|
||||
stack: { ...asset.stack!, assets: asset.stack!.assets.filter(({ id }) => id !== asset.stack!.primaryAssetId) },
|
||||
});
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId);
|
||||
});
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -184,13 +183,13 @@ describe(SearchService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if asset is part of stack', async () => {
|
||||
const id = assetStub.primaryImage.id;
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: 'stack-id' });
|
||||
const asset = AssetFactory.from().stack().build();
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: asset.stackId });
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
const result = await sut.handleSearchDuplicates({ id: asset.id });
|
||||
|
||||
expect(result).toBe(JobStatus.Skipped);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is part of a stack, skipping`);
|
||||
});
|
||||
|
||||
it('should skip if asset is not visible', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MapService } from 'src/services/map.service';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -16,36 +16,41 @@ describe(MapService.name, () => {
|
||||
|
||||
describe('getMapMarkers', () => {
|
||||
it('should get geo information of assets', async () => {
|
||||
const asset = assetStub.withLocation;
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' })
|
||||
.build();
|
||||
const marker = {
|
||||
id: asset.id,
|
||||
lat: asset.exifInfo!.latitude!,
|
||||
lon: asset.exifInfo!.longitude!,
|
||||
city: asset.exifInfo!.city,
|
||||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
lat: asset.exifInfo.latitude!,
|
||||
lon: asset.exifInfo.longitude!,
|
||||
city: asset.exifInfo.city,
|
||||
state: asset.exifInfo.state,
|
||||
country: asset.exifInfo.country,
|
||||
};
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, {});
|
||||
const markers = await sut.getMapMarkers(auth, {});
|
||||
|
||||
expect(markers).toHaveLength(1);
|
||||
expect(markers[0]).toEqual(marker);
|
||||
});
|
||||
|
||||
it('should include partner assets', async () => {
|
||||
const partner = factory.partner();
|
||||
const auth = factory.auth({ user: { id: partner.sharedWithId } });
|
||||
const auth = AuthFactory.create();
|
||||
const partner = factory.partner({ sharedWithId: auth.user.id });
|
||||
|
||||
const asset = assetStub.withLocation;
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' })
|
||||
.build();
|
||||
const marker = {
|
||||
id: asset.id,
|
||||
lat: asset.exifInfo!.latitude!,
|
||||
lon: asset.exifInfo!.longitude!,
|
||||
city: asset.exifInfo!.city,
|
||||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
lat: asset.exifInfo.latitude!,
|
||||
lon: asset.exifInfo.longitude!,
|
||||
city: asset.exifInfo.city,
|
||||
state: asset.exifInfo.state,
|
||||
country: asset.exifInfo.country,
|
||||
};
|
||||
mocks.partner.getAll.mockResolvedValue([partner]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
@@ -62,21 +67,24 @@ describe(MapService.name, () => {
|
||||
});
|
||||
|
||||
it('should include assets from shared albums', async () => {
|
||||
const asset = assetStub.withLocation;
|
||||
const auth = AuthFactory.create(userStub.user1);
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' })
|
||||
.build();
|
||||
const marker = {
|
||||
id: asset.id,
|
||||
lat: asset.exifInfo!.latitude!,
|
||||
lon: asset.exifInfo!.longitude!,
|
||||
city: asset.exifInfo!.city,
|
||||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
lat: asset.exifInfo.latitude!,
|
||||
lon: asset.exifInfo.longitude!,
|
||||
city: asset.exifInfo.city,
|
||||
state: asset.exifInfo.state,
|
||||
country: asset.exifInfo.country,
|
||||
};
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.map.getMapMarkers.mockResolvedValue([marker]);
|
||||
mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]);
|
||||
mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true });
|
||||
const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true });
|
||||
|
||||
expect(markers).toHaveLength(1);
|
||||
expect(markers[0]).toEqual(marker);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { JobCounts, RawImageInfo } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub, personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
@@ -205,7 +204,8 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue assets with edits but missing edited thumbnails', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
const asset = AssetFactory.from().edit().build();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
@@ -213,7 +213,7 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetEditThumbnailGeneration,
|
||||
data: { id: assetStub.withCropEdit.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -221,8 +221,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should not queue assets with missing edited fullsize when feature is disabled', async () => {
|
||||
const asset = AssetFactory.from().edit().build();
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
|
||||
@@ -251,7 +252,8 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue both regular and edited thumbnails for assets with edits when force is true', async () => {
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit]));
|
||||
const asset = AssetFactory.from().edit().build();
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream());
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
|
||||
@@ -259,11 +261,11 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.AssetGenerateThumbnails,
|
||||
data: { id: assetStub.withCropEdit.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
{
|
||||
name: JobName.AssetEditThumbnailGeneration,
|
||||
data: { id: assetStub.withCropEdit.id },
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1504,7 +1506,7 @@ describe(MediaService.name, () => {
|
||||
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id);
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.previewPath, {
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), {
|
||||
colorspace: Colorspace.P3,
|
||||
orientation: undefined,
|
||||
processInvalidImages: false,
|
||||
@@ -2193,7 +2195,7 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete existing transcode if current policy does not require transcoding', async () => {
|
||||
const asset = assetStub.hasEncodedVideo;
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, encodedVideoPath: '/encoded/video/path.mp4' });
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } });
|
||||
mocks.assetJob.getForVideoConversion.mockResolvedValue(asset);
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
@@ -1227,16 +1226,17 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.person.updateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply metadata face tags creating new persons', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
it('should apply metadata face tags creating new people', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true });
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ name: personStub.withName.name }),
|
||||
]);
|
||||
@@ -1244,7 +1244,7 @@ describe(MetadataService.name, () => {
|
||||
[
|
||||
{
|
||||
id: 'random-uuid',
|
||||
assetId: assetStub.primaryImage.id,
|
||||
assetId: asset.id,
|
||||
personId: 'random-uuid',
|
||||
imageHeight: 100,
|
||||
imageWidth: 1000,
|
||||
@@ -1258,7 +1258,7 @@ describe(MetadataService.name, () => {
|
||||
[],
|
||||
);
|
||||
expect(mocks.person.updateAll).toHaveBeenCalledWith([
|
||||
{ id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' },
|
||||
{ id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
@@ -1269,21 +1269,22 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should assign metadata face tags to existing persons', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
||||
mocks.person.createAll.mockResolvedValue([]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true });
|
||||
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: 'random-uuid',
|
||||
assetId: assetStub.primaryImage.id,
|
||||
assetId: asset.id,
|
||||
personId: personStub.withName.id,
|
||||
imageHeight: 100,
|
||||
imageWidth: 1000,
|
||||
@@ -1353,16 +1354,17 @@ describe(MetadataService.name, () => {
|
||||
'should transform RegionInfo geometry according to exif orientation $description',
|
||||
async ({ orientation, expected }) => {
|
||||
const { imgW, imgH, x1, x2, y1, y2 } = expected;
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }, orientation));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, {
|
||||
withHidden: true,
|
||||
});
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
||||
@@ -1372,7 +1374,7 @@ describe(MetadataService.name, () => {
|
||||
[
|
||||
{
|
||||
id: 'random-uuid',
|
||||
assetId: assetStub.primaryImage.id,
|
||||
assetId: asset.id,
|
||||
personId: 'random-uuid',
|
||||
imageWidth: imgW,
|
||||
imageHeight: imgH,
|
||||
@@ -1386,7 +1388,7 @@ describe(MetadataService.name, () => {
|
||||
[],
|
||||
);
|
||||
expect(mocks.person.updateAll).toHaveBeenCalledWith([
|
||||
{ id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' },
|
||||
{ id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
|
||||
@@ -2,7 +2,8 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SearchSuggestionType } from 'src/dtos/search.dto';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -64,16 +65,18 @@ describe(SearchService.name, () => {
|
||||
|
||||
describe('getExploreData', () => {
|
||||
it('should get assets by city and tag', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.from()
|
||||
.exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' })
|
||||
.build();
|
||||
mocks.asset.getAssetIdByCity.mockResolvedValue({
|
||||
fieldName: 'exifInfo.city',
|
||||
items: [{ value: 'test-city', data: assetStub.withLocation.id }],
|
||||
items: [{ value: 'city', data: asset.id }],
|
||||
});
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]);
|
||||
const expectedResponse = [
|
||||
{ fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] },
|
||||
];
|
||||
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]);
|
||||
const expectedResponse = [{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(asset) }] }];
|
||||
|
||||
const result = await sut.getExploreData(authStub.user1);
|
||||
const result = await sut.getExploreData(auth);
|
||||
|
||||
expect(result).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { StackService } from 'src/services/stack.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { stackStub } from 'test/fixtures/asset.stub';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { StackFactory } from 'test/factories/stack.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -20,12 +21,14 @@ describe(StackService.name, () => {
|
||||
|
||||
describe('search', () => {
|
||||
it('should search stacks', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.stack.search.mockResolvedValue([stackStub('stack-id', [asset])]);
|
||||
const stack = StackFactory.from().primaryAsset(asset).build();
|
||||
mocks.stack.search.mockResolvedValue([stack]);
|
||||
|
||||
await sut.search(authStub.admin, { primaryAssetId: asset.id });
|
||||
await sut.search(auth, { primaryAssetId: asset.id });
|
||||
expect(mocks.stack.search).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
ownerId: auth.user.id,
|
||||
primaryAssetId: asset.id,
|
||||
});
|
||||
});
|
||||
@@ -33,8 +36,10 @@ describe(StackService.name, () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('should require asset.update permissions', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
await expect(sut.create(authStub.admin, { assetIds: [primaryAsset.id, asset.id] })).rejects.toBeInstanceOf(
|
||||
|
||||
await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
@@ -43,18 +48,22 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id]));
|
||||
mocks.stack.create.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
await expect(sut.create(authStub.admin, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
|
||||
id: 'stack-id',
|
||||
mocks.stack.create.mockResolvedValue(stack);
|
||||
|
||||
await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
|
||||
id: stack.id,
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })],
|
||||
});
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
stackId: stack.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled();
|
||||
});
|
||||
@@ -78,23 +87,26 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should get stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
|
||||
await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({
|
||||
id: 'stack-id',
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
|
||||
await expect(sut.get(auth, stack.id)).resolves.toEqual({
|
||||
id: stack.id,
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })],
|
||||
});
|
||||
expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled();
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should require stack.update permissions', async () => {
|
||||
await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.stack.getById).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
@@ -104,7 +116,7 @@ describe(StackService.name, () => {
|
||||
it('should fail if stack could not be found', async () => {
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(Error);
|
||||
await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error);
|
||||
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
@@ -112,57 +124,64 @@ describe(StackService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail if the provided primary asset id is not in the stack', async () => {
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
const auth = AuthFactory.create();
|
||||
const stack = StackFactory.from().primaryAsset().asset().build();
|
||||
|
||||
await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
|
||||
await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id);
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
mocks.stack.update.mockResolvedValue(stackStub('stack-id', [primaryAsset, asset]));
|
||||
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
|
||||
|
||||
await sut.update(authStub.admin, 'stack-id', { primaryAssetId: asset.id });
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
|
||||
mocks.stack.getById.mockResolvedValue(stack);
|
||||
mocks.stack.update.mockResolvedValue(stack);
|
||||
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.stack.update).toHaveBeenCalledWith('stack-id', {
|
||||
id: 'stack-id',
|
||||
await sut.update(auth, stack.id, { primaryAssetId: asset.id });
|
||||
|
||||
expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id);
|
||||
expect(mocks.stack.update).toHaveBeenCalledWith(stack.id, {
|
||||
id: stack.id,
|
||||
primaryAssetId: asset.id,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
stackId: stack.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should require stack.delete permissions', async () => {
|
||||
await expect(sut.delete(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.delete(AuthFactory.create(), 'stack-id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.stack.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete stack', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
|
||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||
mocks.stack.delete.mockResolvedValue();
|
||||
|
||||
await sut.delete(authStub.admin, 'stack-id');
|
||||
await sut.delete(auth, 'stack-id');
|
||||
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackDelete', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Stats } from 'node:fs';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { AssetPathType, JobStatus } from 'src/enum';
|
||||
import { AssetPathType, AssetType, JobStatus } from 'src/enum';
|
||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||
import { AlbumFactory } from 'test/factories/album.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getForStorageTemplate } from 'test/mappers';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const motionAsset = assetStub.storageAsset({});
|
||||
const stillAsset = assetStub.storageAsset({ livePhotoVideoId: motionAsset.id });
|
||||
|
||||
describe(StorageTemplateService.name, () => {
|
||||
let sut: StorageTemplateService;
|
||||
let mocks: ServiceMocks;
|
||||
@@ -110,12 +108,27 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should migrate single moving picture', async () => {
|
||||
const motionAsset = AssetFactory.from({
|
||||
type: AssetType.Video,
|
||||
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const stillAsset = AssetFactory.from({
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`;
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`;
|
||||
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`;
|
||||
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
@@ -141,8 +154,8 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should use handlebar if condition for album', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||
@@ -150,7 +163,7 @@ describe(StorageTemplateService.name, () => {
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
|
||||
@@ -166,14 +179,14 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should use handlebar else condition for album', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
|
||||
|
||||
@@ -189,8 +202,8 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle album startDate', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const album = AlbumFactory.from().asset().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template =
|
||||
@@ -199,7 +212,7 @@ describe(StorageTemplateService.name, () => {
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
|
||||
mocks.album.getMetadataForIds.mockResolvedValueOnce([
|
||||
{
|
||||
@@ -225,8 +238,8 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle else condition from album startDate', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const user = userStub.user1;
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from().owner(user).exif().build();
|
||||
const config = structuredClone(defaults);
|
||||
config.storageTemplate.template =
|
||||
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
|
||||
@@ -234,7 +247,7 @@ describe(StorageTemplateService.name, () => {
|
||||
sut.onConfigInit({ newConfig: config });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
|
||||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
|
||||
|
||||
@@ -248,11 +261,18 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should migrate previously failed move from original path when it still exists', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
const asset = assetStub.storageAsset();
|
||||
const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`;
|
||||
const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
const previousFailedNewPath = `/data/library/${user.id}/2023/Feb/${asset.originalFileName}`;
|
||||
const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
|
||||
mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath));
|
||||
mocks.move.getByEntity.mockResolvedValue({
|
||||
@@ -262,7 +282,7 @@ describe(StorageTemplateService.name, () => {
|
||||
oldPath: asset.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset));
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: asset.id,
|
||||
@@ -288,9 +308,16 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif({ fileSizeInByte: 5000 })
|
||||
.build();
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
const asset = assetStub.storageAsset({ fileSizeInByte: 5000 });
|
||||
const previousFailedNewPath = `/data/library/${asset.ownerId}/2022/June/${asset.originalFileName}`;
|
||||
const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
|
||||
@@ -304,7 +331,7 @@ describe(StorageTemplateService.name, () => {
|
||||
oldPath: asset.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset));
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: asset.id,
|
||||
@@ -325,45 +352,53 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail move if copying and hash of asset and the new file do not match', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||
const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`;
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats);
|
||||
mocks.crypto.hashFile.mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset));
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: testAsset.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: testAsset.originalPath,
|
||||
oldPath: asset.originalPath,
|
||||
newPath,
|
||||
});
|
||||
|
||||
await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Success);
|
||||
await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(testAsset.id);
|
||||
expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(newPath);
|
||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||
entityId: testAsset.id,
|
||||
entityId: asset.id,
|
||||
pathType: AssetPathType.Original,
|
||||
oldPath: testAsset.originalPath,
|
||||
oldPath: asset.originalPath,
|
||||
newPath,
|
||||
});
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(testAsset.originalPath, newPath);
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith(testAsset.originalPath, newPath);
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(asset.originalPath, newPath);
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith(asset.originalPath, newPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(newPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const testAsset = assetStub.storageAsset();
|
||||
const testAsset = AssetFactory.from().exif({ fileSizeInByte: 12_345 }).build();
|
||||
|
||||
it.each`
|
||||
failedPathChecksum | failedPathSize | reason
|
||||
${testAsset.checksum} | ${500} | ${'file size'}
|
||||
${Buffer.from('bad checksum', 'utf8')} | ${testAsset.fileSizeInByte} | ${'checksum'}
|
||||
failedPathChecksum | failedPathSize | reason
|
||||
${testAsset.checksum} | ${500} | ${'file size'}
|
||||
${Buffer.from('bad checksum', 'utf8')} | ${testAsset.exifInfo.fileSizeInByte} | ${'checksum'}
|
||||
`(
|
||||
'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails',
|
||||
async ({ failedPathChecksum, failedPathSize }) => {
|
||||
@@ -381,7 +416,7 @@ describe(StorageTemplateService.name, () => {
|
||||
oldPath: testAsset.originalPath,
|
||||
newPath: previousFailedNewPath,
|
||||
});
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(testAsset));
|
||||
mocks.move.update.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: testAsset.id,
|
||||
@@ -414,12 +449,17 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle an asset with a duplicate destination', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
const oldPath = asset.originalPath;
|
||||
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
|
||||
const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
const newPath2 = newPath.replace('.jpg', '+1.jpg');
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -441,9 +481,13 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip when an asset already matches the template', async () => {
|
||||
const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg' });
|
||||
const asset = AssetFactory.from({
|
||||
originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg',
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
|
||||
await sut.handleMigration();
|
||||
@@ -456,9 +500,13 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip when an asset is probably a duplicate', async () => {
|
||||
const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg' });
|
||||
const asset = AssetFactory.from({
|
||||
originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg',
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
|
||||
await sut.handleMigration();
|
||||
@@ -471,10 +519,15 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should move an asset', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
const oldPath = asset.originalPath;
|
||||
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -492,9 +545,15 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should use the user storage label', async () => {
|
||||
const user = factory.userAdmin({ storageLabel: 'label-1' });
|
||||
const asset = assetStub.storageAsset({ ownerId: user.id });
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
const user = UserFactory.create({ storageLabel: 'label-1' });
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -508,7 +567,7 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
asset.originalPath,
|
||||
expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`),
|
||||
);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
@@ -520,10 +579,16 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => {
|
||||
const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 });
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
originalPath: '/path/to/original.jpg',
|
||||
})
|
||||
.exif({ fileSizeInByte: 5000 })
|
||||
.build();
|
||||
|
||||
const oldPath = asset.originalPath;
|
||||
const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`;
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`;
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
@@ -561,10 +626,17 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should not update the database if the move fails due to incorrect newPath filesize', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' });
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
entityId: asset.id,
|
||||
@@ -580,22 +652,29 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
|
||||
asset.originalPath,
|
||||
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`),
|
||||
);
|
||||
expect(mocks.storage.copyFile).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
|
||||
asset.originalPath,
|
||||
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`),
|
||||
);
|
||||
expect(mocks.storage.stat).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
|
||||
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`),
|
||||
);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update the database if the move fails', async () => {
|
||||
const asset = assetStub.storageAsset();
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.storage.rename.mockRejectedValue(new Error('Read only system'));
|
||||
mocks.storage.copyFile.mockRejectedValue(new Error('Read only system'));
|
||||
mocks.move.create.mockResolvedValue({
|
||||
@@ -605,25 +684,37 @@ describe(StorageTemplateService.name, () => {
|
||||
oldPath: asset.originalPath,
|
||||
newPath: '',
|
||||
});
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
|
||||
await sut.handleMigration();
|
||||
|
||||
expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled();
|
||||
expect(mocks.storage.rename).toHaveBeenCalledWith(
|
||||
'/original/path.jpg',
|
||||
expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`),
|
||||
asset.originalPath,
|
||||
expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`),
|
||||
);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should migrate live photo motion video alongside the still image', async () => {
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`;
|
||||
const motionAsset = AssetFactory.from({
|
||||
type: AssetType.Video,
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const stillAsset = AssetFactory.from({
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
})
|
||||
.exif()
|
||||
.build();
|
||||
const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`;
|
||||
const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`;
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([stillAsset]));
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
|
||||
mocks.user.getList.mockResolvedValue([userStub.user1]);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset);
|
||||
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
|
||||
|
||||
mocks.move.create.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
@@ -653,13 +744,17 @@ describe(StorageTemplateService.name, () => {
|
||||
|
||||
describe('file rename correctness', () => {
|
||||
it('should not create double extensions when filename has lower extension', async () => {
|
||||
const user = factory.userAdmin({ storageLabel: 'label-1' });
|
||||
const asset = assetStub.storageAsset({
|
||||
ownerId: user.id,
|
||||
const user = UserFactory.create({ storageLabel: 'label-1' });
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
|
||||
originalFileName: 'IMG_7065.HEIC',
|
||||
});
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -679,13 +774,17 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should not create double extensions when filename has uppercase extension', async () => {
|
||||
const user = factory.userAdmin();
|
||||
const asset = assetStub.storageAsset({
|
||||
ownerId: user.id,
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
|
||||
originalFileName: 'IMG_7065.HEIC',
|
||||
});
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
})
|
||||
.owner(user)
|
||||
.exif({ fileSizeInByte: 12_345 })
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -705,13 +804,17 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should normalize the filename to lowercase (JPEG > jpg)', async () => {
|
||||
const user = factory.userAdmin();
|
||||
const asset = assetStub.storageAsset({
|
||||
ownerId: user.id,
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
|
||||
originalFileName: 'IMG_7065.JPEG',
|
||||
});
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
@@ -731,13 +834,17 @@ describe(StorageTemplateService.name, () => {
|
||||
});
|
||||
|
||||
it('should normalize the filename to lowercase (JPG > jpg)', async () => {
|
||||
const user = factory.userAdmin();
|
||||
const asset = assetStub.storageAsset({
|
||||
ownerId: user.id,
|
||||
const user = UserFactory.create();
|
||||
const asset = AssetFactory.from({
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
originalPath: '/data/library/user-id/2022/2022-06-19/IMG_7065.JPG',
|
||||
originalFileName: 'IMG_7065.JPG',
|
||||
});
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset]));
|
||||
})
|
||||
.owner(user)
|
||||
.exif()
|
||||
.build();
|
||||
|
||||
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)]));
|
||||
mocks.user.getList.mockResolvedValue([user]);
|
||||
mocks.move.create.mockResolvedValue({
|
||||
id: '123',
|
||||
|
||||
Reference in New Issue
Block a user