mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor: tests (#25987)
This commit is contained in:
parent
b3820c259e
commit
e3e243fa2b
14 changed files with 415 additions and 765 deletions
|
|
@ -1,15 +1,18 @@
|
||||||
import { mapAlbum } from 'src/dtos/album.dto';
|
import { mapAlbum } from 'src/dtos/album.dto';
|
||||||
import { albumStub } from 'test/fixtures/album.stub';
|
import { AlbumFactory } from 'test/factories/album.factory';
|
||||||
|
|
||||||
describe('mapAlbum', () => {
|
describe('mapAlbum', () => {
|
||||||
it('should set start and end dates', () => {
|
it('should set start and end dates', () => {
|
||||||
const dto = mapAlbum(albumStub.twoAssets, false);
|
const startDate = new Date('2023-02-22T05:06:29.716Z');
|
||||||
expect(dto.startDate).toEqual(new Date('2020-12-31T23:59:00.000Z'));
|
const endDate = new Date('2025-01-01T01:02:03.456Z');
|
||||||
expect(dto.endDate).toEqual(new Date('2025-01-01T01:02:03.456Z'));
|
const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build();
|
||||||
|
const dto = mapAlbum(album, false);
|
||||||
|
expect(dto.startDate).toEqual(startDate);
|
||||||
|
expect(dto.endDate).toEqual(endDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set start and end dates for empty assets', () => {
|
it('should not set start and end dates for empty assets', () => {
|
||||||
const dto = mapAlbum(albumStub.empty, false);
|
const dto = mapAlbum(AlbumFactory.create(), false);
|
||||||
expect(dto.startDate).toBeUndefined();
|
expect(dto.startDate).toBeUndefined();
|
||||||
expect(dto.endDate).toBeUndefined();
|
expect(dto.endDate).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||||
import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||||
import { AssetStats } from 'src/repositories/asset.repository';
|
import { AssetStats } from 'src/repositories/asset.repository';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
|
import { AuthFactory } from 'test/factories/auth.factory';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
|
|
@ -45,35 +46,33 @@ describe(AssetService.name, () => {
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Timeline })).resolves.toEqual(
|
await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Timeline })).resolves.toEqual(statResponse);
|
||||||
statResponse,
|
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { visibility: AssetVisibility.Timeline });
|
||||||
);
|
|
||||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the statistics for a user for archived assets', async () => {
|
it('should get the statistics for a user for archived assets', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||||
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Archive })).resolves.toEqual(
|
await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Archive })).resolves.toEqual(statResponse);
|
||||||
statResponse,
|
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, {
|
||||||
);
|
|
||||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
|
|
||||||
visibility: AssetVisibility.Archive,
|
visibility: AssetVisibility.Archive,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the statistics for a user for favorite assets', async () => {
|
it('should get the statistics for a user for favorite assets', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||||
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
|
await expect(sut.getStatistics(auth, { isFavorite: true })).resolves.toEqual(statResponse);
|
||||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true });
|
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { isFavorite: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the statistics for a user for all assets', async () => {
|
it('should get the statistics for a user for all assets', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.asset.getStatistics.mockResolvedValue(stats);
|
mocks.asset.getStatistics.mockResolvedValue(stats);
|
||||||
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
|
await expect(sut.getStatistics(auth, {})).resolves.toEqual(statResponse);
|
||||||
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -249,10 +248,11 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail linking a live video if the motion part could not be found', async () => {
|
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]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||||
}),
|
}),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
@ -267,11 +267,12 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||||
assetId: assetStub.livePhotoMotionAsset.id,
|
assetId: assetStub.livePhotoMotionAsset.id,
|
||||||
userId: userStub.admin.id,
|
userId: auth.user.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail linking a live video if the motion part is not a video', async () => {
|
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.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||||
|
|
||||||
|
|
@ -291,16 +292,17 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||||
assetId: assetStub.livePhotoMotionAsset.id,
|
assetId: assetStub.livePhotoMotionAsset.id,
|
||||||
userId: userStub.admin.id,
|
userId: auth.user.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail linking a live video if the motion part has a different owner', async () => {
|
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.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
sut.update(auth, assetStub.livePhotoStillAsset.id, {
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||||
}),
|
}),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
@ -315,52 +317,41 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||||
assetId: assetStub.livePhotoMotionAsset.id,
|
assetId: assetStub.livePhotoMotionAsset.id,
|
||||||
userId: userStub.admin.id,
|
userId: auth.user.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should link a live video', async () => {
|
it('should link a live video', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
|
||||||
mocks.asset.getById.mockResolvedValueOnce({
|
const stillAsset = AssetFactory.create();
|
||||||
...assetStub.livePhotoMotionAsset,
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
|
||||||
ownerId: authStub.admin.user.id,
|
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
|
||||||
visibility: AssetVisibility.Timeline,
|
mocks.asset.getById.mockResolvedValueOnce(stillAsset);
|
||||||
});
|
mocks.asset.update.mockResolvedValue(stillAsset);
|
||||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
|
const auth = AuthFactory.from(motionAsset.owner).build();
|
||||||
mocks.asset.update.mockResolvedValue(assetStub.image);
|
|
||||||
|
|
||||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
|
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, visibility: AssetVisibility.Hidden });
|
||||||
id: assetStub.livePhotoMotionAsset.id,
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { assetId: motionAsset.id, userId: auth.user.id });
|
||||||
visibility: AssetVisibility.Hidden,
|
expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, livePhotoVideoId: motionAsset.id });
|
||||||
});
|
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
|
|
||||||
assetId: assetStub.livePhotoMotionAsset.id,
|
|
||||||
userId: userStub.admin.id,
|
|
||||||
});
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if asset could not be found after update', async () => {
|
it('should throw an error if asset could not be found after update', async () => {
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
|
await expect(sut.update(AuthFactory.create(), 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unlink a live video', async () => {
|
it('should unlink a live video', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
|
||||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||||
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||||
mocks.asset.update.mockResolvedValueOnce(assetStub.image);
|
mocks.asset.update.mockResolvedValueOnce(assetStub.image);
|
||||||
|
|
||||||
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
|
|
@ -372,7 +363,7 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
|
||||||
assetId: assetStub.livePhotoMotionAsset.id,
|
assetId: assetStub.livePhotoMotionAsset.id,
|
||||||
userId: userStub.admin.id,
|
userId: auth.user.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -392,17 +383,15 @@ describe(AssetService.name, () => {
|
||||||
|
|
||||||
describe('updateAll', () => {
|
describe('updateAll', () => {
|
||||||
it('should require asset write access for all ids', async () => {
|
it('should require asset write access for all ids', async () => {
|
||||||
await expect(
|
const auth = AuthFactory.create();
|
||||||
sut.updateAll(authStub.admin, {
|
await expect(sut.updateAll(auth, { ids: ['asset-1'] })).rejects.toBeInstanceOf(BadRequestException);
|
||||||
ids: ['asset-1'],
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update all assets', async () => {
|
it('should update all assets', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||||
|
|
||||||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive });
|
await sut.updateAll(auth, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive });
|
||||||
|
|
||||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
|
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
|
||||||
visibility: AssetVisibility.Archive,
|
visibility: AssetVisibility.Archive,
|
||||||
|
|
@ -410,9 +399,10 @@ describe(AssetService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update Assets table if no relevant fields are provided', async () => {
|
it('should not update Assets table if no relevant fields are provided', async () => {
|
||||||
|
const auth = AuthFactory.create();
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
|
|
||||||
await sut.updateAll(authStub.admin, {
|
await sut.updateAll(auth, {
|
||||||
ids: ['asset-1'],
|
ids: ['asset-1'],
|
||||||
latitude: 0,
|
latitude: 0,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { DownloadResponseDto } from 'src/dtos/download.dto';
|
import { DownloadResponseDto } from 'src/dtos/download.dto';
|
||||||
import { DownloadService } from 'src/services/download.service';
|
import { DownloadService } from 'src/services/download.service';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
@ -60,22 +61,22 @@ describe(DownloadService.name, () => {
|
||||||
stream: new Readable(),
|
stream: new Readable(),
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
const asset1 = AssetFactory.create();
|
||||||
|
const asset2 = AssetFactory.create();
|
||||||
|
|
||||||
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||||
mocks.storage.realpath.mockRejectedValue(new Error('Could not read file'));
|
mocks.storage.realpath.mockRejectedValue(new Error('Could not read file'));
|
||||||
mocks.asset.getByIds.mockResolvedValue([
|
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
|
||||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
|
||||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
|
||||||
]);
|
|
||||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||||
|
|
||||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
|
||||||
stream: archiveMock.stream,
|
stream: archiveMock.stream,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.logger.warn).toHaveBeenCalledTimes(2);
|
expect(mocks.logger.warn).toHaveBeenCalledTimes(2);
|
||||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg');
|
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName);
|
||||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg');
|
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should download an archive', async () => {
|
it('should download an archive', async () => {
|
||||||
|
|
@ -85,20 +86,20 @@ describe(DownloadService.name, () => {
|
||||||
stream: new Readable(),
|
stream: new Readable(),
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
const asset1 = AssetFactory.create();
|
||||||
mocks.asset.getByIds.mockResolvedValue([
|
const asset2 = AssetFactory.create();
|
||||||
{ ...assetStub.noResizePath, id: 'asset-1' },
|
|
||||||
{ ...assetStub.noWebpPath, id: 'asset-2' },
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||||
]);
|
mocks.asset.getByIds.mockResolvedValue([asset1, asset2]);
|
||||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||||
|
|
||||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({
|
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
|
||||||
stream: archiveMock.stream,
|
stream: archiveMock.stream,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg');
|
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName);
|
||||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg');
|
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle duplicate file names', async () => {
|
it('should handle duplicate file names', async () => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { MediaService } from 'src/services/media.service';
|
import { MediaService } from 'src/services/media.service';
|
||||||
import { JobCounts, RawImageInfo } from 'src/types';
|
import { JobCounts, RawImageInfo } from 'src/types';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { assetStub, previewFile } from 'test/fixtures/asset.stub';
|
import { assetStub, previewFile } from 'test/fixtures/asset.stub';
|
||||||
import { faceStub } from 'test/fixtures/face.stub';
|
import { faceStub } from 'test/fixtures/face.stub';
|
||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
|
|
@ -139,33 +140,30 @@ describe(MediaService.name, () => {
|
||||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue all assets with missing webp path', async () => {
|
it('should queue all assets with missing preview', async () => {
|
||||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noWebpPath]));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||||
mocks.person.getAll.mockReturnValue(makeStream());
|
mocks.person.getAll.mockReturnValue(makeStream());
|
||||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||||
|
|
||||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } },
|
||||||
name: JobName.AssetGenerateThumbnails,
|
|
||||||
data: { id: assetStub.image.id },
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue all assets with missing thumbhash', async () => {
|
it('should queue all assets with missing thumbhash', async () => {
|
||||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noThumbhash]));
|
const asset = AssetFactory.from({ thumbhash: null })
|
||||||
|
.files([AssetFileType.Thumbnail, AssetFileType.Preview])
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||||
mocks.person.getAll.mockReturnValue(makeStream());
|
mocks.person.getAll.mockReturnValue(makeStream());
|
||||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||||
|
|
||||||
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false });
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } },
|
||||||
name: JobName.AssetGenerateThumbnails,
|
|
||||||
data: { id: assetStub.image.id },
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
|
||||||
|
|
@ -1052,12 +1050,19 @@ describe(MediaService.name, () => {
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||||
|
.exif({
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
profileDescription: 'Adobe RGB',
|
||||||
|
bitsPerSample: 14,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
});
|
});
|
||||||
|
|
@ -1107,12 +1112,19 @@ describe(MediaService.name, () => {
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.media.copyTagGroup.mockResolvedValue(true);
|
mocks.media.copyTagGroup.mockResolvedValue(true);
|
||||||
|
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif);
|
const asset = AssetFactory.from({ originalFileName: 'panorama.tif' })
|
||||||
|
.exif({
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
projectionType: 'EQUIRECTANGULAR',
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.panoramaTif.originalPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||||
colorspace: Colorspace.Srgb,
|
colorspace: Colorspace.Srgb,
|
||||||
orientation: undefined,
|
orientation: undefined,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
|
|
@ -1135,11 +1147,7 @@ describe(MediaService.name, () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
|
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith(
|
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith('XMP-GPano', asset.originalPath, expect.any(String));
|
||||||
'XMP-GPano',
|
|
||||||
assetStub.panoramaTif.originalPath,
|
|
||||||
expect.any(String),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect encoding options when generating full-size preview', async () => {
|
it('should respect encoding options when generating full-size preview', async () => {
|
||||||
|
|
@ -1149,12 +1157,19 @@ describe(MediaService.name, () => {
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||||
|
.exif({
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
profileDescription: 'Adobe RGB',
|
||||||
|
bitsPerSample: 14,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, {
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
});
|
});
|
||||||
|
|
@ -1181,9 +1196,16 @@ describe(MediaService.name, () => {
|
||||||
});
|
});
|
||||||
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
const asset = AssetFactory.from({ originalFileName: 'image.hif' })
|
||||||
|
.exif({
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
profileDescription: 'Adobe RGB',
|
||||||
|
bitsPerSample: 14,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
|
|
@ -1263,30 +1285,25 @@ describe(MediaService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clean up edited files if an asset has no edits', async () => {
|
it('should clean up edited files if an asset has no edits', async () => {
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
const asset = AssetFactory.from({ thumbhash: factory.buffer() })
|
||||||
...assetStub.withoutEdits,
|
.exif()
|
||||||
});
|
.files([
|
||||||
|
{ type: AssetFileType.Preview, path: 'edited1.jpg', isEdited: true },
|
||||||
|
{ type: AssetFileType.Thumbnail, path: 'edited2.jpg', isEdited: true },
|
||||||
|
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
|
|
||||||
|
const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
|
||||||
|
|
||||||
const status = await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
|
||||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.FileDelete,
|
name: JobName.FileDelete,
|
||||||
data: {
|
data: {
|
||||||
files: expect.arrayContaining([
|
files: expect.arrayContaining(['edited1.jpg', 'edited2.jpg', 'edited3.jpg']),
|
||||||
'/uploads/user-id/fullsize/path_edited.jpg',
|
|
||||||
'/uploads/user-id/preview/path_edited.jpg',
|
|
||||||
'/uploads/user-id/thumbnail/path_edited.jpg',
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({ path: '/uploads/user-id/preview/path_edited.jpg' }),
|
|
||||||
expect.objectContaining({ path: '/uploads/user-id/thumbnail/path_edited.jpg' }),
|
|
||||||
expect.objectContaining({ path: '/uploads/user-id/fullsize/path_edited.jpg' }),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(JobStatus.Success);
|
expect(status).toBe(JobStatus.Success);
|
||||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||||
|
|
@ -1320,11 +1337,9 @@ describe(MediaService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate the original thumbhash if no edits exist', async () => {
|
it('should generate the original thumbhash if no edits exist', async () => {
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
const asset = AssetFactory.from().exif().build();
|
||||||
...assetStub.withoutEdits,
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
|
||||||
});
|
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
|
||||||
|
|
||||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' });
|
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' });
|
||||||
|
|
||||||
|
|
@ -1335,18 +1350,14 @@ describe(MediaService.name, () => {
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({
|
||||||
...assetStub.withCropEdit,
|
...assetStub.withCropEdit,
|
||||||
});
|
});
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = factory.buffer();
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
mocks.person.getFaces.mockResolvedValue([]);
|
mocks.person.getFaces.mockResolvedValue([]);
|
||||||
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
mocks.ocr.getByAssetId.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ thumbhash: thumbhashBuffer }));
|
||||||
expect.objectContaining({
|
|
||||||
thumbhash: thumbhashBuffer,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ export class MediaService extends BaseService {
|
||||||
|
|
||||||
const generated = await this.generateEditedThumbnails(asset, config);
|
const generated = await this.generateEditedThumbnails(asset, config);
|
||||||
await this.syncFiles(
|
await this.syncFiles(
|
||||||
asset.files.filter((asset) => asset.isEdited),
|
asset.files.filter((file) => file.isEdited),
|
||||||
generated?.files ?? [],
|
generated?.files ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||||
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
|
|
@ -24,13 +25,6 @@ import { tagStub } from 'test/fixtures/tag.stub';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
const removeNonSidecarFiles = (asset: any) => {
|
|
||||||
return {
|
|
||||||
...asset,
|
|
||||||
files: asset.files.filter((file: any) => file.type === AssetFileType.Sidecar),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const forSidecarJob = (
|
const forSidecarJob = (
|
||||||
asset: {
|
asset: {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -182,17 +176,18 @@ describe(MetadataService.name, () => {
|
||||||
it('should handle a date in a sidecar file', async () => {
|
it('should handle a date in a sidecar file', async () => {
|
||||||
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
||||||
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar));
|
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.sidecar.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }), {
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }), {
|
||||||
lockedPropertiesBehavior: 'skip',
|
lockedPropertiesBehavior: 'skip',
|
||||||
});
|
});
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: assetStub.image.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: sidecarDate,
|
fileCreatedAt: sidecarDate,
|
||||||
localDateTime: sidecarDate,
|
localDateTime: sidecarDate,
|
||||||
|
|
@ -203,7 +198,8 @@ describe(MetadataService.name, () => {
|
||||||
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
||||||
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: fileModifiedAt,
|
mtime: fileModifiedAt,
|
||||||
|
|
@ -212,14 +208,14 @@ describe(MetadataService.name, () => {
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mockReadTags();
|
mockReadTags();
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ dateTimeOriginal: fileModifiedAt }),
|
expect.objectContaining({ dateTimeOriginal: fileModifiedAt }),
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
);
|
);
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: fileModifiedAt,
|
fileCreatedAt: fileModifiedAt,
|
||||||
fileModifiedAt,
|
fileModifiedAt,
|
||||||
|
|
@ -232,7 +228,8 @@ describe(MetadataService.name, () => {
|
||||||
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
||||||
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
||||||
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: fileModifiedAt,
|
mtime: fileModifiedAt,
|
||||||
|
|
@ -241,14 +238,14 @@ describe(MetadataService.name, () => {
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mockReadTags();
|
mockReadTags();
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ dateTimeOriginal: fileCreatedAt }),
|
expect.objectContaining({ dateTimeOriginal: fileCreatedAt }),
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
);
|
);
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt,
|
fileCreatedAt,
|
||||||
fileModifiedAt,
|
fileModifiedAt,
|
||||||
|
|
@ -260,10 +257,11 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
|
||||||
process.env.TZ = 'America/Los_Angeles';
|
process.env.TZ = 'America/Los_Angeles';
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
dateTimeOriginal: new Date('2022-01-01T00:00:00.000Z'),
|
dateTimeOriginal: new Date('2022-01-01T00:00:00.000Z'),
|
||||||
|
|
@ -279,16 +277,15 @@ describe(MetadataService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle lists of numbers', async () => {
|
it('should handle lists of numbers', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: assetStub.image.fileModifiedAt,
|
mtime: asset.fileModifiedAt,
|
||||||
mtimeMs: assetStub.image.fileModifiedAt.valueOf(),
|
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||||
birthtimeMs: assetStub.image.fileCreatedAt.valueOf(),
|
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mockReadTags({
|
mockReadTags({ ISO: [160] });
|
||||||
ISO: [160],
|
|
||||||
});
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
||||||
|
|
@ -296,11 +293,11 @@ describe(MetadataService.name, () => {
|
||||||
lockedPropertiesBehavior: 'skip',
|
lockedPropertiesBehavior: 'skip',
|
||||||
});
|
});
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.image.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.image.fileCreatedAt,
|
fileModifiedAt: asset.fileCreatedAt,
|
||||||
localDateTime: assetStub.image.fileCreatedAt,
|
localDateTime: asset.fileCreatedAt,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
});
|
});
|
||||||
|
|
@ -308,77 +305,77 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
it('should not delete latituide and longitude without reverse geocode', async () => {
|
it('should not delete latituide and longitude without reverse geocode', async () => {
|
||||||
// regression test for issue 17511
|
// regression test for issue 17511
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation);
|
const asset = AssetFactory.from().exif().build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: assetStub.withLocation.fileModifiedAt,
|
mtime: asset.fileModifiedAt,
|
||||||
mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(),
|
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||||
birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(),
|
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
GPSLatitude: asset.exifInfo.latitude!,
|
||||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
GPSLongitude: asset.exifInfo.longitude!,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ city: null, state: null, country: null }),
|
expect.objectContaining({ city: null, state: null, country: null }),
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
);
|
);
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.withLocation.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
localDateTime: asset.localDateTime,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply reverse geocoding', async () => {
|
it('should apply reverse geocoding', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation));
|
const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||||
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||||
mocks.storage.stat.mockResolvedValue({
|
mocks.storage.stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: assetStub.withLocation.fileModifiedAt,
|
mtime: asset.fileModifiedAt,
|
||||||
mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(),
|
mtimeMs: asset.fileModifiedAt.valueOf(),
|
||||||
birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(),
|
birthtimeMs: asset.fileCreatedAt.valueOf(),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
mockReadTags({
|
mockReadTags({ GPSLatitude: 10, GPSLongitude: 20 });
|
||||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
|
||||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
);
|
);
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.withLocation.id,
|
id: asset.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
localDateTime: asset.localDateTime,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard latitude and longitude on null island', async () => {
|
it('should discard latitude and longitude on null island', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
mockReadTags({
|
mockReadTags({
|
||||||
GPSLatitude: 0,
|
GPSLatitude: 0,
|
||||||
GPSLongitude: 0,
|
GPSLongitude: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ latitude: null, longitude: null }),
|
expect.objectContaining({ latitude: null, longitude: null }),
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
|
|
@ -386,19 +383,25 @@ describe(MetadataService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from TagsList', async () => {
|
it('should extract tags from TagsList', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
.exif({ tags: ['Parent'] })
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ TagsList: ['Parent'] });
|
mockReadTags({ TagsList: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchy from TagsList', async () => {
|
it('should extract hierarchy from TagsList', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
.exif({ tags: ['Parent/Child'] })
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ TagsList: ['Parent/Child'] });
|
mockReadTags({ TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||||
|
|
@ -406,135 +409,147 @@ describe(MetadataService.name, () => {
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent',
|
value: 'Parent',
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
});
|
});
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent/Child',
|
value: 'Parent/Child',
|
||||||
parentId: 'tag-parent',
|
parentId: 'tag-parent',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a string', async () => {
|
it('should extract tags from Keywords as a string', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
.exif({ tags: ['Parent'] })
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ Keywords: 'Parent' });
|
mockReadTags({ Keywords: 'Parent' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a list', async () => {
|
it('should extract tags from Keywords as a list', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) });
|
.exif({ tags: ['Parent'] })
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ Keywords: ['Parent'] });
|
mockReadTags({ Keywords: ['Parent'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from Keywords as a list with a number', async () => {
|
it('should extract tags from Keywords as a list with a number', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({
|
.exif({ tags: ['Parent', '2024'] })
|
||||||
...factory.asset(),
|
.build();
|
||||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
});
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ Keywords: ['Parent', 2024] });
|
mockReadTags({ Keywords: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchal tags from Keywords', async () => {
|
it('should extract hierarchal tags from Keywords', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) });
|
.exif({ tags: ['Parent/Child'] })
|
||||||
|
.build();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ Keywords: 'Parent/Child' });
|
mockReadTags({ Keywords: 'Parent/Child' });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent',
|
value: 'Parent',
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
});
|
});
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent/Child',
|
value: 'Parent/Child',
|
||||||
parentId: 'tag-parent',
|
parentId: 'tag-parent',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore Keywords when TagsList is present', async () => {
|
it('should ignore Keywords when TagsList is present', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({
|
.exif({ tags: ['Parent/Child', 'Child'] })
|
||||||
...factory.asset(),
|
.build();
|
||||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'Child'] }),
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
});
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent',
|
value: 'Parent',
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
});
|
});
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent/Child',
|
value: 'Parent/Child',
|
||||||
parentId: 'tag-parent',
|
parentId: 'tag-parent',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract hierarchy from HierarchicalSubject', async () => {
|
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({
|
.exif({ tags: ['Parent/Child', 'TagA'] })
|
||||||
...factory.asset(),
|
.build();
|
||||||
exifInfo: factory.exif({ tags: ['Parent/Child', 'TagA'] }),
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
});
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
||||||
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent',
|
value: 'Parent',
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
});
|
});
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
userId: 'user-id',
|
userId: asset.ownerId,
|
||||||
value: 'Parent/Child',
|
value: 'Parent/Child',
|
||||||
parentId: 'tag-parent',
|
parentId: 'tag-parent',
|
||||||
});
|
});
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, {
|
||||||
|
userId: asset.ownerId,
|
||||||
|
value: 'TagA',
|
||||||
|
parent: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image));
|
const asset = AssetFactory.from()
|
||||||
mocks.asset.getById.mockResolvedValue({
|
.exif({ tags: ['Parent', '2024'] })
|
||||||
...factory.asset(),
|
.build();
|
||||||
exifInfo: factory.exif({ tags: ['Parent', '2024'] }),
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
});
|
mocks.asset.getById.mockResolvedValue(asset);
|
||||||
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
||||||
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined });
|
||||||
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined });
|
expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||||
|
|
@ -1646,31 +1661,23 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
describe('handleQueueSidecar', () => {
|
describe('handleQueueSidecar', () => {
|
||||||
it('should queue assets with sidecar files', async () => {
|
it('should queue assets with sidecar files', async () => {
|
||||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image]));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset]));
|
||||||
|
|
||||||
await sut.handleQueueSidecar({ force: true });
|
await sut.handleQueueSidecar({ force: true });
|
||||||
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true);
|
|
||||||
|
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true);
|
||||||
{
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]);
|
||||||
name: JobName.SidecarCheck,
|
|
||||||
data: { id: assetStub.sidecar.id },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue assets without sidecar files', async () => {
|
it('should queue assets without sidecar files', async () => {
|
||||||
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image]));
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset]));
|
||||||
|
|
||||||
await sut.handleQueueSidecar({ force: false });
|
await sut.handleQueueSidecar({ force: false });
|
||||||
|
|
||||||
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false);
|
expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false);
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]);
|
||||||
{
|
|
||||||
name: JobName.SidecarCheck,
|
|
||||||
data: { id: assetStub.image.id },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||||
import { StackService } from 'src/services/stack.service';
|
import { StackService } from 'src/services/stack.service';
|
||||||
import { assetStub, stackStub } from 'test/fixtures/asset.stub';
|
import { assetStub, stackStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { newUuid } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
describe(StackService.name, () => {
|
describe(StackService.name, () => {
|
||||||
|
|
@ -204,9 +205,9 @@ describe(StackService.name, () => {
|
||||||
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
|
||||||
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: null, primaryAssetId: null });
|
mocks.stack.getForAssetRemoval.mockResolvedValue({ id: null, primaryAssetId: null });
|
||||||
|
|
||||||
await expect(
|
await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: newUuid() })).rejects.toBeInstanceOf(
|
||||||
sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.imageFrom2015.id }),
|
BadRequestException,
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
);
|
||||||
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.event.emit).not.toHaveBeenCalled();
|
expect(mocks.event.emit).not.toHaveBeenCalled();
|
||||||
|
|
|
||||||
38
server/test/factories/asset-edit.factory.ts
Normal file
38
server/test/factories/asset-edit.factory.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Selectable } from 'kysely';
|
||||||
|
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||||
|
import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
|
import { build } from 'test/factories/builder.factory';
|
||||||
|
import { AssetEditLike, AssetLike, FactoryBuilder } from 'test/factories/types';
|
||||||
|
import { newUuid } from 'test/small.factory';
|
||||||
|
|
||||||
|
export class AssetEditFactory {
|
||||||
|
private constructor(private readonly value: Selectable<AssetEditTable>) {}
|
||||||
|
|
||||||
|
static create(dto: AssetEditLike = {}) {
|
||||||
|
return AssetEditFactory.from(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(dto: AssetEditLike = {}) {
|
||||||
|
const id = dto.id ?? newUuid();
|
||||||
|
|
||||||
|
return new AssetEditFactory({
|
||||||
|
id,
|
||||||
|
assetId: newUuid(),
|
||||||
|
action: AssetEditAction.Crop,
|
||||||
|
parameters: { x: 5, y: 6, width: 200, height: 100 },
|
||||||
|
sequence: 1,
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||||
|
const asset = build(AssetFactory.from(dto), builder);
|
||||||
|
this.value.assetId = asset.build().id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
return { ...this.value } as Selectable<AssetEditTable<AssetEditAction.Crop>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
server/test/factories/asset-file.factory.ts
Normal file
43
server/test/factories/asset-file.factory.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Selectable } from 'kysely';
|
||||||
|
import { AssetFileType } from 'src/enum';
|
||||||
|
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||||
|
import { AssetFactory } from 'test/factories/asset.factory';
|
||||||
|
import { build } from 'test/factories/builder.factory';
|
||||||
|
import { AssetFileLike, AssetLike, FactoryBuilder } from 'test/factories/types';
|
||||||
|
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
|
||||||
|
|
||||||
|
export class AssetFileFactory {
|
||||||
|
private constructor(private readonly value: Selectable<AssetFileTable>) {}
|
||||||
|
|
||||||
|
static create(dto: AssetFileLike = {}) {
|
||||||
|
return AssetFileFactory.from(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(dto: AssetFileLike = {}) {
|
||||||
|
const id = dto.id ?? newUuid();
|
||||||
|
const isEdited = dto.isEdited ?? false;
|
||||||
|
|
||||||
|
return new AssetFileFactory({
|
||||||
|
id,
|
||||||
|
assetId: newUuid(),
|
||||||
|
createdAt: newDate(),
|
||||||
|
updatedAt: newDate(),
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: `/data/12/34/thumbs/${id.slice(0, 2)}/${id.slice(2, 4)}/${id}${isEdited ? '_edited' : ''}.jpg`,
|
||||||
|
updateId: newUuidV7(),
|
||||||
|
isProgressive: false,
|
||||||
|
isEdited,
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
asset(dto: AssetLike = {}, builder?: FactoryBuilder<AssetFactory>) {
|
||||||
|
const asset = build(AssetFactory.from(dto), builder);
|
||||||
|
this.value.assetId = asset.build().id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
return { ...this.value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||||
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
|
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
|
||||||
import { AssetExifFactory } from 'test/factories/asset-exif.factory';
|
import { AssetExifFactory } from 'test/factories/asset-exif.factory';
|
||||||
|
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||||
import { build } from 'test/factories/builder.factory';
|
import { build } from 'test/factories/builder.factory';
|
||||||
import { AssetExifLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types';
|
import { AssetEditLike, AssetExifLike, AssetFileLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types';
|
||||||
import { UserFactory } from 'test/factories/user.factory';
|
import { UserFactory } from 'test/factories/user.factory';
|
||||||
import { factory, newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory';
|
import { newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory';
|
||||||
|
|
||||||
export class AssetFactory {
|
export class AssetFactory {
|
||||||
#assetExif?: AssetExifFactory;
|
|
||||||
#owner!: UserFactory;
|
#owner!: UserFactory;
|
||||||
|
#assetExif?: AssetExifFactory;
|
||||||
|
#files: AssetFileFactory[] = [];
|
||||||
|
#edits: AssetEditFactory[] = [];
|
||||||
|
|
||||||
private constructor(private readonly value: Selectable<AssetTable>) {
|
private constructor(private readonly value: Selectable<AssetTable>) {
|
||||||
value.ownerId ??= newUuid();
|
value.ownerId ??= newUuid();
|
||||||
|
|
@ -21,8 +26,12 @@ export class AssetFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
static from(dto: AssetLike = {}) {
|
static from(dto: AssetLike = {}) {
|
||||||
|
const id = dto.id ?? newUuid();
|
||||||
|
|
||||||
|
const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`;
|
||||||
|
|
||||||
return new AssetFactory({
|
return new AssetFactory({
|
||||||
id: factory.uuid(),
|
id,
|
||||||
createdAt: newDate(),
|
createdAt: newDate(),
|
||||||
updatedAt: newDate(),
|
updatedAt: newDate(),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
|
@ -42,8 +51,8 @@ export class AssetFactory {
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
localDateTime: newDate(),
|
localDateTime: newDate(),
|
||||||
originalFileName: 'IMG_123.jpg',
|
originalFileName,
|
||||||
originalPath: `/data/12/34/IMG_123.jpg`,
|
originalPath: `/data/library/${originalFileName}`,
|
||||||
ownerId: newUuid(),
|
ownerId: newUuid(),
|
||||||
stackId: null,
|
stackId: null,
|
||||||
thumbhash: null,
|
thumbhash: null,
|
||||||
|
|
@ -67,13 +76,51 @@ export class AssetFactory {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
edit(dto: AssetEditLike = {}, builder?: FactoryBuilder<AssetEditFactory>) {
|
||||||
|
this.#edits.push(build(AssetEditFactory.from(dto).asset(this.value), builder));
|
||||||
|
this.value.isEdited = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
file(dto: AssetFileLike = {}, builder?: FactoryBuilder<AssetFileFactory>) {
|
||||||
|
this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
files(dto?: 'edits'): AssetFactory;
|
||||||
|
files(items: AssetFileLike[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory;
|
||||||
|
files(items: AssetFileType[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory;
|
||||||
|
files(dto?: 'edits' | AssetFileLike[] | AssetFileType[], builder?: FactoryBuilder<AssetFileFactory>): AssetFactory {
|
||||||
|
const items: AssetFileLike[] = [];
|
||||||
|
|
||||||
|
if (dto === undefined || dto === 'edits') {
|
||||||
|
items.push(...Object.values(AssetFileType).map((type) => ({ type })));
|
||||||
|
|
||||||
|
if (dto === 'edits') {
|
||||||
|
items.push(...Object.values(AssetFileType).map((type) => ({ type, isEdited: true })));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const item of dto) {
|
||||||
|
items.push(typeof item === 'string' ? { type: item as AssetFileType } : item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of items) {
|
||||||
|
this.file(item, builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
const exif = this.#assetExif?.build();
|
const exif = this.#assetExif?.build();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...this.value,
|
...this.value,
|
||||||
exifInfo: exif as NonNullable<typeof exif>,
|
|
||||||
owner: this.#owner.build(),
|
owner: this.#owner.build(),
|
||||||
|
exifInfo: exif as NonNullable<typeof exif>,
|
||||||
|
files: this.#files.map((file) => file.build()),
|
||||||
|
edits: this.#edits.map((edit) => edit.build()),
|
||||||
|
faces: [] as Selectable<AssetFaceTable>[],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
|
import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
|
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
@ -10,6 +12,8 @@ export type FactoryBuilder<T, R extends T = T> = (builder: T) => R;
|
||||||
|
|
||||||
export type AssetLike = Partial<Selectable<AssetTable>>;
|
export type AssetLike = Partial<Selectable<AssetTable>>;
|
||||||
export type AssetExifLike = Partial<Selectable<AssetExifTable>>;
|
export type AssetExifLike = Partial<Selectable<AssetExifTable>>;
|
||||||
|
export type AssetEditLike = Partial<Selectable<AssetEditTable>>;
|
||||||
|
export type AssetFileLike = Partial<Selectable<AssetFileTable>>;
|
||||||
export type AlbumLike = Partial<Selectable<AlbumTable>>;
|
export type AlbumLike = Partial<Selectable<AlbumTable>>;
|
||||||
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
|
export type AlbumUserLike = Partial<Selectable<AlbumUserTable>>;
|
||||||
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
|
export type SharedLinkLike = Partial<Selectable<SharedLinkTable>>;
|
||||||
|
|
|
||||||
68
server/test/fixtures/album.stub.ts
vendored
68
server/test/fixtures/album.stub.ts
vendored
|
|
@ -45,56 +45,6 @@ export const albumStub = {
|
||||||
order: AssetOrder.Desc,
|
order: AssetOrder.Desc,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
}),
|
}),
|
||||||
sharedWithMultiple: Object.freeze({
|
|
||||||
id: 'album-3',
|
|
||||||
albumName: 'Empty album shared with users',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.admin.user.id,
|
|
||||||
owner: userStub.admin,
|
|
||||||
assets: [],
|
|
||||||
albumThumbnailAsset: null,
|
|
||||||
albumThumbnailAssetId: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [
|
|
||||||
{
|
|
||||||
user: userStub.user1,
|
|
||||||
role: AlbumUserRole.Editor,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user: userStub.user2,
|
|
||||||
role: AlbumUserRole.Editor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.Desc,
|
|
||||||
updateId: '42',
|
|
||||||
}),
|
|
||||||
sharedWithAdmin: Object.freeze({
|
|
||||||
id: 'album-3',
|
|
||||||
albumName: 'Empty album shared with admin',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.user1.user.id,
|
|
||||||
owner: userStub.user1,
|
|
||||||
assets: [],
|
|
||||||
albumThumbnailAsset: null,
|
|
||||||
albumThumbnailAssetId: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [
|
|
||||||
{
|
|
||||||
user: userStub.admin,
|
|
||||||
role: AlbumUserRole.Editor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.Desc,
|
|
||||||
updateId: '42',
|
|
||||||
}),
|
|
||||||
oneAsset: Object.freeze({
|
oneAsset: Object.freeze({
|
||||||
id: 'album-4',
|
id: 'album-4',
|
||||||
albumName: 'Album with one asset',
|
albumName: 'Album with one asset',
|
||||||
|
|
@ -113,24 +63,6 @@ export const albumStub = {
|
||||||
order: AssetOrder.Desc,
|
order: AssetOrder.Desc,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
}),
|
}),
|
||||||
twoAssets: Object.freeze({
|
|
||||||
id: 'album-4a',
|
|
||||||
albumName: 'Album with two assets',
|
|
||||||
description: '',
|
|
||||||
ownerId: authStub.admin.user.id,
|
|
||||||
owner: userStub.admin,
|
|
||||||
assets: [assetStub.image, assetStub.withLocation],
|
|
||||||
albumThumbnailAsset: assetStub.image,
|
|
||||||
albumThumbnailAssetId: assetStub.image.id,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
deletedAt: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
albumUsers: [],
|
|
||||||
isActivityEnabled: true,
|
|
||||||
order: AssetOrder.Desc,
|
|
||||||
updateId: '42',
|
|
||||||
}),
|
|
||||||
emptyWithValidThumbnail: Object.freeze({
|
emptyWithValidThumbnail: Object.freeze({
|
||||||
id: 'album-5',
|
id: 'album-5',
|
||||||
albumName: 'Empty album with valid thumbnail',
|
albumName: 'Empty album with valid thumbnail',
|
||||||
|
|
|
||||||
410
server/test/fixtures/asset.stub.ts
vendored
410
server/test/fixtures/asset.stub.ts
vendored
|
|
@ -20,45 +20,8 @@ const fullsizeFile = factory.assetFile({
|
||||||
path: '/uploads/user-id/fullsize/path.webp',
|
path: '/uploads/user-id/fullsize/path.webp',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidecarFileWithExt = factory.assetFile({
|
|
||||||
type: AssetFileType.Sidecar,
|
|
||||||
path: '/original/path.ext.xmp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const sidecarFileWithoutExt = factory.assetFile({
|
|
||||||
type: AssetFileType.Sidecar,
|
|
||||||
path: '/original/path.xmp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const editedPreviewFile = factory.assetFile({
|
|
||||||
type: AssetFileType.Preview,
|
|
||||||
path: '/uploads/user-id/preview/path_edited.jpg',
|
|
||||||
isEdited: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editedThumbnailFile = factory.assetFile({
|
|
||||||
type: AssetFileType.Thumbnail,
|
|
||||||
path: '/uploads/user-id/thumbnail/path_edited.jpg',
|
|
||||||
isEdited: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editedFullsizeFile = factory.assetFile({
|
|
||||||
type: AssetFileType.FullSize,
|
|
||||||
path: '/uploads/user-id/fullsize/path_edited.jpg',
|
|
||||||
isEdited: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = [fullsizeFile, previewFile, thumbnailFile];
|
const files = [fullsizeFile, previewFile, thumbnailFile];
|
||||||
|
|
||||||
const editedFiles = [
|
|
||||||
fullsizeFile,
|
|
||||||
previewFile,
|
|
||||||
thumbnailFile,
|
|
||||||
editedFullsizeFile,
|
|
||||||
editedPreviewFile,
|
|
||||||
editedThumbnailFile,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
|
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
|
||||||
return {
|
return {
|
||||||
id: stackId,
|
id: stackId,
|
||||||
|
|
@ -132,87 +95,6 @@ export const assetStub = {
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noWebpPath: 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/library/IMG_456.jpg',
|
|
||||||
files: [previewFile],
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
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,
|
|
||||||
livePhotoVideo: null,
|
|
||||||
livePhotoVideoId: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
originalFileName: 'IMG_456.jpg',
|
|
||||||
faces: [],
|
|
||||||
isExternal: false,
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 123_000,
|
|
||||||
} as Exif,
|
|
||||||
deletedAt: null,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
updateId: '42',
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
noThumbhash: 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.ext',
|
|
||||||
files,
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
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,
|
|
||||||
duration: null,
|
|
||||||
isExternal: false,
|
|
||||||
livePhotoVideo: null,
|
|
||||||
livePhotoVideoId: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
originalFileName: 'asset-id.ext',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
updateId: '42',
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
primaryImage: Object.freeze({
|
primaryImage: Object.freeze({
|
||||||
id: 'primary-asset-id',
|
id: 'primary-asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -526,48 +408,6 @@ export const assetStub = {
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageFrom2015: Object.freeze({
|
|
||||||
id: 'asset-id-2015',
|
|
||||||
status: AssetStatus.Active,
|
|
||||||
deviceAssetId: 'device-asset-id',
|
|
||||||
fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'),
|
|
||||||
fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'),
|
|
||||||
owner: userStub.user1,
|
|
||||||
ownerId: 'user-id',
|
|
||||||
deviceId: 'device-id',
|
|
||||||
originalPath: '/original/path.ext',
|
|
||||||
files,
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
thumbhash: Buffer.from('blablabla', 'base64'),
|
|
||||||
encodedVideoPath: null,
|
|
||||||
createdAt: new Date('2015-02-23T05:06:29.716Z'),
|
|
||||||
updatedAt: new Date('2015-02-23T05:06:29.716Z'),
|
|
||||||
localDateTime: new Date('2015-02-23T05:06:29.716Z'),
|
|
||||||
isFavorite: true,
|
|
||||||
isExternal: false,
|
|
||||||
duration: null,
|
|
||||||
livePhotoVideo: null,
|
|
||||||
livePhotoVideoId: null,
|
|
||||||
updateId: 'foo',
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
originalFileName: 'asset-id.ext',
|
|
||||||
faces: [],
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 5000,
|
|
||||||
} as Exif,
|
|
||||||
deletedAt: null,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
video: Object.freeze({
|
video: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -736,81 +576,6 @@ export const assetStub = {
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecar: 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.ext',
|
|
||||||
thumbhash: null,
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
files: [previewFile, sidecarFileWithExt],
|
|
||||||
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: [],
|
|
||||||
originalFileName: 'asset-id.ext',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
updateId: 'foo',
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
sidecarWithoutExt: 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.ext',
|
|
||||||
thumbhash: null,
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
files: [previewFile, sidecarFileWithoutExt],
|
|
||||||
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: [],
|
|
||||||
originalFileName: 'asset-id.ext',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
hasEncodedVideo: Object.freeze({
|
hasEncodedVideo: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -854,46 +619,6 @@ export const assetStub = {
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasFileExtension: 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('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,
|
|
||||||
isExternal: true,
|
|
||||||
duration: null,
|
|
||||||
livePhotoVideo: null,
|
|
||||||
livePhotoVideoId: null,
|
|
||||||
libraryId: 'library-id',
|
|
||||||
sharedLinks: [],
|
|
||||||
originalFileName: 'photo.jpg',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 5000,
|
|
||||||
} as Exif,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
imageDng: Object.freeze({
|
imageDng: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -938,93 +663,6 @@ export const assetStub = {
|
||||||
isEdited: false,
|
isEdited: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageHif: 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.hif',
|
|
||||||
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.hif',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 5000,
|
|
||||||
profileDescription: 'Adobe RGB',
|
|
||||||
bitsPerSample: 14,
|
|
||||||
} as Exif,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
updateId: '42',
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
panoramaTif: 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.tif',
|
|
||||||
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.tif',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
sidecarPath: null,
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 5000,
|
|
||||||
projectionType: 'EQUIRECTANGULAR',
|
|
||||||
} as Exif,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
updateId: '42',
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
edits: [],
|
|
||||||
}),
|
|
||||||
|
|
||||||
withCropEdit: Object.freeze({
|
withCropEdit: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -1082,52 +720,4 @@ export const assetStub = {
|
||||||
] as AssetEditActionItem[],
|
] as AssetEditActionItem[],
|
||||||
isEdited: true,
|
isEdited: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
withoutEdits: 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',
|
|
||||||
files: editedFiles,
|
|
||||||
checksum: Buffer.from('file hash', 'utf8'),
|
|
||||||
type: AssetType.Image,
|
|
||||||
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('2025-01-01T01:02:03.456Z'),
|
|
||||||
isFavorite: true,
|
|
||||||
duration: null,
|
|
||||||
isExternal: false,
|
|
||||||
livePhotoVideo: null,
|
|
||||||
livePhotoVideoId: null,
|
|
||||||
updateId: 'foo',
|
|
||||||
libraryId: null,
|
|
||||||
stackId: null,
|
|
||||||
sharedLinks: [],
|
|
||||||
originalFileName: 'asset-id.jpg',
|
|
||||||
faces: [],
|
|
||||||
deletedAt: null,
|
|
||||||
sidecarPath: null,
|
|
||||||
exifInfo: {
|
|
||||||
fileSizeInByte: 5000,
|
|
||||||
exifImageHeight: 3840,
|
|
||||||
exifImageWidth: 2160,
|
|
||||||
} as Exif,
|
|
||||||
duplicateId: null,
|
|
||||||
isOffline: false,
|
|
||||||
stack: null,
|
|
||||||
orientation: '',
|
|
||||||
projectionType: null,
|
|
||||||
height: 3840,
|
|
||||||
width: 2160,
|
|
||||||
visibility: AssetVisibility.Timeline,
|
|
||||||
edits: [],
|
|
||||||
isEdited: false,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
17
server/test/fixtures/user.stub.ts
vendored
17
server/test/fixtures/user.stub.ts
vendored
|
|
@ -38,21 +38,4 @@ export const userStub = {
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
},
|
},
|
||||||
user2: <UserAdmin>{
|
|
||||||
...authStub.user2.user,
|
|
||||||
status: UserStatus.Active,
|
|
||||||
profileChangedAt: new Date('2021-01-01'),
|
|
||||||
metadata: [],
|
|
||||||
name: 'immich_name',
|
|
||||||
storageLabel: null,
|
|
||||||
oauthId: '',
|
|
||||||
shouldChangePassword: false,
|
|
||||||
avatarColor: null,
|
|
||||||
profileImagePath: '',
|
|
||||||
createdAt: new Date('2021-01-01'),
|
|
||||||
deletedAt: null,
|
|
||||||
updatedAt: new Date('2021-01-01'),
|
|
||||||
quotaSizeInBytes: null,
|
|
||||||
quotaUsageInBytes: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue