mirror of
https://github.com/immich-app/immich.git
synced 2026-02-13 12:27:56 +03:00
refactor: more small tests (#26159)
This commit is contained in:
@@ -429,45 +429,40 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a live photo', async () => {
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden })
|
||||
.owner(authStub.user1.user)
|
||||
.build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
authStub.user1,
|
||||
{ ...createDto, livePhotoVideoId: 'live-photo-motion-asset' },
|
||||
fileStub.livePhotoStill,
|
||||
),
|
||||
sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill),
|
||||
).resolves.toEqual({
|
||||
status: AssetMediaStatus.CREATED,
|
||||
id: 'live-photo-still-asset',
|
||||
id: asset.id,
|
||||
});
|
||||
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id);
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should hide the linked motion asset', async () => {
|
||||
mocks.asset.getById.mockResolvedValueOnce({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.create.mockResolvedValueOnce(asset);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
authStub.user1,
|
||||
{ ...createDto, livePhotoVideoId: 'live-photo-motion-asset' },
|
||||
fileStub.livePhotoStill,
|
||||
),
|
||||
sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill),
|
||||
).resolves.toEqual({
|
||||
status: AssetMediaStatus.CREATED,
|
||||
id: 'live-photo-still-asset',
|
||||
id: asset.id,
|
||||
});
|
||||
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: 'live-photo-motion-asset',
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
});
|
||||
@@ -777,12 +772,13 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should fall back to the original path', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
||||
mocks.asset.getForVideo.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForVideo.mockResolvedValue(asset);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
|
||||
await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.video.originalPath,
|
||||
path: asset.originalPath,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'application/octet-stream',
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||
@@ -10,7 +9,7 @@ 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 } from 'test/small.factory';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const stats: AssetStats = {
|
||||
@@ -34,14 +33,8 @@ describe(AssetService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
const mockGetById = (assets: MapAsset[]) => {
|
||||
mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId)));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(AssetService));
|
||||
|
||||
mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
@@ -254,74 +247,79 @@ describe(AssetService.name, () => {
|
||||
|
||||
it('should fail linking a live video if the motion part could not be found', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
|
||||
await expect(
|
||||
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
sut.update(auth, asset.id, {
|
||||
livePhotoVideoId: 'unknown',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: 'unknown',
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
id: 'unknown',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
assetId: 'unknown',
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part is not a video', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
const motionAsset = AssetFactory.from().owner(auth.user).build();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(asset);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
sut.update(authStub.admin, asset.id, {
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
assetId: motionAsset.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking a live video if the motion part has a different owner', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValue(motionAsset);
|
||||
|
||||
await expect(
|
||||
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
sut.update(auth, asset.id, {
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
assetId: motionAsset.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
@@ -351,36 +349,40 @@ describe(AssetService.name, () => {
|
||||
|
||||
it('should unlink a live video', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.update.mockResolvedValueOnce(asset);
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden })
|
||||
.owner(auth.user)
|
||||
.build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
const unlinkedAsset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(asset);
|
||||
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||
mocks.asset.update.mockResolvedValueOnce(unlinkedAsset);
|
||||
|
||||
await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||
await sut.update(auth, asset.id, { livePhotoVideoId: null });
|
||||
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: null,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: assetStub.livePhotoStillAsset.visibility,
|
||||
id: motionAsset.id,
|
||||
visibility: asset.visibility,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
assetId: motionAsset.id,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail unlinking a live video if the asset could not be found', async () => {
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
mocks.asset.getById.mockResolvedValueOnce(undefined);
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getById.mockResolvedValueOnce(void 0);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.update(authStub.admin, asset.id, { livePhotoVideoId: null })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||
@@ -600,63 +602,31 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
id: asset.id,
|
||||
deleteOnDisk: true,
|
||||
});
|
||||
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.AssetDelete,
|
||||
data: {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
deleteOnDisk: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: [
|
||||
'/uploads/user-id/webp/path.ext',
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
'/uploads/user-id/fullsize/path.webp',
|
||||
'fake_path/asset_1.jpeg',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[{ name: JobName.AssetDelete, data: { id: motionAsset.id, deleteOnDisk: true } }],
|
||||
[{ name: JobName.FileDelete, data: { files: [asset.originalPath] } }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: newUuid() });
|
||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
|
||||
|
||||
await sut.handleAssetDeletion({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
deleteOnDisk: true,
|
||||
});
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.FileDelete,
|
||||
data: {
|
||||
files: [
|
||||
'/uploads/user-id/webp/path.ext',
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
'/uploads/user-id/fullsize/path.webp',
|
||||
'fake_path/asset_1.jpeg',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[{ name: JobName.FileDelete, data: { files: [`/data/library/IMG_${asset.id}.jpg`] } }],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
|
||||
@@ -151,9 +152,7 @@ describe(SearchService.name, () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
const result = await sut.handleSearchDuplicates({ id: newUuid() });
|
||||
|
||||
expect(result).toBe(JobStatus.Skipped);
|
||||
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
|
||||
@@ -168,9 +167,7 @@ describe(SearchService.name, () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
|
||||
const result = await sut.handleSearchDuplicates({ id });
|
||||
const result = await sut.handleSearchDuplicates({ id: newUuid() });
|
||||
|
||||
expect(result).toBe(JobStatus.Skipped);
|
||||
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
|
||||
@@ -197,16 +194,13 @@ describe(SearchService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if asset is not visible', async () => {
|
||||
const id = assetStub.livePhotoMotionAsset.id;
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({
|
||||
...hasEmbedding,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, ...asset });
|
||||
|
||||
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 not visible, skipping`);
|
||||
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is not visible, skipping`);
|
||||
});
|
||||
|
||||
it('should fail if asset is missing embedding', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { AssetType, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { JobItem } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(JobService.name, () => {
|
||||
@@ -56,22 +56,22 @@ describe(JobService.name, () => {
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } },
|
||||
jobs: [],
|
||||
stub: [AssetFactory.create({ id: 'asset-id' })],
|
||||
stub: [AssetFactory.create({ id: 'asset-1' })],
|
||||
},
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } },
|
||||
jobs: [],
|
||||
stub: [assetStub.video],
|
||||
stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })],
|
||||
},
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } },
|
||||
jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr],
|
||||
stub: [assetStub.livePhotoStillAsset],
|
||||
stub: [AssetFactory.create({ id: 'asset-1', livePhotoVideoId: newUuid() })],
|
||||
},
|
||||
{
|
||||
item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } },
|
||||
jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr, JobName.AssetEncodeVideo],
|
||||
stub: [assetStub.video],
|
||||
stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })],
|
||||
},
|
||||
{
|
||||
item: { name: JobName.SmartSearch, data: { id: 'asset-1' } },
|
||||
|
||||
@@ -7,11 +7,10 @@ import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { makeMockWatcher } from 'test/repositories/storage.repository.mock';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { factory, newDate, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
import { vitest } from 'vitest';
|
||||
|
||||
@@ -307,13 +306,13 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should queue asset sync', async () => {
|
||||
const library = factory.library({ importPaths: ['/foo', '/bar'] });
|
||||
const asset = AssetFactory.create({ libraryId: library.id, isExternal: true });
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.storage.walk.mockImplementation(async function* generator() {});
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([asset]));
|
||||
mocks.asset.getLibraryAssetCount.mockResolvedValue(1);
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n });
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
|
||||
|
||||
const response = await sut.handleQueueSyncAssets({ id: library.id });
|
||||
|
||||
@@ -323,7 +322,7 @@ describe(LibraryService.name, () => {
|
||||
libraryId: library.id,
|
||||
importPaths: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
assetIds: [assetStub.external.id],
|
||||
assetIds: [asset.id],
|
||||
progressCounter: 1,
|
||||
totalAssets: 1,
|
||||
},
|
||||
@@ -344,8 +343,9 @@ describe(LibraryService.name, () => {
|
||||
|
||||
describe('handleSyncAssets', () => {
|
||||
it('should offline assets no longer on disk', async () => {
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.external.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -353,20 +353,21 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
|
||||
isOffline: true,
|
||||
deletedAt: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should set assets deleted from disk as offline', async () => {
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.external.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/data/user2'],
|
||||
exclusionPatterns: [],
|
||||
@@ -374,20 +375,21 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
|
||||
isOffline: true,
|
||||
deletedAt: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing with offline assets deleted from disk', async () => {
|
||||
const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.trashedOffline.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/data/user2'],
|
||||
exclusionPatterns: [],
|
||||
@@ -395,7 +397,7 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockRejectedValue(new Error('Could not read file'));
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
@@ -404,8 +406,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should un-trash an asset previously marked as offline', async () => {
|
||||
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.trashedOffline.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/original/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -413,20 +416,21 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], {
|
||||
isOffline: false,
|
||||
deletedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing with offline asset if covered by exclusion pattern', async () => {
|
||||
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.trashedOffline.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/original/'],
|
||||
exclusionPatterns: ['**/path.jpg'],
|
||||
@@ -434,8 +438,8 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
@@ -445,8 +449,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should do nothing with offline asset if not in import path', async () => {
|
||||
const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.trashedOffline.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/import/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -454,8 +459,8 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
@@ -465,8 +470,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should do nothing with unchanged online assets', async () => {
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.external.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -474,8 +480,8 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: asset.fileModifiedAt } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
@@ -483,8 +489,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => {
|
||||
const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.trashedOffline.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -492,13 +499,13 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(
|
||||
[assetStub.trashedOffline.id],
|
||||
[asset.id],
|
||||
expect.not.objectContaining({
|
||||
fileCreatedAt: expect.anything(),
|
||||
}),
|
||||
@@ -506,8 +513,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should update with online assets that have changed', async () => {
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
const mockAssetJob: ILibraryBulkIdsJob = {
|
||||
assetIds: [assetStub.external.id],
|
||||
assetIds: [asset.id],
|
||||
libraryId: newUuid(),
|
||||
importPaths: ['/'],
|
||||
exclusionPatterns: [],
|
||||
@@ -515,13 +523,9 @@ describe(LibraryService.name, () => {
|
||||
progressCounter: 0,
|
||||
};
|
||||
|
||||
if (assetStub.external.fileModifiedAt == null) {
|
||||
throw new Error('fileModifiedAt is null');
|
||||
}
|
||||
const mtime = new Date(asset.fileModifiedAt.getDate() + 1);
|
||||
|
||||
const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1);
|
||||
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]);
|
||||
mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]);
|
||||
mocks.storage.stat.mockResolvedValue({ mtime } as Stats);
|
||||
|
||||
await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success);
|
||||
@@ -530,7 +534,7 @@ describe(LibraryService.name, () => {
|
||||
{
|
||||
name: JobName.SidecarCheck,
|
||||
data: {
|
||||
id: assetStub.external.id,
|
||||
id: asset.id,
|
||||
source: 'upload',
|
||||
},
|
||||
},
|
||||
@@ -1023,9 +1027,10 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should handle an error event', async () => {
|
||||
const library = factory.library({ importPaths: ['/foo', '/bar'] });
|
||||
const asset = AssetFactory.create({ libraryId: library.id, isExternal: true });
|
||||
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
||||
mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset);
|
||||
mocks.library.getAll.mockResolvedValue([library]);
|
||||
mocks.storage.watch.mockImplementation(
|
||||
makeMockWatcher({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import { DateTime } from 'luxon';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Stats } from 'node:fs';
|
||||
import { defaults } from 'src/config';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetType,
|
||||
@@ -18,7 +17,6 @@ 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 { fileStub } from 'test/fixtures/file.stub';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
@@ -604,14 +602,12 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should not apply motion photos if asset is video', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
@@ -632,13 +628,14 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should extract the correct video orientation', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
mockReadTags({});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
@@ -646,16 +643,14 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoWithOriginalFileName,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: null,
|
||||
});
|
||||
const asset = AssetFactory.create();
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
@@ -667,57 +662,52 @@ describe(MetadataService.name, () => {
|
||||
EmbeddedVideoType: 'MotionPhoto_Data',
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
||||
mocks.asset.create.mockResolvedValue(motionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(motionAsset.id);
|
||||
const video = randomBytes(512);
|
||||
mocks.metadata.extractBinaryTag.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||
expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(
|
||||
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||
'MotionPhotoVideo',
|
||||
);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'MotionPhotoVideo');
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'),
|
||||
ownerId: assetStub.livePhotoWithOriginalFileName.ownerId,
|
||||
libraryId: asset.libraryId,
|
||||
localDateTime: asset.fileCreatedAt,
|
||||
originalFileName: `IMG_${asset.id}.mp4`,
|
||||
originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`),
|
||||
ownerId: asset.ownerId,
|
||||
type: AssetType.Video,
|
||||
});
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({
|
||||
name: JobName.AssetEncodeVideo,
|
||||
data: { id: assetStub.livePhotoMotionAsset.id },
|
||||
data: { id: motionAsset.id },
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoWithOriginalFileName,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: null,
|
||||
});
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
EmbeddedVideoFile: new BinaryField(0, ''),
|
||||
@@ -725,56 +715,51 @@ describe(MetadataService.name, () => {
|
||||
MotionPhoto: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
||||
mocks.asset.create.mockResolvedValue(motionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(motionAsset.id);
|
||||
const video = randomBytes(512);
|
||||
mocks.metadata.extractBinaryTag.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||
expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(
|
||||
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||
'EmbeddedVideoFile',
|
||||
);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'EmbeddedVideoFile');
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'),
|
||||
ownerId: assetStub.livePhotoWithOriginalFileName.ownerId,
|
||||
libraryId: asset.libraryId,
|
||||
localDateTime: asset.fileCreatedAt,
|
||||
originalFileName: `IMG_${asset.id}.mp4`,
|
||||
originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`),
|
||||
ownerId: asset.ownerId,
|
||||
type: AssetType.Video,
|
||||
});
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({
|
||||
name: JobName.AssetEncodeVideo,
|
||||
data: { id: assetStub.livePhotoMotionAsset.id },
|
||||
data: { id: motionAsset.id },
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoWithOriginalFileName,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: null,
|
||||
});
|
||||
const asset = AssetFactory.create();
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.storage.stat.mockResolvedValue({
|
||||
size: 123_456,
|
||||
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
|
||||
mtime: asset.fileModifiedAt,
|
||||
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||
} as Stats);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
@@ -783,47 +768,46 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
||||
mocks.asset.create.mockResolvedValue(motionAsset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(motionAsset.id);
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
||||
expect(mocks.storage.readFile).toHaveBeenCalledWith(
|
||||
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||
expect.any(Object),
|
||||
);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object));
|
||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||
checksum: expect.any(Buffer),
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
|
||||
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
|
||||
originalFileName: 'asset_1.mp4',
|
||||
originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'),
|
||||
ownerId: assetStub.livePhotoWithOriginalFileName.ownerId,
|
||||
libraryId: asset.libraryId,
|
||||
localDateTime: asset.fileCreatedAt,
|
||||
originalFileName: `IMG_${asset.id}.mp4`,
|
||||
originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`),
|
||||
ownerId: asset.ownerId,
|
||||
type: AssetType.Video,
|
||||
});
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512);
|
||||
expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({
|
||||
name: JobName.AssetEncodeVideo,
|
||||
data: { id: assetStub.livePhotoMotionAsset.id },
|
||||
data: { id: motionAsset.id },
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoWithOriginalFileName);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@@ -831,21 +815,21 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockImplementation(
|
||||
(asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise<MapAsset>,
|
||||
);
|
||||
mocks.asset.create.mockResolvedValue(AssetFactory.create({ type: AssetType.Video }));
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.job.queue).toHaveBeenNthCalledWith(1, {
|
||||
name: JobName.AssetDelete,
|
||||
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true },
|
||||
data: { id: asset.livePhotoVideoId, deleteOnDisk: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@@ -853,12 +837,12 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.getByChecksum.mockResolvedValue(motionAsset);
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
mocks.storage.checkFileExists.mockResolvedValue(true);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||
// The still asset gets saved by handleMetadataExtraction, but not the video
|
||||
@@ -867,10 +851,9 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoStillAsset,
|
||||
livePhotoVideoId: null,
|
||||
});
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@@ -878,31 +861,26 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.getByChecksum.mockResolvedValue({
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
mocks.asset.getByChecksum.mockResolvedValue(motionAsset);
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should not update storage usage if motion photo is external', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoStillAsset,
|
||||
livePhotoVideoId: null,
|
||||
isExternal: true,
|
||||
});
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
|
||||
const asset = AssetFactory.create({ isExternal: true });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({
|
||||
Directory: 'foo/bar/',
|
||||
MotionPhoto: 1,
|
||||
@@ -910,11 +888,11 @@ describe(MetadataService.name, () => {
|
||||
MicroVideoOffset: 1,
|
||||
});
|
||||
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
|
||||
mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.asset.create.mockResolvedValue(motionAsset);
|
||||
const video = randomBytes(512);
|
||||
mocks.storage.readFile.mockResolvedValue(video);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1026,7 +1004,8 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should extract duration', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@@ -1035,13 +1014,13 @@ describe(MetadataService.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.video.id,
|
||||
id: asset.id,
|
||||
duration: '00:00:06.210',
|
||||
}),
|
||||
);
|
||||
@@ -1070,7 +1049,8 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should omit duration of zero', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@@ -1079,20 +1059,21 @@ describe(MetadataService.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.video.id,
|
||||
id: asset.id,
|
||||
duration: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should a handle duration of 1 week', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
format: {
|
||||
@@ -1101,13 +1082,13 @@ describe(MetadataService.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: assetStub.video.id,
|
||||
id: asset.id,
|
||||
duration: '168:00:00.000',
|
||||
}),
|
||||
);
|
||||
@@ -1148,7 +1129,8 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should ignore Duration from exif for videos', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
mocks.media.probe.mockResolvedValue({
|
||||
...probeStub.videoStreamH264,
|
||||
@@ -1158,7 +1140,7 @@ describe(MetadataService.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
|
||||
@@ -1487,17 +1469,18 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle not finding a match', async () => {
|
||||
const asset = AssetFactory.create({ type: AssetType.Video });
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
livePhotoCID: 'CID',
|
||||
ownerId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
||||
ownerId: asset.ownerId,
|
||||
otherAssetId: asset.id,
|
||||
libraryId: null,
|
||||
type: AssetType.Image,
|
||||
});
|
||||
@@ -1508,65 +1491,67 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should link photo and video', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id);
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
libraryId: null,
|
||||
livePhotoCID: 'CID',
|
||||
ownerId: assetStub.livePhotoStillAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoStillAsset.id,
|
||||
ownerId: asset.ownerId,
|
||||
otherAssetId: asset.id,
|
||||
type: AssetType.Video,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
id: asset.id,
|
||||
livePhotoVideoId: motionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
||||
expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([motionAsset.id]);
|
||||
});
|
||||
|
||||
it('should notify clients on live photo link', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoStillAsset,
|
||||
});
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video });
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
|
||||
userId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: motionAsset.ownerId,
|
||||
assetId: motionAsset.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should search by libraryId', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.livePhotoStillAsset,
|
||||
libraryId: 'library-id',
|
||||
});
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' });
|
||||
const asset = AssetFactory.create({ libraryId: 'library-id' });
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', {
|
||||
assetId: assetStub.livePhotoStillAsset.id,
|
||||
userId: assetStub.livePhotoStillAsset.ownerId,
|
||||
assetId: asset.id,
|
||||
userId: asset.ownerId,
|
||||
});
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
ownerId: 'user-id',
|
||||
otherAssetId: 'live-photo-still-asset',
|
||||
ownerId: asset.ownerId,
|
||||
otherAssetId: asset.id,
|
||||
livePhotoCID: 'CID',
|
||||
libraryId: 'library-id',
|
||||
type: 'VIDEO',
|
||||
type: AssetType.Video,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -23,10 +22,14 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe('getAllAssetsForUserFullSync', () => {
|
||||
it('should return a list of all assets owned by the user', async () => {
|
||||
mocks.asset.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]);
|
||||
const [asset1, asset2] = [
|
||||
AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(),
|
||||
AssetFactory.from().owner(authStub.user1.user).build(),
|
||||
];
|
||||
mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]);
|
||||
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
|
||||
mapAsset(assetStub.external, mapAssetOpts),
|
||||
mapAsset(assetStub.hasEncodedVideo, mapAssetOpts),
|
||||
mapAsset(asset1, mapAssetOpts),
|
||||
mapAsset(asset2, mapAssetOpts),
|
||||
]);
|
||||
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
|
||||
ownerId: authStub.user1.user.id,
|
||||
@@ -73,15 +76,16 @@ describe(SyncService.name, () => {
|
||||
|
||||
it('should return a response with changes and deletions', async () => {
|
||||
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
|
||||
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]);
|
||||
mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]);
|
||||
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
|
||||
await expect(
|
||||
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
|
||||
).resolves.toEqual({
|
||||
needsFullSync: false,
|
||||
upserted: [mapAsset(asset, mapAssetOpts)],
|
||||
deleted: [assetStub.external.id],
|
||||
deleted: [deletedAsset.id],
|
||||
});
|
||||
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -29,7 +29,7 @@ export class AssetFactory {
|
||||
static from(dto: AssetLike = {}) {
|
||||
const id = dto.id ?? newUuid();
|
||||
|
||||
const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`;
|
||||
const originalFileName = dto.originalFileName ?? (dto.type === AssetType.Video ? `MOV_${id}.mp4` : `IMG_${id}.jpg`);
|
||||
|
||||
return new AssetFactory({
|
||||
id,
|
||||
|
||||
298
server/test/fixtures/asset.stub.ts
vendored
298
server/test/fixtures/asset.stub.ts
vendored
@@ -1,10 +1,8 @@
|
||||
import { AssetFace, AssetFile, Exif } from 'src/database';
|
||||
import { Exif } from 'src/database';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { StorageAsset } from 'src/types';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
|
||||
@@ -105,300 +103,6 @@ export const assetStub = {
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
trashed: Object.freeze({
|
||||
id: 'asset-id',
|
||||
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.jpg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: false,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
faces: [],
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
exifImageHeight: 3840,
|
||||
exifImageWidth: 2160,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
status: AssetStatus.Trashed,
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
trashedOffline: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
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.jpg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
encodedVideoPath: null,
|
||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
|
||||
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||
isFavorite: false,
|
||||
duration: null,
|
||||
libraryId: 'library-id',
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
faces: [],
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
exifImageHeight: 3840,
|
||||
exifImageWidth: 2160,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: true,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
archived: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
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.jpg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
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,
|
||||
duration: null,
|
||||
isExternal: false,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
exifImageHeight: 3840,
|
||||
exifImageWidth: 2160,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
updateId: '42',
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
external: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
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: '/data/user1/photo.jpg',
|
||||
checksum: Buffer.from('path hash', 'utf8'),
|
||||
type: AssetType.Image,
|
||||
files,
|
||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||
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,
|
||||
isExternal: true,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
libraryId: 'library-id',
|
||||
sharedLinks: [],
|
||||
originalFileName: 'asset-id.jpg',
|
||||
faces: [],
|
||||
deletedAt: null,
|
||||
exifInfo: {
|
||||
fileSizeInByte: 5000,
|
||||
} as Exif,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: '42',
|
||||
stackId: null,
|
||||
stack: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
video: Object.freeze({
|
||||
id: 'asset-id',
|
||||
status: AssetStatus.Active,
|
||||
originalFileName: 'asset-id.ext',
|
||||
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',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.Video,
|
||||
files: [previewFile],
|
||||
thumbhash: 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,
|
||||
isExternal: false,
|
||||
duration: null,
|
||||
livePhotoVideo: null,
|
||||
livePhotoVideoId: null,
|
||||
sharedLinks: [],
|
||||
faces: [],
|
||||
exifInfo: {
|
||||
fileSizeInByte: 100_000,
|
||||
exifImageHeight: 2160,
|
||||
exifImageWidth: 3840,
|
||||
} as Exif,
|
||||
deletedAt: null,
|
||||
duplicateId: null,
|
||||
isOffline: false,
|
||||
updateId: '42',
|
||||
libraryId: null,
|
||||
stackId: null,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [],
|
||||
isEdited: false,
|
||||
}),
|
||||
|
||||
livePhotoMotionAsset: Object.freeze({
|
||||
status: AssetStatus.Active,
|
||||
id: fileStub.livePhotoMotion.uuid,
|
||||
originalPath: fileStub.livePhotoMotion.originalPath,
|
||||
ownerId: authStub.user1.user.id,
|
||||
type: AssetType.Video,
|
||||
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
exifInfo: {
|
||||
fileSizeInByte: 100_000,
|
||||
timeZone: `America/New_York`,
|
||||
},
|
||||
files: [],
|
||||
libraryId: null,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
isEdited: false,
|
||||
} as unknown as MapAsset & {
|
||||
faces: AssetFace[];
|
||||
files: (AssetFile & { isProgressive: boolean })[];
|
||||
exifInfo: Exif;
|
||||
edits: AssetEditActionItem[];
|
||||
}),
|
||||
|
||||
livePhotoStillAsset: Object.freeze({
|
||||
id: 'live-photo-still-asset',
|
||||
status: AssetStatus.Active,
|
||||
originalPath: fileStub.livePhotoStill.originalPath,
|
||||
ownerId: authStub.user1.user.id,
|
||||
type: AssetType.Image,
|
||||
livePhotoVideoId: 'live-photo-motion-asset',
|
||||
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
exifInfo: {
|
||||
fileSizeInByte: 25_000,
|
||||
timeZone: `America/New_York`,
|
||||
},
|
||||
files,
|
||||
faces: [] as AssetFace[],
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
isEdited: false,
|
||||
} as unknown as MapAsset & {
|
||||
faces: AssetFace[];
|
||||
files: (AssetFile & { isProgressive: boolean })[];
|
||||
edits: AssetEditActionItem[];
|
||||
}),
|
||||
|
||||
livePhotoWithOriginalFileName: Object.freeze({
|
||||
id: 'live-photo-still-asset',
|
||||
status: AssetStatus.Active,
|
||||
originalPath: fileStub.livePhotoStill.originalPath,
|
||||
originalFileName: fileStub.livePhotoStill.originalName,
|
||||
ownerId: authStub.user1.user.id,
|
||||
type: AssetType.Image,
|
||||
livePhotoVideoId: 'live-photo-motion-asset',
|
||||
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
|
||||
exifInfo: {
|
||||
fileSizeInByte: 25_000,
|
||||
timeZone: `America/New_York`,
|
||||
},
|
||||
files: [] as AssetFile[],
|
||||
libraryId: null,
|
||||
faces: [] as AssetFace[],
|
||||
visibility: AssetVisibility.Timeline,
|
||||
width: null,
|
||||
height: null,
|
||||
edits: [] as AssetEditActionItem[],
|
||||
isEdited: false,
|
||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
||||
|
||||
withLocation: Object.freeze({
|
||||
id: 'asset-with-favorite-id',
|
||||
status: AssetStatus.Active,
|
||||
|
||||
Reference in New Issue
Block a user