Skip to content

Commit 2c8c365

Browse files
chore: some more unit tests :) (immich-app#13159)
1 parent db1623f commit 2c8c365

File tree

3 files changed

+304
-9
lines changed

3 files changed

+304
-9
lines changed

server/src/services/album.service.spec.ts

+24
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,17 @@ describe(AlbumService.name, () => {
306306
expect(albumMock.update).not.toHaveBeenCalled();
307307
});
308308

309+
it('should throw an error if the userId is the ownerId', async () => {
310+
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
311+
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
312+
await expect(
313+
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
314+
albumUsers: [{ userId: userStub.user1.id }],
315+
}),
316+
).rejects.toBeInstanceOf(BadRequestException);
317+
expect(albumMock.update).not.toHaveBeenCalled();
318+
});
319+
309320
it('should add valid shared users', async () => {
310321
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
311322
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
@@ -415,6 +426,19 @@ describe(AlbumService.name, () => {
415426
});
416427
});
417428

429+
describe('updateUser', () => {
430+
it('should update user role', async () => {
431+
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
432+
await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
433+
role: AlbumUserRole.EDITOR,
434+
});
435+
expect(albumUserMock.update).toHaveBeenCalledWith(
436+
{ albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id },
437+
{ role: AlbumUserRole.EDITOR },
438+
);
439+
});
440+
});
441+
418442
describe('getAlbumInfo', () => {
419443
it('should get a shared album', async () => {
420444
albumMock.getById.mockResolvedValue(albumStub.oneAsset);

server/src/services/asset-media.service.spec.ts

+280-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common';
1+
import {
2+
BadRequestException,
3+
InternalServerErrorException,
4+
NotFoundException,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
27
import { Stats } from 'node:fs';
38
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
4-
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
9+
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
510
import { AssetFileEntity } from 'src/entities/asset-files.entity';
611
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
7-
import { AssetStatus, AssetType, CacheControl } from 'src/enum';
12+
import { AssetFileType, AssetStatus, AssetType, CacheControl } from 'src/enum';
813
import { IAssetRepository } from 'src/interfaces/asset.interface';
914
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
1015
import { IStorageRepository } from 'src/interfaces/storage.interface';
@@ -14,6 +19,7 @@ import { ImmichFileResponse } from 'src/utils/file';
1419
import { assetStub } from 'test/fixtures/asset.stub';
1520
import { authStub } from 'test/fixtures/auth.stub';
1621
import { fileStub } from 'test/fixtures/file.stub';
22+
import { userStub } from 'test/fixtures/user.stub';
1723
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
1824
import { newTestService } from 'test/utils';
1925
import { QueryFailedError } from 'typeorm';
@@ -194,6 +200,10 @@ describe(AssetMediaService.name, () => {
194200
});
195201

196202
describe('getUploadAssetIdByChecksum', () => {
203+
it('should return if checksum is undefined', async () => {
204+
await expect(sut.getUploadAssetIdByChecksum(authStub.admin)).resolves.toBe(undefined);
205+
});
206+
197207
it('should handle a non-existent asset', async () => {
198208
await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('hex'))).resolves.toBeUndefined();
199209
expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
@@ -295,6 +305,35 @@ describe(AssetMediaService.name, () => {
295305
});
296306

297307
describe('uploadAsset', () => {
308+
it('should throw an error if the quota is exceeded', async () => {
309+
const file = {
310+
uuid: 'random-uuid',
311+
originalPath: 'fake_path/asset_1.jpeg',
312+
mimeType: 'image/jpeg',
313+
checksum: Buffer.from('file hash', 'utf8'),
314+
originalName: 'asset_1.jpeg',
315+
size: 42,
316+
};
317+
318+
assetMock.create.mockResolvedValue(assetEntity);
319+
320+
await expect(
321+
sut.uploadAsset(
322+
{ ...authStub.admin, user: { ...authStub.admin.user, quotaSizeInBytes: 42, quotaUsageInBytes: 1 } },
323+
createDto,
324+
file,
325+
),
326+
).rejects.toBeInstanceOf(BadRequestException);
327+
328+
expect(assetMock.create).not.toHaveBeenCalled();
329+
expect(userMock.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
330+
expect(storageMock.utimes).not.toHaveBeenCalledWith(
331+
file.originalPath,
332+
expect.any(Date),
333+
new Date(createDto.fileModifiedAt),
334+
);
335+
});
336+
298337
it('should handle a file upload', async () => {
299338
const file = {
300339
uuid: 'random-uuid',
@@ -348,6 +387,31 @@ describe(AssetMediaService.name, () => {
348387
expect(userMock.updateUsage).not.toHaveBeenCalled();
349388
});
350389

390+
it('should throw an error if the duplicate could not be found by checksum', async () => {
391+
const file = {
392+
uuid: 'random-uuid',
393+
originalPath: 'fake_path/asset_1.jpeg',
394+
mimeType: 'image/jpeg',
395+
checksum: Buffer.from('file hash', 'utf8'),
396+
originalName: 'asset_1.jpeg',
397+
size: 0,
398+
};
399+
const error = new QueryFailedError('', [], new Error('unique key violation'));
400+
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
401+
402+
assetMock.create.mockRejectedValue(error);
403+
404+
await expect(sut.uploadAsset(authStub.user1, createDto, file)).rejects.toBeInstanceOf(
405+
InternalServerErrorException,
406+
);
407+
408+
expect(jobMock.queue).toHaveBeenCalledWith({
409+
name: JobName.DELETE_FILES,
410+
data: { files: ['fake_path/asset_1.jpeg', undefined] },
411+
});
412+
expect(userMock.updateUsage).not.toHaveBeenCalled();
413+
});
414+
351415
it('should handle a live photo', async () => {
352416
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
353417
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
@@ -385,6 +449,23 @@ describe(AssetMediaService.name, () => {
385449
expect(assetMock.getById).toHaveBeenCalledWith('live-photo-motion-asset');
386450
expect(assetMock.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
387451
});
452+
453+
it('should handle a sidecar file', async () => {
454+
assetMock.getById.mockResolvedValueOnce(assetStub.image);
455+
assetMock.create.mockResolvedValueOnce(assetStub.image);
456+
457+
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
458+
status: AssetMediaStatus.CREATED,
459+
id: assetStub.image.id,
460+
});
461+
462+
expect(storageMock.utimes).toHaveBeenCalledWith(
463+
fileStub.photoSidecar.originalPath,
464+
expect.any(Date),
465+
new Date(createDto.fileModifiedAt),
466+
);
467+
expect(assetMock.update).not.toHaveBeenCalled();
468+
});
388469
});
389470

390471
describe('downloadOriginal', () => {
@@ -419,6 +500,170 @@ describe(AssetMediaService.name, () => {
419500
});
420501
});
421502

503+
describe('viewThumbnail', () => {
504+
it('should require asset.view permissions', async () => {
505+
await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
506+
507+
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
508+
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
509+
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
510+
});
511+
512+
it('should throw an error if the asset does not exist', async () => {
513+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
514+
assetMock.getById.mockResolvedValue(null);
515+
516+
await expect(
517+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
518+
).rejects.toBeInstanceOf(NotFoundException);
519+
});
520+
521+
it('should throw an error if the requested thumbnail file does not exist', async () => {
522+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
523+
assetMock.getById.mockResolvedValue({ ...assetStub.image, files: [] });
524+
525+
await expect(
526+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
527+
).rejects.toBeInstanceOf(NotFoundException);
528+
});
529+
530+
it('should throw an error if the requested preview file does not exist', async () => {
531+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
532+
assetMock.getById.mockResolvedValue({
533+
...assetStub.image,
534+
files: [
535+
{
536+
assetId: assetStub.image.id,
537+
createdAt: assetStub.image.fileCreatedAt,
538+
id: '42',
539+
path: '/path/to/preview',
540+
type: AssetFileType.THUMBNAIL,
541+
updatedAt: new Date(),
542+
},
543+
],
544+
});
545+
await expect(
546+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
547+
).rejects.toBeInstanceOf(NotFoundException);
548+
});
549+
550+
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
551+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
552+
assetMock.getById.mockResolvedValue({
553+
...assetStub.image,
554+
files: [
555+
{
556+
assetId: assetStub.image.id,
557+
createdAt: assetStub.image.fileCreatedAt,
558+
id: '42',
559+
path: '/path/to/preview.jpg',
560+
type: AssetFileType.PREVIEW,
561+
updatedAt: new Date(),
562+
},
563+
],
564+
});
565+
566+
await expect(
567+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
568+
).resolves.toEqual(
569+
new ImmichFileResponse({
570+
path: '/path/to/preview.jpg',
571+
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
572+
contentType: 'image/jpeg',
573+
}),
574+
);
575+
});
576+
577+
it('should get preview file', async () => {
578+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
579+
assetMock.getById.mockResolvedValue({ ...assetStub.image });
580+
await expect(
581+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
582+
).resolves.toEqual(
583+
new ImmichFileResponse({
584+
path: assetStub.image.files[0].path,
585+
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
586+
contentType: 'image/jpeg',
587+
}),
588+
);
589+
});
590+
591+
it('should get thumbnail file', async () => {
592+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
593+
assetMock.getById.mockResolvedValue({ ...assetStub.image });
594+
await expect(
595+
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
596+
).resolves.toEqual(
597+
new ImmichFileResponse({
598+
path: assetStub.image.files[1].path,
599+
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
600+
contentType: 'application/octet-stream',
601+
}),
602+
);
603+
});
604+
});
605+
606+
describe('playbackVideo', () => {
607+
it('should require asset.view permissions', async () => {
608+
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
609+
610+
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
611+
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
612+
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
613+
});
614+
615+
it('should throw an error if the asset does not exist', async () => {
616+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
617+
assetMock.getById.mockResolvedValue(null);
618+
619+
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
620+
});
621+
622+
it('should throw an error if the asset is not a video', async () => {
623+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
624+
assetMock.getById.mockResolvedValue(assetStub.image);
625+
626+
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
627+
});
628+
629+
it('should return the encoded video path if available', async () => {
630+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
631+
assetMock.getById.mockResolvedValue(assetStub.hasEncodedVideo);
632+
633+
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
634+
new ImmichFileResponse({
635+
path: assetStub.hasEncodedVideo.encodedVideoPath!,
636+
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
637+
contentType: 'video/mp4',
638+
}),
639+
);
640+
});
641+
642+
it('should fall back to the original path', async () => {
643+
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
644+
assetMock.getById.mockResolvedValue(assetStub.video);
645+
646+
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
647+
new ImmichFileResponse({
648+
path: assetStub.video.originalPath,
649+
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
650+
contentType: 'application/octet-stream',
651+
}),
652+
);
653+
});
654+
});
655+
656+
describe('checkExistingAssets', () => {
657+
it('should get existing asset ids', async () => {
658+
assetMock.getByDeviceIds.mockResolvedValue(['42']);
659+
await expect(
660+
sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
661+
).resolves.toEqual({ existingIds: ['42'] });
662+
663+
expect(assetMock.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
664+
});
665+
});
666+
422667
describe('replaceAsset', () => {
423668
it('should error when update photo does not exist', async () => {
424669
assetMock.getById.mockResolvedValueOnce(null);
@@ -601,5 +846,37 @@ describe(AssetMediaService.name, () => {
601846

602847
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
603848
});
849+
850+
it('should return non-duplicates as well', async () => {
851+
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
852+
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
853+
854+
assetMock.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
855+
856+
await expect(
857+
sut.bulkUploadCheck(authStub.admin, {
858+
assets: [
859+
{ id: '1', checksum: file1.toString('hex') },
860+
{ id: '2', checksum: file2.toString('base64') },
861+
],
862+
}),
863+
).resolves.toEqual({
864+
results: [
865+
{
866+
id: '1',
867+
assetId: 'asset-1',
868+
action: AssetUploadAction.REJECT,
869+
reason: AssetRejectReason.DUPLICATE,
870+
isTrashed: false,
871+
},
872+
{
873+
id: '2',
874+
action: AssetUploadAction.ACCEPT,
875+
},
876+
],
877+
});
878+
879+
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
880+
});
604881
});
605882
});

server/src/services/asset-media.service.ts

-6
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,6 @@ export class AssetMediaService extends BaseService {
185185
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
186186

187187
const asset = await this.findOrFail(id);
188-
if (!asset) {
189-
throw new NotFoundException('Asset does not exist');
190-
}
191188

192189
return new ImmichFileResponse({
193190
path: asset.originalPath,
@@ -223,9 +220,6 @@ export class AssetMediaService extends BaseService {
223220
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
224221

225222
const asset = await this.findOrFail(id);
226-
if (!asset) {
227-
throw new NotFoundException('Asset does not exist');
228-
}
229223

230224
if (asset.type !== AssetType.VIDEO) {
231225
throw new BadRequestException('Asset is not a video');

0 commit comments

Comments
 (0)