1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

chore: asset service unit tests (#13179)

This commit is contained in:
Daniel Dietzler 2024-10-04 19:37:18 +02:00 committed by GitHub
parent bb3b4c8086
commit 4adedea128
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,13 +1,15 @@
import { BadRequestException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum';
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetService } from 'src/services/asset.service';
import { assetStub } from 'test/fixtures/asset.stub';
@ -41,6 +43,7 @@ describe(AssetService.name, () => {
let jobMock: Mocked<IJobRepository>;
let partnerMock: Mocked<IPartnerRepository>;
let stackMock: Mocked<IStackRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let userMock: Mocked<IUserRepository>;
it('should work', () => {
@ -54,7 +57,7 @@ describe(AssetService.name, () => {
};
beforeEach(() => {
({ sut, accessMock, assetMock, eventMock, jobMock, userMock, partnerMock, stackMock } =
({ sut, accessMock, assetMock, eventMock, jobMock, partnerMock, stackMock, systemMock, userMock } =
newTestService(AssetService));
mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
@ -126,6 +129,28 @@ describe(AssetService.name, () => {
});
});
describe('getRandom', () => {
it('should get own random assets', async () => {
assetMock.getRandom.mockResolvedValue([assetStub.image]);
await sut.getRandom(authStub.admin, 1);
expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
});
it('should not include partner assets if not in timeline', async () => {
assetMock.getRandom.mockResolvedValue([assetStub.image]);
partnerMock.getAll.mockResolvedValue([{ ...partnerStub.user1ToAdmin1, inTimeline: false }]);
await sut.getRandom(authStub.admin, 1);
expect(assetMock.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
});
it('should include partner assets if in timeline', async () => {
assetMock.getRandom.mockResolvedValue([assetStub.image]);
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
await sut.getRandom(authStub.admin, 1);
expect(assetMock.getRandom).toHaveBeenCalledWith([userStub.admin.id, userStub.user1.id], 1);
});
});
describe('get', () => {
it('should allow owner access', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
@ -147,6 +172,23 @@ describe(AssetService.name, () => {
);
});
it('should strip metadata for shared link if exif is disabled', async () => {
accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
const result = await sut.get(
{ ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
assetStub.image.id,
);
expect(result).toEqual(expect.objectContaining({ hasMetadata: false }));
expect(result).not.toHaveProperty('exifInfo');
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLink?.id,
new Set([assetStub.image.id]),
);
});
it('should allow partner sharing access', async () => {
accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(assetStub.image);
@ -177,6 +219,11 @@ describe(AssetService.name, () => {
expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled();
expect(assetMock.getById).not.toHaveBeenCalled();
});
it('should throw an error if the asset could not be found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('update', () => {
@ -207,6 +254,132 @@ describe(AssetService.name, () => {
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
});
it('should fail linking a live video if the motion part could not be found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(null);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
});
it('should fail linking a live video if the motion part is not a video', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
});
it('should fail linking a live video if the motion part has a different owner', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(assetMock.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(eventMock.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
});
it('should link a live video', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset,
ownerId: authStub.admin.user.id,
isVisible: true,
});
assetMock.getById.mockResolvedValueOnce(assetStub.image);
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
expect(eventMock.emit).toHaveBeenCalledWith('asset.hide', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
expect(assetMock.update).toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
});
it('should throw an error if asset could not be found after update', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf(
BadRequestException,
);
});
it('should unlink a live video', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetMock.getById.mockResolvedValueOnce(assetStub.image);
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
expect(assetMock.update).toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: null,
});
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(eventMock.emit).toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
});
});
it('should fail unlinking a live video if the asset could not be found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(null);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }),
).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.update).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
expect(eventMock.emit).not.toHaveBeenCalledWith();
});
});
describe('updateAll', () => {
@ -259,6 +432,42 @@ describe(AssetService.name, () => {
});
});
describe('handleAssetDeletionCheck', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('should immediately queue assets for deletion if trash is disabled', async () => {
assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
systemMock.get.mockResolvedValue({ trash: { enabled: false } });
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), { trashedBefore: new Date() });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
]);
});
it('should queue assets for deletion after trash duration', async () => {
assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
systemMock.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.getAll).toHaveBeenCalledWith(expect.anything(), {
trashedBefore: DateTime.now().minus({ days: 7 }).toJSDate(),
});
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } },
]);
});
});
describe('handleAssetDeletion', () => {
it('should remove faces', async () => {
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
@ -298,6 +507,17 @@ describe(AssetService.name, () => {
});
});
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
assetMock.getById.mockResolvedValue({
...assetStub.primaryImage,
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
} as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
});
it('should delete a live photo', async () => {
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
assetMock.getLivePhotoCount.mockResolvedValue(0);
@ -354,6 +574,12 @@ describe(AssetService.name, () => {
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
});
it('should fail if asset could not be found', async () => {
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
JobStatus.FAILED,
);
});
});
describe('run', () => {