refactor: more small tests (#26159)

This commit is contained in:
Daniel Dietzler
2026-02-12 14:34:32 +01:00
committed by GitHub
parent 913904f418
commit 7e0356e227
10 changed files with 463 additions and 782 deletions

View File

@@ -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',
}),

View File

@@ -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`] } }],
]);
});

View File

@@ -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 () => {

View File

@@ -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' } },

View File

@@ -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

View File

@@ -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,
});
});

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,