chore: upgrade to kysely 0.28.11 (#26744)

This commit is contained in:
Daniel Dietzler
2026-03-11 16:17:31 +01:00
committed by GitHub
parent 8764a1894b
commit 34ce68095d
50 changed files with 941 additions and 857 deletions
+5 -4
View File
@@ -1,6 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { ReactionType } from 'src/dtos/activity.dto';
import { ActivityService } from 'src/services/activity.service';
import { getForActivity } from 'test/mappers';
import { factory, newUuid, newUuids } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -78,7 +79,7 @@ describe(ActivityService.name, () => {
const activity = factory.activity({ albumId, assetId, userId });
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
mocks.activity.create.mockResolvedValue(getForActivity(activity));
await sut.create(factory.auth({ user: { id: userId } }), {
albumId,
@@ -101,7 +102,7 @@ describe(ActivityService.name, () => {
const activity = factory.activity({ albumId, assetId });
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
mocks.activity.create.mockResolvedValue(getForActivity(activity));
await expect(
sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }),
@@ -113,7 +114,7 @@ describe(ActivityService.name, () => {
const activity = factory.activity({ userId, albumId, assetId, isLiked: true });
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
mocks.activity.create.mockResolvedValue(getForActivity(activity));
mocks.activity.search.mockResolvedValue([]);
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE });
@@ -127,7 +128,7 @@ describe(ActivityService.name, () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([activity]);
mocks.activity.search.mockResolvedValue([getForActivity(activity)]);
await sut.create(factory.auth(), { albumId, assetId, type: ReactionType.LIKE });
+59 -54
View File
@@ -1,5 +1,4 @@
import { BadRequestException } from '@nestjs/common';
import _ from 'lodash';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum';
import { AlbumService } from 'src/services/album.service';
@@ -9,6 +8,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { UserFactory } from 'test/factories/user.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAlbum } from 'test/mappers';
import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -45,7 +45,7 @@ describe(AlbumService.name, () => {
it('gets list of albums for auth user', async () => {
const album = AlbumFactory.from().albumUser().build();
const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).albumUser().build();
mocks.album.getOwned.mockResolvedValue([album, sharedWithUserAlbum]);
mocks.album.getOwned.mockResolvedValue([getForAlbum(album), getForAlbum(sharedWithUserAlbum)]);
mocks.album.getMetadataForIds.mockResolvedValue([
{
albumId: album.id,
@@ -70,8 +70,13 @@ describe(AlbumService.name, () => {
});
it('gets list of albums that have a specific asset', async () => {
const album = AlbumFactory.from().owner({ isAdmin: true }).albumUser().asset().asset().build();
mocks.album.getByAssetId.mockResolvedValue([album]);
const album = AlbumFactory.from()
.owner({ isAdmin: true })
.albumUser()
.asset({}, (builder) => builder.exif())
.asset({}, (builder) => builder.exif())
.build();
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
mocks.album.getMetadataForIds.mockResolvedValue([
{
albumId: album.id,
@@ -90,7 +95,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are shared', async () => {
const album = AlbumFactory.from().albumUser().build();
mocks.album.getShared.mockResolvedValue([album]);
mocks.album.getShared.mockResolvedValue([getForAlbum(album)]);
mocks.album.getMetadataForIds.mockResolvedValue([
{
albumId: album.id,
@@ -109,7 +114,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are NOT shared', async () => {
const album = AlbumFactory.create();
mocks.album.getNotShared.mockResolvedValue([album]);
mocks.album.getNotShared.mockResolvedValue([getForAlbum(album)]);
mocks.album.getMetadataForIds.mockResolvedValue([
{
albumId: album.id,
@@ -129,7 +134,7 @@ describe(AlbumService.name, () => {
it('counts assets correctly', async () => {
const album = AlbumFactory.create();
mocks.album.getOwned.mockResolvedValue([album]);
mocks.album.getOwned.mockResolvedValue([getForAlbum(album)]);
mocks.album.getMetadataForIds.mockResolvedValue([
{
albumId: album.id,
@@ -155,7 +160,7 @@ describe(AlbumService.name, () => {
.albumUser(albumUser)
.build();
mocks.album.create.mockResolvedValue(album);
mocks.album.create.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user));
mocks.user.getMetadata.mockResolvedValue([]);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
@@ -192,7 +197,7 @@ describe(AlbumService.name, () => {
.asset({ id: assetId }, (asset) => asset.exif())
.albumUser(albumUser)
.build();
mocks.album.create.mockResolvedValue(album);
mocks.album.create.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
mocks.user.getMetadata.mockResolvedValue([
{
@@ -250,7 +255,7 @@ describe(AlbumService.name, () => {
.albumUser()
.build();
mocks.user.get.mockResolvedValue(album.albumUsers[0].user);
mocks.album.create.mockResolvedValue(album);
mocks.album.create.mockResolvedValue(getForAlbum(album));
mocks.user.getMetadata.mockResolvedValue([]);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
@@ -316,7 +321,7 @@ describe(AlbumService.name, () => {
it('should require a valid thumbnail asset id', async () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValue(new Set());
await expect(
@@ -330,8 +335,8 @@ describe(AlbumService.name, () => {
it('should allow the owner to update the album', async () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.update.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.update.mockResolvedValue(getForAlbum(album));
await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' });
@@ -352,7 +357,7 @@ describe(AlbumService.name, () => {
it('should not let a shared user delete the album', async () => {
const album = AlbumFactory.create();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set());
await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException);
@@ -363,7 +368,7 @@ describe(AlbumService.name, () => {
it('should let the owner delete an album', async () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await sut.delete(AuthFactory.create(album.owner), album.id);
@@ -387,7 +392,7 @@ describe(AlbumService.name, () => {
const userId = newUuid();
const album = AlbumFactory.from().albumUser({ userId }).build();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }),
).rejects.toBeInstanceOf(BadRequestException);
@@ -398,7 +403,7 @@ describe(AlbumService.name, () => {
it('should throw an error if the userId does not exist', async () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(void 0);
await expect(
sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }),
@@ -410,7 +415,7 @@ describe(AlbumService.name, () => {
it('should throw an error if the userId is the ownerId', async () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(
sut.addUsers(AuthFactory.create(album.owner), album.id, {
albumUsers: [{ userId: album.owner.id }],
@@ -424,8 +429,8 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.create();
const user = UserFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.update.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.update.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(user);
mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build());
@@ -456,7 +461,7 @@ describe(AlbumService.name, () => {
const userId = newUuid();
const album = AlbumFactory.from().albumUser({ userId }).build();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.albumUser.delete.mockResolvedValue();
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined();
@@ -470,7 +475,7 @@ describe(AlbumService.name, () => {
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const album = AlbumFactory.from().albumUser({ userId: user1.id }).albumUser({ userId: user2.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(sut.removeUser(AuthFactory.create(user1), album.id, user2.id)).rejects.toBeInstanceOf(
BadRequestException,
@@ -483,7 +488,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves', async () => {
const user1 = UserFactory.create();
const album = AlbumFactory.from().albumUser({ userId: user1.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.albumUser.delete.mockResolvedValue();
await sut.removeUser(AuthFactory.create(user1), album.id, user1.id);
@@ -495,7 +500,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves using "me"', async () => {
const user = UserFactory.create();
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.albumUser.delete.mockResolvedValue();
await sut.removeUser(AuthFactory.create(user), album.id, 'me');
@@ -506,7 +511,7 @@ describe(AlbumService.name, () => {
it('should not allow the owner to be removed', async () => {
const album = AlbumFactory.from().albumUser().build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf(
BadRequestException,
@@ -517,7 +522,7 @@ describe(AlbumService.name, () => {
it('should throw an error for a user not in the album', async () => {
const album = AlbumFactory.from().albumUser().build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf(
BadRequestException,
@@ -546,7 +551,7 @@ describe(AlbumService.name, () => {
describe('getAlbumInfo', () => {
it('should get a shared album', async () => {
const album = AlbumFactory.from().albumUser().build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getMetadataForIds.mockResolvedValue([
{
@@ -566,7 +571,7 @@ describe(AlbumService.name, () => {
it('should get a shared album via a shared link', async () => {
const album = AlbumFactory.from().albumUser().build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getMetadataForIds.mockResolvedValue([
{
@@ -588,7 +593,7 @@ describe(AlbumService.name, () => {
it('should get a shared album via shared with user', async () => {
const user = UserFactory.create();
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getMetadataForIds.mockResolvedValue([
{
@@ -630,7 +635,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(
@@ -654,7 +659,7 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([
@@ -675,7 +680,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(
@@ -703,7 +708,7 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build();
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set());
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(
sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }),
@@ -718,7 +723,7 @@ describe(AlbumService.name, () => {
const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build();
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([
@@ -742,7 +747,7 @@ describe(AlbumService.name, () => {
const asset = AssetFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -762,7 +767,7 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id]));
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -776,7 +781,7 @@ describe(AlbumService.name, () => {
const asset = AssetFactory.create();
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set());
await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -791,7 +796,7 @@ describe(AlbumService.name, () => {
const user = UserFactory.create();
const album = AlbumFactory.create();
const asset = AssetFactory.create({ ownerId: user.id });
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf(
BadRequestException,
@@ -804,7 +809,7 @@ describe(AlbumService.name, () => {
it('should not allow unauthorized shared link access to the album', async () => {
const album = AlbumFactory.create();
const asset = AssetFactory.create();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
await expect(
sut.addAssets(AuthFactory.from().sharedLink({ allowUpload: true }).build(), album.id, { ids: [asset.id] }),
@@ -821,7 +826,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -859,7 +864,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -897,7 +902,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -943,7 +948,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -965,7 +970,7 @@ describe(AlbumService.name, () => {
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build();
@@ -1004,7 +1009,7 @@ describe(AlbumService.name, () => {
];
mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id]));
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -1048,7 +1053,7 @@ describe(AlbumService.name, () => {
mocks.album.getAssetIds
.mockResolvedValueOnce(new Set([asset1.id, asset2.id, asset3.id]))
.mockResolvedValueOnce(new Set());
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
await expect(
sut.addAssetsToAlbums(AuthFactory.create(album1.owner), {
@@ -1078,7 +1083,7 @@ describe(AlbumService.name, () => {
.mockResolvedValueOnce(new Set([album1.id]))
.mockResolvedValueOnce(new Set([album2.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id]));
await expect(
@@ -1107,7 +1112,7 @@ describe(AlbumService.name, () => {
mocks.access.album.checkSharedAlbumAccess
.mockResolvedValueOnce(new Set([album1.id]))
.mockResolvedValueOnce(new Set([album2.id]));
mocks.album.getById.mockResolvedValueOnce(_.cloneDeep(album1)).mockResolvedValueOnce(_.cloneDeep(album2));
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set());
await expect(
@@ -1138,7 +1143,7 @@ describe(AlbumService.name, () => {
const album1 = AlbumFactory.create();
const album2 = AlbumFactory.create();
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
await expect(
sut.addAssetsToAlbums(AuthFactory.create(user), {
@@ -1160,7 +1165,7 @@ describe(AlbumService.name, () => {
const album1 = AlbumFactory.create();
const album2 = AlbumFactory.create();
const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()];
mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2);
mocks.album.getById.mockResolvedValueOnce(getForAlbum(album1)).mockResolvedValueOnce(getForAlbum(album2));
await expect(
sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), {
@@ -1182,7 +1187,7 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -1196,7 +1201,7 @@ describe(AlbumService.name, () => {
const asset = AssetFactory.create();
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValue(new Set());
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -1210,7 +1215,7 @@ describe(AlbumService.name, () => {
const asset = AssetFactory.create();
const album = AlbumFactory.create();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id]));
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([
@@ -1224,7 +1229,7 @@ describe(AlbumService.name, () => {
const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id]));
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id]));
await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset1.id] })).resolves.toEqual([
+7 -6
View File
@@ -21,6 +21,7 @@ import { Permission } from 'src/enum';
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util';
import { asDateString } from 'src/utils/date';
import { getPreferences } from 'src/utils/preferences';
@Injectable()
@@ -64,11 +65,11 @@ export class AlbumService extends BaseService {
return albums.map((album) => ({
...mapAlbumWithoutAssets(album),
sharedLinks: undefined,
startDate: albumMetadata[album.id]?.startDate ?? undefined,
endDate: albumMetadata[album.id]?.endDate ?? undefined,
startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined),
endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined),
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
lastModifiedAssetTimestamp: asDateString(albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined),
}));
}
@@ -85,10 +86,10 @@ export class AlbumService extends BaseService {
return {
...mapAlbum(album, withAssets, auth),
startDate: albumMetadataForIds?.startDate ?? undefined,
endDate: albumMetadataForIds?.endDate ?? undefined,
startDate: asDateString(albumMetadataForIds?.startDate ?? undefined),
endDate: asDateString(albumMetadataForIds?.endDate ?? undefined),
assetCount: albumMetadataForIds?.assetCount ?? 0,
lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
lastModifiedAssetTimestamp: asDateString(albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined),
contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined,
};
}
+6 -203
View File
@@ -4,13 +4,12 @@ import {
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { Stats } from 'node:fs';
import { AssetFile } from 'src/database';
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
import { AssetMediaCreateDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetEditAction } from 'src/dtos/editing.dto';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
import { AssetFileType, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
import { AuthRequest } from 'src/middleware/auth.guard';
import { AssetMediaService } from 'src/services/asset-media.service';
import { UploadBody } from 'src/types';
@@ -22,6 +21,7 @@ import { AuthFactory } from 'test/factories/auth.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { userStub } from 'test/fixtures/user.stub';
import { getForAsset } from 'test/mappers';
import { newTestService, ServiceMocks } from 'test/utils';
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
@@ -152,13 +152,6 @@ const createDto = Object.freeze({
duration: '0:00:00.000000',
}) as AssetMediaCreateDto;
const replaceDto = Object.freeze({
deviceAssetId: 'deviceAssetId',
deviceId: 'deviceId',
fileModifiedAt: new Date('2024-04-15T23:41:36.910Z'),
fileCreatedAt: new Date('2024-04-15T23:41:36.910Z'),
}) as AssetMediaReplaceDto;
const assetEntity = Object.freeze({
id: 'id_1',
ownerId: 'user_id_1',
@@ -180,25 +173,6 @@ const assetEntity = Object.freeze({
livePhotoVideoId: null,
} as MapAsset);
const existingAsset = Object.freeze({
...assetEntity,
duration: null,
type: AssetType.Image,
checksum: Buffer.from('_getExistingAsset', 'utf8'),
libraryId: 'libraryId',
originalFileName: 'existing-filename.jpeg',
}) as MapAsset;
const sidecarAsset = Object.freeze({
...existingAsset,
checksum: Buffer.from('_getExistingAssetWithSideCar', 'utf8'),
}) as MapAsset;
const copiedAsset = Object.freeze({
id: 'copied-asset',
originalPath: 'copied-path',
}) as MapAsset;
describe(AssetMediaService.name, () => {
let sut: AssetMediaService;
let mocks: ServiceMocks;
@@ -434,7 +408,7 @@ describe(AssetMediaService.name, () => {
.owner(authStub.user1.user)
.build();
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.create.mockResolvedValueOnce(asset);
await expect(
@@ -451,7 +425,7 @@ describe(AssetMediaService.name, () => {
it('should hide the linked motion asset', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build();
const asset = AssetFactory.create();
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.create.mockResolvedValueOnce(asset);
await expect(
@@ -470,7 +444,7 @@ describe(AssetMediaService.name, () => {
it('should handle a sidecar file', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
mocks.asset.getById.mockResolvedValueOnce(asset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
mocks.asset.create.mockResolvedValueOnce(asset);
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
@@ -776,177 +750,6 @@ describe(AssetMediaService.name, () => {
});
});
describe('replaceAsset', () => {
it('should fail the auth check when update photo does not exist', async () => {
await expect(sut.replaceAsset(authStub.user1, 'id', replaceDto, fileStub.photo)).rejects.toThrow(
'Not found or no asset.update access',
);
expect(mocks.asset.create).not.toHaveBeenCalled();
});
it('should fail if asset cannot be fetched', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, fileStub.photo)).rejects.toThrow(
'Asset not found',
);
expect(mocks.asset.create).not.toHaveBeenCalled();
});
it('should update a photo with no sidecar to photo with no sidecar', async () => {
const updatedFile = fileStub.photo;
const updatedAsset = { ...existingAsset, ...updatedFile };
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([existingAsset.id]));
// this is the original file size
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
// this is for the clone call
mocks.asset.create.mockResolvedValue(copiedAsset);
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
status: AssetMediaStatus.REPLACED,
id: 'copied-asset',
});
expect(mocks.asset.update).toHaveBeenCalledWith(
expect.objectContaining({
id: existingAsset.id,
originalFileName: 'photo1.jpeg',
originalPath: 'fake_path/photo1.jpeg',
}),
);
expect(mocks.asset.create).toHaveBeenCalledWith(
expect.objectContaining({
originalFileName: 'existing-filename.jpeg',
originalPath: 'fake_path/asset_1.jpeg',
}),
);
expect(mocks.asset.deleteFile).toHaveBeenCalledWith(
expect.objectContaining({
assetId: existingAsset.id,
type: AssetFileType.Sidecar,
}),
);
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.Trashed,
});
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith(
updatedFile.originalPath,
expect.any(Date),
new Date(replaceDto.fileModifiedAt),
);
});
it('should update a photo with sidecar to photo with sidecar', async () => {
const updatedFile = fileStub.photo;
const sidecarFile = fileStub.photoSidecar;
const updatedAsset = { ...sidecarAsset, ...updatedFile };
mocks.asset.getById.mockResolvedValueOnce(existingAsset);
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
// this is the original file size
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
// this is for the clone call
mocks.asset.create.mockResolvedValue(copiedAsset);
await expect(
sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile, sidecarFile),
).resolves.toEqual({
status: AssetMediaStatus.REPLACED,
id: 'copied-asset',
});
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.Trashed,
});
expect(mocks.asset.upsertFile).toHaveBeenCalledWith(
expect.objectContaining({
assetId: existingAsset.id,
path: sidecarFile.originalPath,
type: AssetFileType.Sidecar,
}),
);
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith(
updatedFile.originalPath,
expect.any(Date),
new Date(replaceDto.fileModifiedAt),
);
});
it('should update a photo with a sidecar to photo with no sidecar', async () => {
const updatedFile = fileStub.photo;
const updatedAsset = { ...sidecarAsset, ...updatedFile };
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
mocks.asset.getById.mockResolvedValueOnce(updatedAsset);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
// this is the original file size
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
// this is for the copy call
mocks.asset.create.mockResolvedValue(copiedAsset);
await expect(sut.replaceAsset(authStub.user1, existingAsset.id, replaceDto, updatedFile)).resolves.toEqual({
status: AssetMediaStatus.REPLACED,
id: 'copied-asset',
});
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date),
status: AssetStatus.Trashed,
});
expect(mocks.asset.deleteFile).toHaveBeenCalledWith(
expect.objectContaining({
assetId: existingAsset.id,
type: AssetFileType.Sidecar,
}),
);
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith(
updatedFile.originalPath,
expect.any(Date),
new Date(replaceDto.fileModifiedAt),
);
});
it('should handle a photo with sidecar to duplicate photo ', async () => {
const updatedFile = fileStub.photo;
const error = new Error('unique key violation');
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
mocks.asset.update.mockRejectedValue(error);
mocks.asset.getById.mockResolvedValueOnce(sidecarAsset);
mocks.asset.getUploadAssetIdByChecksum.mockResolvedValue(sidecarAsset.id);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([sidecarAsset.id]));
// this is the original file size
mocks.storage.stat.mockResolvedValue({ size: 0 } as Stats);
// this is for the clone call
mocks.asset.create.mockResolvedValue(copiedAsset);
await expect(sut.replaceAsset(authStub.user1, sidecarAsset.id, replaceDto, updatedFile)).resolves.toEqual({
status: AssetMediaStatus.DUPLICATE,
id: sidecarAsset.id,
});
expect(mocks.asset.create).not.toHaveBeenCalled();
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
expect(mocks.asset.deleteFile).not.toHaveBeenCalled();
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.FileDelete,
data: { files: [updatedFile.originalPath, undefined] },
});
expect(mocks.user.updateUsage).not.toHaveBeenCalled();
});
});
describe('bulkUploadCheck', () => {
it('should accept hex and base64 checksums', async () => {
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
+31 -34
View File
@@ -8,6 +8,7 @@ import { AssetService } from 'src/services/asset.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers';
import { factory, newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
@@ -71,7 +72,7 @@ describe(AssetService.name, () => {
describe('getRandom', () => {
it('should get own random assets', async () => {
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
await sut.getRandom(authStub.admin, 1);
@@ -82,8 +83,8 @@ describe(AssetService.name, () => {
const partner = factory.partner({ inTimeline: false });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
mocks.partner.getAll.mockResolvedValue([partner]);
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
await sut.getRandom(auth, 1);
@@ -94,8 +95,8 @@ describe(AssetService.name, () => {
const partner = factory.partner({ inTimeline: true });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]);
mocks.partner.getAll.mockResolvedValue([partner]);
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
await sut.getRandom(auth, 1);
@@ -107,7 +108,7 @@ describe(AssetService.name, () => {
it('should allow owner access', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await sut.get(authStub.admin, asset.id);
@@ -121,7 +122,7 @@ describe(AssetService.name, () => {
it('should allow shared link access', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await sut.get(authStub.adminSharedLink, asset.id);
@@ -134,7 +135,7 @@ describe(AssetService.name, () => {
it('should strip metadata for shared link if exif is disabled', async () => {
const asset = AssetFactory.from().exif({ description: 'foo' }).build();
mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
const result = await sut.get(
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
@@ -152,7 +153,7 @@ describe(AssetService.name, () => {
it('should allow partner sharing access', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await sut.get(authStub.admin, asset.id);
@@ -162,7 +163,7 @@ describe(AssetService.name, () => {
it('should allow shared album access', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await sut.get(authStub.admin, asset.id);
@@ -204,8 +205,8 @@ describe(AssetService.name, () => {
it('should update the asset', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.update.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
mocks.asset.update.mockResolvedValue(getForAsset(asset));
await sut.update(authStub.admin, asset.id, { isFavorite: true });
@@ -215,8 +216,8 @@ describe(AssetService.name, () => {
it('should update the exif description', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.update.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
mocks.asset.update.mockResolvedValue(getForAsset(asset));
await sut.update(authStub.admin, asset.id, { description: 'Test description' });
@@ -229,8 +230,8 @@ describe(AssetService.name, () => {
it('should update the exif rating', async () => {
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValueOnce(asset);
mocks.asset.update.mockResolvedValueOnce(asset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
mocks.asset.update.mockResolvedValueOnce(getForAsset(asset));
await sut.update(authStub.admin, asset.id, { rating: 3 });
@@ -274,7 +275,7 @@ describe(AssetService.name, () => {
const motionAsset = AssetFactory.from().owner(auth.user).build();
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(asset);
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
await expect(
sut.update(authStub.admin, asset.id, {
@@ -301,7 +302,7 @@ describe(AssetService.name, () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video });
const asset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValue(motionAsset);
mocks.asset.getById.mockResolvedValue(getForAsset(motionAsset));
await expect(
sut.update(auth, asset.id, {
@@ -327,9 +328,9 @@ describe(AssetService.name, () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline });
const stillAsset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id]));
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
mocks.asset.getById.mockResolvedValueOnce(stillAsset);
mocks.asset.update.mockResolvedValue(stillAsset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(stillAsset));
mocks.asset.update.mockResolvedValue(getForAsset(stillAsset));
const auth = AuthFactory.from(motionAsset.owner).build();
await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id });
@@ -354,9 +355,9 @@ describe(AssetService.name, () => {
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
const unlinkedAsset = AssetFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.asset.getById.mockResolvedValueOnce(asset);
mocks.asset.getById.mockResolvedValueOnce(motionAsset);
mocks.asset.update.mockResolvedValueOnce(unlinkedAsset);
mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset));
mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset));
mocks.asset.update.mockResolvedValueOnce(getForAsset(unlinkedAsset));
await sut.update(auth, asset.id, { livePhotoVideoId: null });
@@ -569,7 +570,7 @@ describe(AssetService.name, () => {
.file({ type: AssetFileType.Preview, isEdited: true })
.file({ type: AssetFileType.Thumbnail, isEdited: true })
.build();
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
@@ -583,7 +584,7 @@ describe(AssetService.name, () => {
},
],
]);
expect(mocks.asset.remove).toHaveBeenCalledWith(asset);
expect(mocks.asset.remove).toHaveBeenCalledWith(getForAssetDeletion(asset));
});
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
@@ -591,11 +592,7 @@ describe(AssetService.name, () => {
.stack({}, (builder) => builder.asset())
.build();
mocks.stack.delete.mockResolvedValue();
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
...asset,
// TODO the specific query filters out the primary asset from `stack.assets`. This should be in a mapper eventually
stack: { ...asset.stack!, assets: asset.stack!.assets.filter(({ id }) => id !== asset.stack!.primaryAssetId) },
});
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
@@ -605,7 +602,7 @@ describe(AssetService.name, () => {
it('should delete a live photo', async () => {
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
await sut.handleAssetDeletion({
@@ -622,7 +619,7 @@ describe(AssetService.name, () => {
it('should not delete a live motion part if it is being used by another asset', async () => {
const asset = AssetFactory.create({ livePhotoVideoId: newUuid() });
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
@@ -633,7 +630,7 @@ describe(AssetService.name, () => {
it('should update usage', async () => {
const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build();
mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset);
mocks.assetJob.getForAssetDeletion.mockResolvedValue(getForAssetDeletion(asset));
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000);
});
@@ -3,6 +3,7 @@ import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForDuplicate } from 'test/mappers';
import { newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
import { beforeEach, vitest } from 'vitest';
@@ -39,11 +40,11 @@ describe(SearchService.name, () => {
describe('getDuplicates', () => {
it('should get duplicates', async () => {
const asset = AssetFactory.create();
const asset = AssetFactory.from().exif().build();
mocks.duplicateRepository.getAll.mockResolvedValue([
{
duplicateId: 'duplicate-id',
assets: [asset, asset],
assets: [getForDuplicate(asset), getForDuplicate(asset)],
},
]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
+2 -2
View File
@@ -186,8 +186,8 @@ export class JobService extends BaseService {
exifImageHeight: exif.exifImageHeight,
fileSizeInByte: exif.fileSizeInByte,
orientation: exif.orientation,
dateTimeOriginal: exif.dateTimeOriginal,
modifyDate: exif.modifyDate,
dateTimeOriginal: exif.dateTimeOriginal ? new Date(exif.dateTimeOriginal) : null,
modifyDate: exif.modifyDate ? new Date(exif.modifyDate) : null,
timeZone: exif.timeZone,
latitude: exif.latitude,
longitude: exif.longitude,
+6 -3
View File
@@ -3,6 +3,7 @@ import { AlbumFactory } from 'test/factories/album.factory';
import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { userStub } from 'test/fixtures/user.stub';
import { getForAlbum, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -52,7 +53,7 @@ describe(MapService.name, () => {
state: asset.exifInfo.state,
country: asset.exifInfo.country,
};
mocks.partner.getAll.mockResolvedValue([partner]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
mocks.map.getMapMarkers.mockResolvedValue([marker]);
const markers = await sut.getMapMarkers(auth, { withPartners: true });
@@ -81,8 +82,10 @@ describe(MapService.name, () => {
};
mocks.partner.getAll.mockResolvedValue([]);
mocks.map.getMapMarkers.mockResolvedValue([marker]);
mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]);
mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]);
mocks.album.getOwned.mockResolvedValue([getForAlbum(AlbumFactory.create())]);
mocks.album.getShared.mockResolvedValue([
getForAlbum(AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()),
]);
const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true });
+63 -57
View File
@@ -1,3 +1,4 @@
import { ShallowDehydrateObject } from 'kysely';
import { OutputInfo } from 'sharp';
import { SystemConfig } from 'src/config';
import { Exif } from 'src/database';
@@ -27,6 +28,7 @@ import { PersonFactory } from 'test/factories/person.factory';
import { probeStub } from 'test/fixtures/media.stub';
import { personThumbnailStub } from 'test/fixtures/person.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { getForGenerateThumbnail } from 'test/mappers';
import { factory, newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
@@ -367,8 +369,10 @@ describe(MediaService.name, () => {
});
it('should skip thumbnail generation if asset type is unknown', async () => {
const asset = AssetFactory.create({ type: 'foo' as AssetType });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
const asset = AssetFactory.from({ type: 'foo' as AssetType })
.exif()
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
expect(mocks.media.probe).not.toHaveBeenCalled();
@@ -377,17 +381,17 @@ describe(MediaService.name, () => {
});
it('should skip video thumbnail generation if no video stream', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
const asset = AssetFactory.from({ type: AssetType.Video }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError();
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalledWith();
});
it('should skip invisible assets', async () => {
const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden }).exif().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped);
@@ -398,7 +402,7 @@ describe(MediaService.name, () => {
it('should delete previous preview if different path', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -415,7 +419,7 @@ describe(MediaService.name, () => {
.exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 })
.files([AssetFileType.Preview, AssetFileType.Thumbnail])
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
@@ -490,9 +494,9 @@ describe(MediaService.name, () => {
});
it('should generate a thumbnail for a video', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
@@ -532,9 +536,9 @@ describe(MediaService.name, () => {
});
it('should tonemap thumbnail for hdr video', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
@@ -574,12 +578,12 @@ describe(MediaService.name, () => {
});
it('should always generate video thumbnail in one pass', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
mocks.systemMetadata.get.mockResolvedValue({
ffmpeg: { twoPass: true, maxBitrate: '5000k' },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.media.transcode).toHaveBeenCalledWith(
@@ -600,9 +604,9 @@ describe(MediaService.name, () => {
});
it('should not skip intra frames for MTS file', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStreamMTS);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.media.transcode).toHaveBeenCalledWith(
@@ -618,9 +622,9 @@ describe(MediaService.name, () => {
});
it('should override reserved color metadata', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStreamReserved);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.media.transcode).toHaveBeenCalledWith(
@@ -638,10 +642,10 @@ describe(MediaService.name, () => {
});
it('should use scaling divisible by 2 even when using quick sync', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
expect(mocks.media.transcode).toHaveBeenCalledWith(
@@ -658,7 +662,7 @@ describe(MediaService.name, () => {
it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => {
const asset = AssetFactory.from().exif().build();
mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.${format}`;
@@ -708,7 +712,7 @@ describe(MediaService.name, () => {
it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => {
const asset = AssetFactory.from().exif().build();
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`;
@@ -760,7 +764,7 @@ describe(MediaService.name, () => {
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { progressive: true }, thumbnail: { progressive: false } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -799,7 +803,7 @@ describe(MediaService.name, () => {
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -834,12 +838,12 @@ describe(MediaService.name, () => {
});
it('should never set isProgressive for videos', async () => {
const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' });
const asset = AssetFactory.from({ type: AssetType.Video, originalPath: '/original/path.ext' }).exif().build();
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
mocks.systemMetadata.get.mockResolvedValue({
image: { preview: { progressive: true }, thumbnail: { progressive: true } },
});
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -860,7 +864,7 @@ describe(MediaService.name, () => {
it('should delete previous thumbnail if different path', async () => {
const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build();
mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -879,7 +883,7 @@ describe(MediaService.name, () => {
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -896,7 +900,7 @@ describe(MediaService.name, () => {
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
.build();
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -910,7 +914,7 @@ describe(MediaService.name, () => {
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -925,7 +929,7 @@ describe(MediaService.name, () => {
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false });
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -941,7 +945,7 @@ describe(MediaService.name, () => {
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
.build();
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -958,7 +962,7 @@ describe(MediaService.name, () => {
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
.build();
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -977,7 +981,7 @@ describe(MediaService.name, () => {
.exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined })
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1018,7 +1022,7 @@ describe(MediaService.name, () => {
});
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1056,7 +1060,7 @@ describe(MediaService.name, () => {
});
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1104,7 +1108,7 @@ describe(MediaService.name, () => {
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1156,7 +1160,7 @@ describe(MediaService.name, () => {
bitsPerSample: 14,
})
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1187,7 +1191,7 @@ describe(MediaService.name, () => {
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg });
mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false });
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1219,7 +1223,7 @@ describe(MediaService.name, () => {
})
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1264,7 +1268,7 @@ describe(MediaService.name, () => {
bitsPerSample: 14,
})
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1303,7 +1307,7 @@ describe(MediaService.name, () => {
bitsPerSample: 14,
})
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await sut.handleGenerateThumbnails({ id: asset.id });
@@ -1338,7 +1342,7 @@ describe(MediaService.name, () => {
it('should skip videos', async () => {
const asset = AssetFactory.from({ type: AssetType.Video }).exif().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success);
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
@@ -1355,7 +1359,7 @@ describe(MediaService.name, () => {
])
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
mocks.person.getFaces.mockResolvedValue([]);
@@ -1377,7 +1381,7 @@ describe(MediaService.name, () => {
.exif()
.edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } })
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
mocks.person.getFaces.mockResolvedValue([]);
mocks.ocr.getByAssetId.mockResolvedValue([]);
@@ -1405,7 +1409,7 @@ describe(MediaService.name, () => {
{ type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true },
])
.build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id });
@@ -1423,7 +1427,7 @@ describe(MediaService.name, () => {
it('should generate all 3 edited files if an asset has edits', async () => {
const asset = AssetFactory.from().exif().edit().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
mocks.person.getFaces.mockResolvedValue([]);
mocks.ocr.getByAssetId.mockResolvedValue([]);
@@ -1449,7 +1453,7 @@ describe(MediaService.name, () => {
it('should generate the original thumbhash if no edits exist', async () => {
const asset = AssetFactory.from().exif().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
mocks.media.generateThumbhash.mockResolvedValue(factory.buffer());
await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' });
@@ -1459,7 +1463,7 @@ describe(MediaService.name, () => {
it('should apply thumbhash if job source is edit and edits exist', async () => {
const asset = AssetFactory.from().exif().edit().build();
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset);
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(getForGenerateThumbnail(asset));
const thumbhashBuffer = factory.buffer();
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
mocks.person.getFaces.mockResolvedValue([]);
@@ -3603,15 +3607,15 @@ describe(MediaService.name, () => {
describe('isSRGB', () => {
it('should return true for srgb colorspace', () => {
expect(sut.isSRGB({ colorspace: 'sRGB' } as Exif)).toEqual(true);
expect(sut.isSRGB({ colorspace: 'sRGB' } as ShallowDehydrateObject<Exif>)).toEqual(true);
});
it('should return true for srgb profile description', () => {
expect(sut.isSRGB({ profileDescription: 'sRGB v1.31' } as Exif)).toEqual(true);
expect(sut.isSRGB({ profileDescription: 'sRGB v1.31' } as ShallowDehydrateObject<Exif>)).toEqual(true);
});
it('should return true for 8-bit image with no colorspace metadata', () => {
expect(sut.isSRGB({ bitsPerSample: 8 } as Exif)).toEqual(true);
expect(sut.isSRGB({ bitsPerSample: 8 } as ShallowDehydrateObject<Exif>)).toEqual(true);
});
it('should return true for image with no colorspace or bit depth metadata', () => {
@@ -3619,23 +3623,25 @@ describe(MediaService.name, () => {
});
it('should return false for non-srgb colorspace', () => {
expect(sut.isSRGB({ colorspace: 'Adobe RGB' } as Exif)).toEqual(false);
expect(sut.isSRGB({ colorspace: 'Adobe RGB' } as ShallowDehydrateObject<Exif>)).toEqual(false);
});
it('should return false for non-srgb profile description', () => {
expect(sut.isSRGB({ profileDescription: 'sP3C' } as Exif)).toEqual(false);
expect(sut.isSRGB({ profileDescription: 'sP3C' } as ShallowDehydrateObject<Exif>)).toEqual(false);
});
it('should return false for 16-bit image with no colorspace metadata', () => {
expect(sut.isSRGB({ bitsPerSample: 16 } as Exif)).toEqual(false);
expect(sut.isSRGB({ bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(false);
});
it('should return true for 16-bit image with sRGB colorspace', () => {
expect(sut.isSRGB({ colorspace: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
expect(sut.isSRGB({ colorspace: 'sRGB', bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(true);
});
it('should return true for 16-bit image with sRGB profile', () => {
expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as ShallowDehydrateObject<Exif>)).toEqual(
true,
);
});
});
+11 -3
View File
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
import { AssetFile, Exif } from 'src/database';
import { AssetFile } from 'src/database';
import { OnEvent, OnJob } from 'src/decorators';
import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
@@ -258,7 +258,7 @@ export class MediaService extends BaseService {
return extracted;
}
private async decodeImage(thumbSource: string | Buffer, exifInfo: Exif, targetSize?: number) {
private async decodeImage(thumbSource: string | Buffer, exifInfo: ThumbnailAsset['exifInfo'], targetSize?: number) {
const { image } = await this.getConfig({ withCache: true });
const colorspace = this.isSRGB(exifInfo) ? Colorspace.Srgb : image.colorspace;
const decodeOptions: DecodeToBufferOptions = {
@@ -754,7 +754,15 @@ export class MediaService extends BaseService {
return name !== VideoContainer.Mp4 && !ffmpegConfig.acceptedContainers.includes(name);
}
isSRGB({ colorspace, profileDescription, bitsPerSample }: Exif): boolean {
isSRGB({
colorspace,
profileDescription,
bitsPerSample,
}: {
colorspace: string | null;
profileDescription: string | null;
bitsPerSample: number | null;
}): boolean {
if (colorspace || profileDescription) {
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
} else if (bitsPerSample) {
+12 -11
View File
@@ -3,6 +3,7 @@ import { MemoryService } from 'src/services/memory.service';
import { OnThisDayData } from 'src/types';
import { AssetFactory } from 'test/factories/asset.factory';
import { MemoryFactory } from 'test/factories/memory.factory';
import { getForMemory } from 'test/mappers';
import { factory, newUuid, newUuids } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -33,7 +34,7 @@ describe(MemoryService.name, () => {
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
const memory2 = MemoryFactory.create({ ownerId: userId });
mocks.memory.search.mockResolvedValue([memory1, memory2]);
mocks.memory.search.mockResolvedValue([getForMemory(memory1), getForMemory(memory2)]);
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
expect.arrayContaining([
@@ -68,7 +69,7 @@ describe(MemoryService.name, () => {
const userId = newUuid();
const memory = MemoryFactory.create({ ownerId: userId });
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.get.mockResolvedValue(getForMemory(memory));
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
await expect(sut.get(factory.auth({ user: { id: userId } }), memory.id)).resolves.toMatchObject({
@@ -85,7 +86,7 @@ describe(MemoryService.name, () => {
const [assetId, userId] = newUuids();
const memory = MemoryFactory.create({ ownerId: userId });
mocks.memory.create.mockResolvedValue(memory);
mocks.memory.create.mockResolvedValue(getForMemory(memory));
await expect(
sut.create(factory.auth({ user: { id: userId } }), {
@@ -115,7 +116,7 @@ describe(MemoryService.name, () => {
const memory = MemoryFactory.from().asset(asset).build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.memory.create.mockResolvedValue(memory);
mocks.memory.create.mockResolvedValue(getForMemory(memory));
await expect(
sut.create(factory.auth({ user: { id: userId } }), {
@@ -135,7 +136,7 @@ describe(MemoryService.name, () => {
it('should create a memory without assets', async () => {
const memory = MemoryFactory.create();
mocks.memory.create.mockResolvedValue(memory);
mocks.memory.create.mockResolvedValue(getForMemory(memory));
await expect(
sut.create(factory.auth(), {
@@ -160,7 +161,7 @@ describe(MemoryService.name, () => {
const memory = MemoryFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.update.mockResolvedValue(memory);
mocks.memory.update.mockResolvedValue(getForMemory(memory));
await expect(sut.update(factory.auth(), memory.id, { isSaved: true })).resolves.toBeDefined();
@@ -203,7 +204,7 @@ describe(MemoryService.name, () => {
const memory = MemoryFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.get.mockResolvedValue(getForMemory(memory));
mocks.memory.getAssetIds.mockResolvedValue(new Set());
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([
@@ -218,7 +219,7 @@ describe(MemoryService.name, () => {
const memory = MemoryFactory.from().asset(asset).build();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.get.mockResolvedValue(getForMemory(memory));
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
@@ -234,8 +235,8 @@ describe(MemoryService.name, () => {
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.update.mockResolvedValue(memory);
mocks.memory.get.mockResolvedValue(getForMemory(memory));
mocks.memory.update.mockResolvedValue(getForMemory(memory));
mocks.memory.getAssetIds.mockResolvedValue(new Set());
mocks.memory.addAssetIds.mockResolvedValue();
@@ -275,7 +276,7 @@ describe(MemoryService.name, () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
mocks.memory.removeAssetIds.mockResolvedValue();
mocks.memory.update.mockResolvedValue(memory);
mocks.memory.update.mockResolvedValue(getForMemory(memory));
await expect(sut.removeAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
{ id: asset.id, success: true },
+76 -72
View File
@@ -19,6 +19,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
import { PersonFactory } from 'test/factories/person.factory';
import { probeStub } from 'test/fixtures/media.stub';
import { tagStub } from 'test/fixtures/tag.stub';
import { getForMetadataExtraction, getForSidecarWrite } from 'test/mappers';
import { factory } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
@@ -176,7 +177,7 @@ describe(MetadataService.name, () => {
const originalDate = new Date('2023-11-21T16:13:17.517Z');
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -198,7 +199,7 @@ describe(MetadataService.name, () => {
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: fileModifiedAt,
@@ -228,7 +229,7 @@ describe(MetadataService.name, () => {
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: fileModifiedAt,
@@ -257,7 +258,7 @@ describe(MetadataService.name, () => {
it('should determine dateTimeOriginal regardless of the server time zone', async () => {
process.env.TZ = 'America/Los_Angeles';
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -277,7 +278,7 @@ describe(MetadataService.name, () => {
it('should handle lists of numbers', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: asset.fileModifiedAt,
@@ -305,7 +306,7 @@ describe(MetadataService.name, () => {
it('should not delete latituide and longitude without reverse geocode', async () => {
// regression test for issue 17511
const asset = AssetFactory.from().exif().build();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
mocks.storage.stat.mockResolvedValue({
size: 123_456,
@@ -337,7 +338,7 @@ describe(MetadataService.name, () => {
it('should apply reverse geocoding', async () => {
const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
mocks.storage.stat.mockResolvedValue({
@@ -367,7 +368,7 @@ describe(MetadataService.name, () => {
it('should discard latitude and longitude on null island', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
GPSLatitude: 0,
GPSLongitude: 0,
@@ -383,7 +384,7 @@ describe(MetadataService.name, () => {
it('should extract tags from TagsList', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
mockReadTags({ TagsList: ['Parent'] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -395,7 +396,7 @@ describe(MetadataService.name, () => {
it('should extract hierarchy from TagsList', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
mockReadTags({ TagsList: ['Parent/Child'] });
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
@@ -417,7 +418,7 @@ describe(MetadataService.name, () => {
it('should extract tags from Keywords as a string', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
mockReadTags({ Keywords: 'Parent' });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -429,7 +430,7 @@ describe(MetadataService.name, () => {
it('should extract tags from Keywords as a list', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] });
mockReadTags({ Keywords: ['Parent'] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -441,7 +442,7 @@ describe(MetadataService.name, () => {
it('should extract tags from Keywords as a list with a number', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
mockReadTags({ Keywords: ['Parent', 2024] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -454,7 +455,7 @@ describe(MetadataService.name, () => {
it('should extract hierarchal tags from Keywords', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] });
mockReadTags({ Keywords: 'Parent/Child' });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -474,7 +475,7 @@ describe(MetadataService.name, () => {
it('should ignore Keywords when TagsList is present', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Child'] });
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -495,7 +496,7 @@ describe(MetadataService.name, () => {
it('should extract hierarchy from HierarchicalSubject', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'TagA'] });
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
@@ -522,7 +523,7 @@ describe(MetadataService.name, () => {
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] });
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -535,7 +536,7 @@ describe(MetadataService.name, () => {
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Mom|Dad'] });
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
@@ -551,7 +552,7 @@ describe(MetadataService.name, () => {
it('should ignore HierarchicalSubject when TagsList is present', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Parent2/Child2'] });
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
@@ -572,7 +573,7 @@ describe(MetadataService.name, () => {
it('should remove existing tags', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({});
await sut.handleMetadataExtraction({ id: asset.id });
@@ -582,7 +583,7 @@ describe(MetadataService.name, () => {
it('should not apply motion photos if asset is video', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
await sut.handleMetadataExtraction({ id: asset.id });
@@ -597,7 +598,7 @@ describe(MetadataService.name, () => {
it('should handle an invalid Directory Item', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
MotionPhoto: 1,
ContainerDirectory: [{ Foo: 100 }],
@@ -608,7 +609,7 @@ describe(MetadataService.name, () => {
it('should extract the correct video orientation', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
mockReadTags({});
@@ -624,7 +625,7 @@ describe(MetadataService.name, () => {
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
const asset = AssetFactory.create();
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: asset.fileModifiedAt,
@@ -686,7 +687,7 @@ describe(MetadataService.name, () => {
mtimeMs: asset.fileModifiedAt.valueOf(),
birthtimeMs: asset.fileCreatedAt.valueOf(),
} as Stats);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
Directory: 'foo/bar/',
EmbeddedVideoFile: new BinaryField(0, ''),
@@ -733,7 +734,7 @@ describe(MetadataService.name, () => {
it('should extract the motion photo video from the XMP directory entry ', async () => {
const asset = AssetFactory.create();
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.storage.stat.mockResolvedValue({
size: 123_456,
mtime: asset.fileModifiedAt,
@@ -786,7 +787,7 @@ describe(MetadataService.name, () => {
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
@@ -808,7 +809,7 @@ describe(MetadataService.name, () => {
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
@@ -832,7 +833,7 @@ describe(MetadataService.name, () => {
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video });
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
@@ -859,7 +860,7 @@ describe(MetadataService.name, () => {
it('should not update storage usage if motion photo is external', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden });
const asset = AssetFactory.create({ isExternal: true });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({
Directory: 'foo/bar/',
MotionPhoto: 1,
@@ -904,7 +905,7 @@ describe(MetadataService.name, () => {
Rating: 3,
};
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags(tags);
await sut.handleMetadataExtraction({ id: asset.id });
@@ -969,7 +970,7 @@ describe(MetadataService.name, () => {
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
zone: undefined,
};
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags(tags);
await sut.handleMetadataExtraction({ id: asset.id });
@@ -984,7 +985,7 @@ describe(MetadataService.name, () => {
it('should extract duration', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
@@ -1007,7 +1008,7 @@ describe(MetadataService.name, () => {
it('should only extract duration for videos', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
@@ -1029,7 +1030,7 @@ describe(MetadataService.name, () => {
it('should omit duration of zero', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
@@ -1052,7 +1053,7 @@ describe(MetadataService.name, () => {
it('should a handle duration of 1 week', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
@@ -1075,7 +1076,7 @@ describe(MetadataService.name, () => {
it('should use Duration from exif', async () => {
const asset = AssetFactory.create({ originalFileName: 'file.webp' });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Duration: 123 }, {});
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1086,7 +1087,7 @@ describe(MetadataService.name, () => {
it('should prefer Duration from exif over sidecar', async () => {
const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Duration: 123 }, { Duration: 456 });
@@ -1098,7 +1099,7 @@ describe(MetadataService.name, () => {
it('should ignore all Duration tags for definitely static images', async () => {
const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Duration: 123 }, { Duration: 456 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1109,7 +1110,7 @@ describe(MetadataService.name, () => {
it('should ignore Duration from exif for videos', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Duration: 123 }, {});
mocks.media.probe.mockResolvedValue({
...probeStub.videoStreamH264,
@@ -1127,7 +1128,7 @@ describe(MetadataService.name, () => {
it('should trim whitespace from description', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Description: '\t \v \f \n \r' });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1150,7 +1151,7 @@ describe(MetadataService.name, () => {
it('should handle a numeric description', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Description: 1000 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1164,7 +1165,7 @@ describe(MetadataService.name, () => {
it('should skip importing metadata when the feature is disabled', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1173,7 +1174,7 @@ describe(MetadataService.name, () => {
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags();
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1182,7 +1183,7 @@ describe(MetadataService.name, () => {
it('should skip importing faces without name', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags(makeFaceTags());
mocks.person.getDistinctNames.mockResolvedValue([]);
@@ -1195,7 +1196,7 @@ describe(MetadataService.name, () => {
it('should skip importing faces with empty name', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags(makeFaceTags({ Name: '' }));
mocks.person.getDistinctNames.mockResolvedValue([]);
@@ -1210,7 +1211,7 @@ describe(MetadataService.name, () => {
const asset = AssetFactory.create();
const person = PersonFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags(makeFaceTags({ Name: person.name }));
mocks.person.getDistinctNames.mockResolvedValue([]);
@@ -1252,7 +1253,7 @@ describe(MetadataService.name, () => {
const asset = AssetFactory.create();
const person = PersonFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags(makeFaceTags({ Name: person.name }));
mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]);
@@ -1339,7 +1340,7 @@ describe(MetadataService.name, () => {
const asset = AssetFactory.create();
const person = PersonFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
mockReadTags(makeFaceTags({ Name: person.name }, orientation));
mocks.person.getDistinctNames.mockResolvedValue([]);
@@ -1383,7 +1384,7 @@ describe(MetadataService.name, () => {
it('should handle invalid modify date', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ ModifyDate: '00:00:00.000' });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1397,7 +1398,7 @@ describe(MetadataService.name, () => {
it('should handle invalid rating value', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Rating: 6 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1411,7 +1412,7 @@ describe(MetadataService.name, () => {
it('should handle valid rating value', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Rating: 5 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1425,7 +1426,7 @@ describe(MetadataService.name, () => {
it('should handle 0 as unrated -> null', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Rating: 0 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1439,7 +1440,7 @@ describe(MetadataService.name, () => {
it('should handle valid negative rating value', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ Rating: -1 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1453,7 +1454,7 @@ describe(MetadataService.name, () => {
it('should handle livePhotoCID not set', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1468,7 +1469,7 @@ describe(MetadataService.name, () => {
it('should handle not finding a match', async () => {
const asset = AssetFactory.create({ type: AssetType.Video });
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ ContentIdentifier: 'CID' });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1490,7 +1491,7 @@ describe(MetadataService.name, () => {
it('should link photo and video', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video });
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
mockReadTags({ ContentIdentifier: 'CID' });
@@ -1518,7 +1519,7 @@ describe(MetadataService.name, () => {
it('should notify clients on live photo link', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video });
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
mockReadTags({ ContentIdentifier: 'CID' });
@@ -1533,7 +1534,7 @@ describe(MetadataService.name, () => {
it('should search by libraryId', async () => {
const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' });
const asset = AssetFactory.create({ libraryId: 'library-id' });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset);
mockReadTags({ ContentIdentifier: 'CID' });
@@ -1570,7 +1571,7 @@ describe(MetadataService.name, () => {
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags(exif);
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1595,7 +1596,7 @@ describe(MetadataService.name, () => {
{ exif: { LensID: '' }, expected: null },
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags(exif);
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1609,7 +1610,7 @@ describe(MetadataService.name, () => {
it('should properly set width/height for normal images', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1623,7 +1624,7 @@ describe(MetadataService.name, () => {
it('should properly swap asset width/height for rotated images', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1637,7 +1638,7 @@ describe(MetadataService.name, () => {
it('should not overwrite existing width/height if they already exist', async () => {
const asset = AssetFactory.create({ width: 1920, height: 1080 });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
await sut.handleMetadataExtraction({ id: asset.id });
@@ -1754,17 +1755,20 @@ describe(MetadataService.name, () => {
it('should skip jobs with no metadata', async () => {
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([]);
const asset = factory.jobAssets.sidecarWrite();
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Skipped);
expect(mocks.metadata.writeTags).not.toHaveBeenCalled();
});
it('should write tags', async () => {
const asset = factory.jobAssets.sidecarWrite();
const description = 'this is a description';
const gps = 12;
const date = '2023-11-21T22:56:12.196-06:00';
const asset = AssetFactory.from()
.file({ type: AssetFileType.Sidecar })
.exif({ description, dateTimeOriginal: new Date(date), latitude: gps, longitude: gps })
.build();
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([
'description',
@@ -1773,7 +1777,7 @@ describe(MetadataService.name, () => {
'dateTimeOriginal',
'timeZone',
]);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
await expect(
sut.handleSidecarWrite({
id: asset.id,
@@ -1796,22 +1800,22 @@ describe(MetadataService.name, () => {
});
it('should write rating', async () => {
const asset = factory.jobAssets.sidecarWrite();
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
asset.exifInfo.rating = 4;
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
});
it('should write null rating as 0', async () => {
const asset = factory.jobAssets.sidecarWrite();
const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).exif().build();
asset.exifInfo.rating = null;
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(getForSidecarWrite(asset));
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
+3 -4
View File
@@ -8,7 +8,7 @@ import { constants } from 'node:fs/promises';
import { join, parse } from 'node:path';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { Asset, AssetFace, AssetFile } from 'src/database';
import { Asset, AssetFile } from 'src/database';
import { OnEvent, OnJob } from 'src/decorators';
import {
AssetFileType,
@@ -447,8 +447,7 @@ export class MetadataService extends BaseService {
const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
{
description: asset.exifInfo.description,
// the kysely type is wrong here; fixed in 0.28.3
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
dateTimeOriginal: asset.exifInfo.dateTimeOriginal,
latitude: asset.exifInfo.latitude,
longitude: asset.exifInfo.longitude,
rating: asset.exifInfo.rating ?? 0,
@@ -829,7 +828,7 @@ export class MetadataService extends BaseService {
}
private async applyTaggedFaces(
asset: { id: string; ownerId: string; faces: AssetFace[]; originalPath: string },
asset: { id: string; ownerId: string; faces: { id: string; sourceType: SourceType }[]; originalPath: string },
tags: ImmichTags,
) {
if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) {
@@ -10,6 +10,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
import { UserFactory } from 'test/factories/user.factory';
import { notificationStub } from 'test/fixtures/notification.stub';
import { userStub } from 'test/fixtures/user.stub';
import { getForAlbum } from 'test/mappers';
import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -269,14 +270,14 @@ describe(NotificationService.name, () => {
});
it('should skip if recipient could not be found', async () => {
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
expect(mocks.job.queue).not.toHaveBeenCalled();
});
it('should skip if the recipient has email notifications disabled', async () => {
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -292,7 +293,7 @@ describe(NotificationService.name, () => {
});
it('should skip if the recipient has email notifications for album invite disabled', async () => {
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -308,7 +309,7 @@ describe(NotificationService.name, () => {
});
it('should send invite email', async () => {
mocks.album.getById.mockResolvedValue(AlbumFactory.create());
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create()));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -331,7 +332,7 @@ describe(NotificationService.name, () => {
it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => {
const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() });
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -363,7 +364,7 @@ describe(NotificationService.name, () => {
it('should send invite email with album thumbnail as jpeg', async () => {
const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail });
const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId });
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -394,8 +395,10 @@ describe(NotificationService.name, () => {
it('should send invite email with album thumbnail and arbitrary extension', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build();
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build();
mocks.album.getById.mockResolvedValue(album);
const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id })
.asset(asset, (builder) => builder.exif())
.build();
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue({
...userStub.user1,
metadata: [
@@ -432,7 +435,7 @@ describe(NotificationService.name, () => {
});
it('should skip if owner could not be found', async () => {
mocks.album.getById.mockResolvedValue(AlbumFactory.create({ ownerId: 'non-existent' }));
mocks.album.getById.mockResolvedValue(getForAlbum(AlbumFactory.create({ ownerId: 'non-existent' })));
await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped);
expect(mocks.systemMetadata.get).not.toHaveBeenCalled();
@@ -440,7 +443,7 @@ describe(NotificationService.name, () => {
it('should skip recipient that could not be looked up', async () => {
const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValueOnce(album.owner);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
@@ -459,7 +462,7 @@ describe(NotificationService.name, () => {
})
.build();
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(user);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
@@ -478,7 +481,7 @@ describe(NotificationService.name, () => {
})
.build();
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(user);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
@@ -492,7 +495,7 @@ describe(NotificationService.name, () => {
it('should send email', async () => {
const user = UserFactory.create();
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
mocks.album.getById.mockResolvedValue(album);
mocks.album.getById.mockResolvedValue(getForAlbum(album));
mocks.user.get.mockResolvedValue(user);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
+40 -26
View File
@@ -1,6 +1,8 @@
import { BadRequestException } from '@nestjs/common';
import { PartnerDirection } from 'src/repositories/partner.repository';
import { PartnerService } from 'src/services/partner.service';
import { UserFactory } from 'test/factories/user.factory';
import { getDehydrated, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -18,26 +20,38 @@ describe(PartnerService.name, () => {
describe('search', () => {
it("should return a list of partners with whom I've shared my library", async () => {
const user1 = factory.user();
const user2 = factory.user();
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const sharedWithUser2 = factory.partner({
sharedBy: getDehydrated(user1),
sharedWith: getDehydrated(user2),
});
const sharedWithUser1 = factory.partner({
sharedBy: getDehydrated(user2),
sharedWith: getDehydrated(user1),
});
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
await expect(sut.search(auth, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
});
it('should return a list of partners who have shared their libraries with me', async () => {
const user1 = factory.user();
const user2 = factory.user();
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const sharedWithUser2 = factory.partner({
sharedBy: getDehydrated(user1),
sharedWith: getDehydrated(user2),
});
const sharedWithUser1 = factory.partner({
sharedBy: getDehydrated(user2),
sharedWith: getDehydrated(user1),
});
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
expect(mocks.partner.getAll).toHaveBeenCalledWith(user1.id);
});
@@ -45,13 +59,13 @@ describe(PartnerService.name, () => {
describe('create', () => {
it('should create a new partner', async () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(void 0);
mocks.partner.create.mockResolvedValue(partner);
mocks.partner.create.mockResolvedValue(getForPartner(partner));
await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined();
@@ -62,12 +76,12 @@ describe(PartnerService.name, () => {
});
it('should throw an error when the partner already exists', async () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(partner);
mocks.partner.get.mockResolvedValue(getForPartner(partner));
await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException);
@@ -77,12 +91,12 @@ describe(PartnerService.name, () => {
describe('remove', () => {
it('should remove a partner', async () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(partner);
mocks.partner.get.mockResolvedValue(getForPartner(partner));
await sut.remove(auth, user2.id);
@@ -110,13 +124,13 @@ describe(PartnerService.name, () => {
});
it('should update partner', async () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const user1 = UserFactory.create();
const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) });
const auth = factory.auth({ user: { id: user1.id } });
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.id]));
mocks.partner.update.mockResolvedValue(partner);
mocks.partner.update.mockResolvedValue(getForPartner(partner));
await expect(sut.update(auth, user2.id, { inTimeline: true })).resolves.toBeDefined();
expect(mocks.partner.update).toHaveBeenCalledWith(
+2 -3
View File
@@ -49,9 +49,8 @@ export class PartnerService extends BaseService {
private mapPartner(partner: Partner, direction: PartnerDirection): PartnerResponseDto {
// this is opposite to return the non-me user of the "partner"
const user = mapUser(
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
) as PartnerResponseDto;
const sharedUser = direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy;
const user = mapUser(sharedUser);
return { ...user, inTimeline: partner.inTimeline };
}
+32 -28
View File
@@ -12,7 +12,7 @@ import { PersonFactory } from 'test/factories/person.factory';
import { UserFactory } from 'test/factories/user.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { getAsDetectedFace, getForFacialRecognitionJob } from 'test/mappers';
import { getAsDetectedFace, getForAssetFace, getForDetectedFaces, getForFacialRecognitionJob } from 'test/mappers';
import { newDate, newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
@@ -202,16 +202,16 @@ describe(PersonService.name, () => {
mocks.person.update.mockResolvedValue(person);
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
await expect(sut.update(auth, person.id, { birthDate: new Date('1976-06-30') })).resolves.toEqual({
await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({
id: person.id,
name: person.name,
birthDate: '1976-06-30',
thumbnailPath: person.thumbnailPath,
isHidden: false,
isFavorite: false,
updatedAt: expect.any(Date),
updatedAt: expect.any(String),
});
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: new Date('1976-06-30') });
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' });
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
@@ -319,7 +319,7 @@ describe(PersonService.name, () => {
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
mocks.person.getById.mockResolvedValue(person);
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
mocks.person.getFacesByIds.mockResolvedValue([face]);
mocks.person.getFacesByIds.mockResolvedValue([getForAssetFace(face)]);
mocks.person.reassignFace.mockResolvedValue(1);
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
mocks.person.refreshFaces.mockResolvedValue();
@@ -353,15 +353,17 @@ describe(PersonService.name, () => {
const face = AssetFaceFactory.create();
const asset = AssetFactory.from({ id: face.assetId }).exif().build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.person.getFaces.mockResolvedValue([face]);
mocks.person.getFaces.mockResolvedValue([getForAssetFace(face)]);
mocks.asset.getForFaces.mockResolvedValue({ edits: [], ...asset.exifInfo });
await expect(sut.getFacesById(auth, { id: face.assetId })).resolves.toStrictEqual([mapFaces(face, auth)]);
await expect(sut.getFacesById(auth, { id: face.assetId })).resolves.toStrictEqual([
mapFaces(getForAssetFace(face), auth),
]);
});
it('should reject if the user has not access to the asset', async () => {
const face = AssetFaceFactory.create();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
mocks.person.getFaces.mockResolvedValue([face]);
mocks.person.getFaces.mockResolvedValue([getForAssetFace(face)]);
await expect(sut.getFacesById(AuthFactory.create(), { id: face.assetId })).rejects.toBeInstanceOf(
BadRequestException,
);
@@ -390,7 +392,7 @@ describe(PersonService.name, () => {
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
mocks.person.getFaceById.mockResolvedValue(face);
mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face));
mocks.person.reassignFace.mockResolvedValue(1);
mocks.person.getById.mockResolvedValue(person);
await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({
@@ -400,7 +402,7 @@ describe(PersonService.name, () => {
id: person.id,
name: person.name,
thumbnailPath: person.thumbnailPath,
updatedAt: expect.any(Date),
updatedAt: expect.any(String),
});
expect(mocks.job.queue).not.toHaveBeenCalledWith();
@@ -412,7 +414,7 @@ describe(PersonService.name, () => {
const person = PersonFactory.create();
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
mocks.person.getFaceById.mockResolvedValue(face);
mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face));
mocks.person.reassignFace.mockResolvedValue(1);
mocks.person.getById.mockResolvedValue(person);
await expect(
@@ -735,18 +737,18 @@ describe(PersonService.name, () => {
});
it('should skip when no resize path', async () => {
const asset = AssetFactory.create();
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
const asset = AssetFactory.from().exif().build();
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
await sut.handleDetectFaces({ id: asset.id });
expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled();
});
it('should handle no results', async () => {
const start = Date.now();
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
mocks.machineLearning.detectFaces.mockResolvedValue({ imageHeight: 500, imageWidth: 400, faces: [] });
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
await sut.handleDetectFaces({ id: asset.id });
expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith(
asset.files[0].path,
@@ -764,12 +766,12 @@ describe(PersonService.name, () => {
});
it('should create a face with no person and queue recognition job', async () => {
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build();
const face = AssetFaceFactory.create({ assetId: asset.id });
mocks.crypto.randomUUID.mockReturnValue(face.id);
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
mocks.person.refreshFaces.mockResolvedValue();
await sut.handleDetectFaces({ id: asset.id });
@@ -788,9 +790,9 @@ describe(PersonService.name, () => {
});
it('should delete an existing face not among the new detected faces', async () => {
const asset = AssetFactory.from().face().file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from().face().file({ type: AssetFileType.Preview }).exif().build();
mocks.machineLearning.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 });
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
await sut.handleDetectFaces({ id: asset.id });
@@ -809,9 +811,9 @@ describe(PersonService.name, () => {
boundingBoxY1: 200,
boundingBoxY2: 300,
});
const asset = AssetFactory.from({ id: assetId }).face().file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from({ id: assetId }).face().file({ type: AssetFileType.Preview }).exif().build();
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
mocks.crypto.randomUUID.mockReturnValue(face.id);
mocks.person.refreshFaces.mockResolvedValue();
@@ -832,9 +834,9 @@ describe(PersonService.name, () => {
it('should add embedding to matching metadata face', async () => {
const face = AssetFaceFactory.create({ sourceType: SourceType.Exif });
const asset = AssetFactory.from().face(face).file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from().face(face).file({ type: AssetFileType.Preview }).exif().build();
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
mocks.person.refreshFaces.mockResolvedValue();
await sut.handleDetectFaces({ id: asset.id });
@@ -848,9 +850,9 @@ describe(PersonService.name, () => {
it('should not add embedding to non-matching metadata face', async () => {
const assetId = newUuid();
const face = AssetFaceFactory.create({ assetId, sourceType: SourceType.Exif });
const asset = AssetFactory.from({ id: assetId }).file({ type: AssetFileType.Preview }).build();
const asset = AssetFactory.from({ id: assetId }).file({ type: AssetFileType.Preview }).exif().build();
mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face));
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset);
mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset));
mocks.crypto.randomUUID.mockReturnValue(face.id);
await sut.handleDetectFaces({ id: asset.id });
@@ -1237,7 +1239,7 @@ describe(PersonService.name, () => {
const person = PersonFactory.create({ ownerId: user.id });
const face = AssetFaceFactory.from().person(person).build();
expect(mapFaces(face, auth)).toEqual({
expect(mapFaces(getForAssetFace(face), auth)).toEqual({
boundingBoxX1: 100,
boundingBoxX2: 200,
boundingBoxY1: 100,
@@ -1251,11 +1253,13 @@ describe(PersonService.name, () => {
});
it('should not map person if person is null', () => {
expect(mapFaces(AssetFaceFactory.create(), AuthFactory.create()).person).toBeNull();
expect(mapFaces(getForAssetFace(AssetFaceFactory.create()), AuthFactory.create()).person).toBeNull();
});
it('should not map person if person does not match auth user id', () => {
expect(mapFaces(AssetFaceFactory.from().person().build(), AuthFactory.create()).person).toBeNull();
expect(
mapFaces(getForAssetFace(AssetFaceFactory.from().person().build()), AuthFactory.create()).person,
).toBeNull();
});
});
});
+2 -2
View File
@@ -491,7 +491,7 @@ export class PersonService extends BaseService {
embedding: face.faceSearch.embedding,
maxDistance: machineLearning.facialRecognition.maxDistance,
numResults: machineLearning.facialRecognition.minFaces,
minBirthDate: face.asset.fileCreatedAt ?? undefined,
minBirthDate: new Date(face.asset.fileCreatedAt),
});
// `matches` also includes the face itself
@@ -519,7 +519,7 @@ export class PersonService extends BaseService {
maxDistance: machineLearning.facialRecognition.maxDistance,
numResults: 1,
hasPerson: true,
minBirthDate: face.asset.fileCreatedAt ?? undefined,
minBirthDate: new Date(face.asset.fileCreatedAt),
});
if (matchWithPerson.length > 0) {
+4 -1
View File
@@ -5,6 +5,7 @@ import { SearchService } from 'src/services/search.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset } from 'test/mappers';
import { newTestService, ServiceMocks } from 'test/utils';
import { beforeEach, vitest } from 'vitest';
@@ -74,7 +75,9 @@ describe(SearchService.name, () => {
items: [{ value: 'city', data: asset.id }],
});
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]);
const expectedResponse = [{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(asset) }] }];
const expectedResponse = [
{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(getForAsset(asset)) }] },
];
const result = await sut.getExploreData(auth);
+71 -36
View File
@@ -1,12 +1,14 @@
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { mapSharedLink } from 'src/dtos/shared-link.dto';
import { SharedLinkType } from 'src/enum';
import { SharedLinkService } from 'src/services/shared-link.service';
import { AlbumFactory } from 'test/factories/album.factory';
import { AssetFactory } from 'test/factories/asset.factory';
import { SharedLinkFactory } from 'test/factories/shared-link.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { getForSharedLink } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -24,11 +26,13 @@ describe(SharedLinkService.name, () => {
describe('getAll', () => {
it('should return all shared links for a user', async () => {
mocks.sharedLink.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([
sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid,
]);
const [sharedLink1, sharedLink2] = [SharedLinkFactory.create(), SharedLinkFactory.create()];
mocks.sharedLink.getAll.mockResolvedValue([getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)]);
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual(
[getForSharedLink(sharedLink1), getForSharedLink(sharedLink2)].map((link) =>
mapSharedLink(link, { stripAssetMetadata: false }),
),
);
expect(mocks.sharedLink.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
});
});
@@ -41,8 +45,11 @@ describe(SharedLinkService.name, () => {
it('should return the shared link for the public user', async () => {
const authDto = authStub.adminSharedLink;
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid);
const sharedLink = SharedLinkFactory.create();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
await expect(sut.getMine(authDto, [])).resolves.toEqual(
mapSharedLink(getForSharedLink(sharedLink), { stripAssetMetadata: false }),
);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
});
@@ -54,7 +61,13 @@ describe(SharedLinkService.name, () => {
allowUpload: true,
},
});
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
mocks.sharedLink.get.mockResolvedValue(
getForSharedLink(
SharedLinkFactory.from({ showExif: false })
.asset({}, (builder) => builder.exif())
.build(),
),
);
const response = await sut.getMine(authDto, []);
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
@@ -68,7 +81,8 @@ describe(SharedLinkService.name, () => {
});
it('should accept a valid shared link auth token', async () => {
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
const sharedLink = SharedLinkFactory.create({ password: '123' });
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
const secret = Buffer.from('auth-token-123');
mocks.crypto.hashSha256.mockReturnValue(secret);
await expect(sut.getMine(authStub.adminSharedLink, [secret.toString('base64')])).resolves.toBeDefined();
@@ -90,9 +104,12 @@ describe(SharedLinkService.name, () => {
});
it('should get a shared link by id', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
const sharedLink = SharedLinkFactory.create();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
await expect(sut.get(authStub.user1, sharedLink.id)).resolves.toEqual(
mapSharedLink(getForSharedLink(sharedLink), { stripAssetMetadata: true }),
);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLink.id);
});
});
@@ -123,8 +140,9 @@ describe(SharedLinkService.name, () => {
it('should create an album shared link', async () => {
const album = AlbumFactory.from().asset().build();
const sharedLink = SharedLinkFactory.from().album(album).build();
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid);
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: album.id });
@@ -145,8 +163,11 @@ describe(SharedLinkService.name, () => {
it('should create an individual shared link', async () => {
const asset = AssetFactory.create();
const sharedLink = SharedLinkFactory.from()
.asset(asset, (builder) => builder.exif())
.build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
await sut.create(authStub.admin, {
type: SharedLinkType.Individual,
@@ -178,8 +199,11 @@ describe(SharedLinkService.name, () => {
it('should create a shared link with allowDownload set to false when showMetadata is false', async () => {
const asset = AssetFactory.create();
const sharedLink = SharedLinkFactory.from({ allowDownload: false })
.asset(asset, (builder) => builder.exif())
.build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
await sut.create(authStub.admin, {
type: SharedLinkType.Individual,
@@ -221,8 +245,9 @@ describe(SharedLinkService.name, () => {
});
it('should update a shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
const sharedLink = SharedLinkFactory.create();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
@@ -247,19 +272,21 @@ describe(SharedLinkService.name, () => {
});
it('should remove a key', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
const sharedLink = SharedLinkFactory.create();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.remove.mockResolvedValue();
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
await sut.remove(authStub.user1, sharedLink.id);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid.id);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLink.id);
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLink.id);
});
});
describe('addAssets', () => {
it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
const sharedLink = SharedLinkFactory.from().album().build();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
@@ -268,11 +295,13 @@ describe(SharedLinkService.name, () => {
it('should add assets to a shared link', async () => {
const asset = AssetFactory.create();
const sharedLink = SharedLinkFactory.from().asset(asset).build();
const sharedLink = SharedLinkFactory.from()
.asset(asset, (builder) => builder.exif())
.build();
const newAsset = AssetFactory.create();
mocks.sharedLink.get.mockResolvedValue(sharedLink);
mocks.sharedLink.create.mockResolvedValue(sharedLink);
mocks.sharedLink.update.mockResolvedValue(sharedLink);
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id]));
await expect(
@@ -286,7 +315,7 @@ describe(SharedLinkService.name, () => {
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
expect(mocks.sharedLink.update).toHaveBeenCalled();
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
...sharedLink,
...getForSharedLink(sharedLink),
slug: null,
assetIds: [newAsset.id],
});
@@ -295,19 +324,22 @@ describe(SharedLinkService.name, () => {
describe('removeAssets', () => {
it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
const sharedLink = SharedLinkFactory.from().album().build();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
await expect(sut.removeAssets(authStub.admin, sharedLink.id, { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
);
});
it('should remove assets from a shared link', async () => {
const asset = AssetFactory.create();
const sharedLink = SharedLinkFactory.from().asset(asset).build();
mocks.sharedLink.get.mockResolvedValue(sharedLink);
mocks.sharedLink.create.mockResolvedValue(sharedLink);
mocks.sharedLink.update.mockResolvedValue(sharedLink);
const sharedLink = SharedLinkFactory.from()
.asset(asset, (builder) => builder.exif())
.build();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.create.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLink.update.mockResolvedValue(getForSharedLink(sharedLink));
mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]);
await expect(
@@ -338,11 +370,14 @@ describe(SharedLinkService.name, () => {
});
it('should return metadata tags', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual);
const sharedLink = SharedLinkFactory.from({ description: null })
.asset({}, (builder) => builder.exif())
.build();
mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink));
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '1 shared photos & videos',
imageUrl: `https://my.immich.app/api/assets/${sharedLinkStub.individual.assets[0].id}/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
imageUrl: `https://my.immich.app/api/assets/${sharedLink.assets[0].id}/thumbnail?key=${sharedLink.key.toString('base64url')}`,
title: 'Public Share',
});
+30 -15
View File
@@ -4,6 +4,7 @@ import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { StackFactory } from 'test/factories/stack.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForStack } from 'test/mappers';
import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -22,9 +23,11 @@ describe(StackService.name, () => {
describe('search', () => {
it('should search stacks', async () => {
const auth = AuthFactory.create();
const asset = AssetFactory.create();
const stack = StackFactory.from().primaryAsset(asset).build();
mocks.stack.search.mockResolvedValue([stack]);
const asset = AssetFactory.from().exif().build();
const stack = StackFactory.from()
.primaryAsset(asset, (builder) => builder.exif())
.build();
mocks.stack.search.mockResolvedValue([getForStack(stack)]);
await sut.search(auth, { primaryAssetId: asset.id });
expect(mocks.stack.search).toHaveBeenCalledWith({
@@ -49,11 +52,14 @@ describe(StackService.name, () => {
it('should create a stack', async () => {
const auth = AuthFactory.create();
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
const stack = StackFactory.from()
.primaryAsset(primaryAsset, (builder) => builder.exif())
.asset(asset, (builder) => builder.exif())
.build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id]));
mocks.stack.create.mockResolvedValue(stack);
mocks.stack.create.mockResolvedValue(getForStack(stack));
await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({
id: stack.id,
@@ -88,11 +94,14 @@ describe(StackService.name, () => {
it('should get stack', async () => {
const auth = AuthFactory.create();
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
const stack = StackFactory.from()
.primaryAsset(primaryAsset, (builder) => builder.exif())
.asset(asset, (builder) => builder.exif())
.build();
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
mocks.stack.getById.mockResolvedValue(stack);
mocks.stack.getById.mockResolvedValue(getForStack(stack));
await expect(sut.get(auth, stack.id)).resolves.toEqual({
id: stack.id,
@@ -125,10 +134,13 @@ describe(StackService.name, () => {
it('should fail if the provided primary asset id is not in the stack', async () => {
const auth = AuthFactory.create();
const stack = StackFactory.from().primaryAsset().asset().build();
const stack = StackFactory.from()
.primaryAsset({}, (builder) => builder.exif())
.asset({}, (builder) => builder.exif())
.build();
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
mocks.stack.getById.mockResolvedValue(stack);
mocks.stack.getById.mockResolvedValue(getForStack(stack));
await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf(
BadRequestException,
@@ -141,12 +153,15 @@ describe(StackService.name, () => {
it('should update stack', async () => {
const auth = AuthFactory.create();
const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()];
const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build();
const [primaryAsset, asset] = [AssetFactory.from().exif().build(), AssetFactory.from().exif().build()];
const stack = StackFactory.from()
.primaryAsset(primaryAsset, (builder) => builder.exif())
.asset(asset, (builder) => builder.exif())
.build();
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id]));
mocks.stack.getById.mockResolvedValue(stack);
mocks.stack.update.mockResolvedValue(stack);
mocks.stack.getById.mockResolvedValue(getForStack(stack));
mocks.stack.update.mockResolvedValue(getForStack(stack));
await sut.update(auth, stack.id, { primaryAssetId: asset.id });
@@ -6,7 +6,7 @@ import { AlbumFactory } from 'test/factories/album.factory';
import { AssetFactory } from 'test/factories/asset.factory';
import { UserFactory } from 'test/factories/user.factory';
import { userStub } from 'test/fixtures/user.stub';
import { getForStorageTemplate } from 'test/mappers';
import { getForAlbum, getForStorageTemplate } from 'test/mappers';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build();
@@ -170,7 +170,9 @@ describe(StorageTemplateService.name, () => {
.exif()
.build();
const album = AlbumFactory.from().asset().build();
const album = AlbumFactory.from()
.asset({}, (builder) => builder.exif())
.build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
sut.onConfigInit({ newConfig: config });
@@ -182,7 +184,7 @@ describe(StorageTemplateService.name, () => {
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset));
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
mocks.album.getByAssetId.mockResolvedValue([album]);
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
mocks.move.create.mockResolvedValueOnce({
id: '123',
@@ -211,7 +213,9 @@ describe(StorageTemplateService.name, () => {
it('should use handlebar if condition for album', async () => {
const user = UserFactory.create();
const asset = AssetFactory.from().owner(user).exif().build();
const album = AlbumFactory.from().asset().build();
const album = AlbumFactory.from()
.asset({}, (builder) => builder.exif())
.build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
@@ -219,7 +223,7 @@ describe(StorageTemplateService.name, () => {
mocks.user.get.mockResolvedValue(user);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
mocks.album.getByAssetId.mockResolvedValueOnce([getForAlbum(album)]);
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success);
@@ -259,7 +263,9 @@ describe(StorageTemplateService.name, () => {
it('should handle album startDate', async () => {
const user = UserFactory.create();
const asset = AssetFactory.from().owner(user).exif().build();
const album = AlbumFactory.from().asset().build();
const album = AlbumFactory.from()
.asset({}, (builder) => builder.exif())
.build();
const config = structuredClone(defaults);
config.storageTemplate.template =
'{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}';
@@ -268,7 +274,7 @@ describe(StorageTemplateService.name, () => {
mocks.user.get.mockResolvedValue(user);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset));
mocks.album.getByAssetId.mockResolvedValueOnce([album]);
mocks.album.getByAssetId.mockResolvedValueOnce([getForAlbum(album)]);
mocks.album.getMetadataForIds.mockResolvedValueOnce([
{
startDate: asset.fileCreatedAt,
@@ -764,7 +770,9 @@ describe(StorageTemplateService.name, () => {
})
.exif()
.build();
const album = AlbumFactory.from().asset().build();
const album = AlbumFactory.from()
.asset({}, (builder) => builder.exif())
.build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
sut.onConfigInit({ newConfig: config });
@@ -775,7 +783,7 @@ describe(StorageTemplateService.name, () => {
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
mocks.user.getList.mockResolvedValue([userStub.user1]);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
mocks.album.getByAssetId.mockResolvedValue([album]);
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
mocks.move.create.mockResolvedValueOnce({
id: '123',
@@ -803,7 +811,9 @@ describe(StorageTemplateService.name, () => {
it('should use still photo album info when migrating live photo motion video', async () => {
const user = userStub.user1;
const album = AlbumFactory.from().asset().build();
const album = AlbumFactory.from()
.asset({}, (builder) => builder.exif())
.build();
const config = structuredClone(defaults);
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other{{/if}}/{{filename}}';
@@ -812,7 +822,7 @@ describe(StorageTemplateService.name, () => {
mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)]));
mocks.user.getList.mockResolvedValue([user]);
mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset));
mocks.album.getByAssetId.mockResolvedValue([album]);
mocks.album.getByAssetId.mockResolvedValue([getForAlbum(album)]);
mocks.move.create.mockResolvedValueOnce({
id: '123',
+10 -7
View File
@@ -2,6 +2,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { SyncService } from 'src/services/sync.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -26,10 +27,10 @@ describe(SyncService.name, () => {
AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(),
AssetFactory.from().owner(authStub.user1.user).build(),
];
mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]);
mocks.asset.getAllForUserFullSync.mockResolvedValue([getForAsset(asset1), getForAsset(asset2)]);
await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
mapAsset(asset1, mapAssetOpts),
mapAsset(asset2, mapAssetOpts),
mapAsset(getForAsset(asset1), mapAssetOpts),
mapAsset(getForAsset(asset2), mapAssetOpts),
]);
expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({
ownerId: authStub.user1.user.id,
@@ -44,7 +45,7 @@ describe(SyncService.name, () => {
const partner = factory.partner();
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.partner.getAll.mockResolvedValue([partner]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [auth.user.id] }),
@@ -66,7 +67,9 @@ describe(SyncService.name, () => {
it('should return a response requiring a full sync when there are too many changes', async () => {
const asset = AssetFactory.create();
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getChangedDeltaSync.mockResolvedValue(Array.from<typeof asset>({ length: 10_000 }).fill(asset));
mocks.asset.getChangedDeltaSync.mockResolvedValue(
Array.from<ReturnType<typeof getForAsset>>({ length: 10_000 }).fill(getForAsset(asset)),
);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
@@ -78,13 +81,13 @@ describe(SyncService.name, () => {
const asset = AssetFactory.create({ ownerId: authStub.user1.user.id });
const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true });
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]);
mocks.asset.getChangedDeltaSync.mockResolvedValue([getForAsset(asset)]);
mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]);
await expect(
sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({
needsFullSync: false,
upserted: [mapAsset(asset, mapAssetOpts)],
upserted: [mapAsset(getForAsset(asset), mapAssetOpts)],
deleted: [deletedAsset.id],
});
expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1);
+2 -1
View File
@@ -2,6 +2,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { ViewService } from 'src/services/view.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset } from 'test/mappers';
import { newTestService, ServiceMocks } from 'test/utils';
describe(ViewService.name, () => {
@@ -37,7 +38,7 @@ describe(ViewService.name, () => {
const mockAssets = [asset1, asset2];
const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin }));
const mockAssetReponseDto = mockAssets.map((asset) => mapAsset(getForAsset(asset), { auth: authStub.admin }));
mocks.view.getAssetsByOriginalPath.mockResolvedValue(mockAssets as any);