mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 02:23:09 -05:00
chore: some more unit tests :) (#13159)
This commit is contained in:
parent
db1623f43f
commit
2c8c3651de
3 changed files with 304 additions and 9 deletions
|
@ -306,6 +306,17 @@ describe(AlbumService.name, () => {
|
|||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the userId is the ownerId', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
|
||||
await expect(
|
||||
sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
|
||||
albumUsers: [{ userId: userStub.user1.id }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add valid shared users', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
|
||||
|
@ -415,6 +426,19 @@ describe(AlbumService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('should update user role', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
|
||||
role: AlbumUserRole.EDITOR,
|
||||
});
|
||||
expect(albumUserMock.update).toHaveBeenCalledWith(
|
||||
{ albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id },
|
||||
{ role: AlbumUserRole.EDITOR },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlbumInfo', () => {
|
||||
it('should get a shared album', async () => {
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Stats } from 'node:fs';
|
||||
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType, CacheControl } from 'src/enum';
|
||||
import { AssetFileType, AssetStatus, AssetType, CacheControl } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
|
@ -14,6 +19,7 @@ import { ImmichFileResponse } from 'src/utils/file';
|
|||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
@ -194,6 +200,10 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
describe('getUploadAssetIdByChecksum', () => {
|
||||
it('should return if checksum is undefined', async () => {
|
||||
await expect(sut.getUploadAssetIdByChecksum(authStub.admin)).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle a non-existent asset', async () => {
|
||||
await expect(sut.getUploadAssetIdByChecksum(authStub.admin, file1.toString('hex'))).resolves.toBeUndefined();
|
||||
expect(assetMock.getUploadAssetIdByChecksum).toHaveBeenCalledWith(authStub.admin.user.id, file1);
|
||||
|
@ -295,6 +305,35 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
|
||||
describe('uploadAsset', () => {
|
||||
it('should throw an error if the quota is exceeded', async () => {
|
||||
const file = {
|
||||
uuid: 'random-uuid',
|
||||
originalPath: 'fake_path/asset_1.jpeg',
|
||||
mimeType: 'image/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
originalName: 'asset_1.jpeg',
|
||||
size: 42,
|
||||
};
|
||||
|
||||
assetMock.create.mockResolvedValue(assetEntity);
|
||||
|
||||
await expect(
|
||||
sut.uploadAsset(
|
||||
{ ...authStub.admin, user: { ...authStub.admin.user, quotaSizeInBytes: 42, quotaUsageInBytes: 1 } },
|
||||
createDto,
|
||||
file,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetMock.create).not.toHaveBeenCalled();
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(storageMock.utimes).not.toHaveBeenCalledWith(
|
||||
file.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(createDto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a file upload', async () => {
|
||||
const file = {
|
||||
uuid: 'random-uuid',
|
||||
|
@ -348,6 +387,31 @@ describe(AssetMediaService.name, () => {
|
|||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the duplicate could not be found by checksum', async () => {
|
||||
const file = {
|
||||
uuid: 'random-uuid',
|
||||
originalPath: 'fake_path/asset_1.jpeg',
|
||||
mimeType: 'image/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
originalName: 'asset_1.jpeg',
|
||||
size: 0,
|
||||
};
|
||||
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
||||
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.create.mockRejectedValue(error);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, file)).rejects.toBeInstanceOf(
|
||||
InternalServerErrorException,
|
||||
);
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['fake_path/asset_1.jpeg', undefined] },
|
||||
});
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a live photo', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
|
@ -385,6 +449,23 @@ describe(AssetMediaService.name, () => {
|
|||
expect(assetMock.getById).toHaveBeenCalledWith('live-photo-motion-asset');
|
||||
expect(assetMock.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
|
||||
});
|
||||
|
||||
it('should handle a sidecar file', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce(assetStub.image);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.image);
|
||||
|
||||
await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({
|
||||
status: AssetMediaStatus.CREATED,
|
||||
id: assetStub.image.id,
|
||||
});
|
||||
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
fileStub.photoSidecar.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(createDto.fileModifiedAt),
|
||||
);
|
||||
expect(assetMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadOriginal', () => {
|
||||
|
@ -419,6 +500,170 @@ describe(AssetMediaService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('viewThumbnail', () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw an error if the requested thumbnail file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image, files: [] });
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw an error if the requested preview file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
createdAt: assetStub.image.fileCreatedAt,
|
||||
id: '42',
|
||||
path: '/path/to/preview',
|
||||
type: AssetFileType.THUMBNAIL,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
{
|
||||
assetId: assetStub.image.id,
|
||||
createdAt: assetStub.image.fileCreatedAt,
|
||||
id: '42',
|
||||
path: '/path/to/preview.jpg',
|
||||
type: AssetFileType.PREVIEW,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/path/to/preview.jpg',
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
contentType: 'image/jpeg',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get preview file', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.image.files[0].path,
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
contentType: 'image/jpeg',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should get thumbnail file', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue({ ...assetStub.image });
|
||||
await expect(
|
||||
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }),
|
||||
).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.image.files[1].path,
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
contentType: 'application/octet-stream',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('playbackVideo', () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw an error if the asset is not a video', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should return the encoded video path if available', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.hasEncodedVideo);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.hasEncodedVideo.encodedVideoPath!,
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
contentType: 'video/mp4',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to the original path', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id]));
|
||||
assetMock.getById.mockResolvedValue(assetStub.video);
|
||||
|
||||
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: assetStub.video.originalPath,
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
contentType: 'application/octet-stream',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkExistingAssets', () => {
|
||||
it('should get existing asset ids', async () => {
|
||||
assetMock.getByDeviceIds.mockResolvedValue(['42']);
|
||||
await expect(
|
||||
sut.checkExistingAssets(authStub.admin, { deviceId: '420', deviceAssetIds: ['69'] }),
|
||||
).resolves.toEqual({ existingIds: ['42'] });
|
||||
|
||||
expect(assetMock.getByDeviceIds).toHaveBeenCalledWith(userStub.admin.id, '420', ['69']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceAsset', () => {
|
||||
it('should error when update photo does not exist', async () => {
|
||||
assetMock.getById.mockResolvedValueOnce(null);
|
||||
|
@ -601,5 +846,37 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
|
||||
it('should return non-duplicates as well', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetMock.getByChecksums.mockResolvedValue([{ id: 'asset-1', checksum: file1 } as AssetEntity]);
|
||||
|
||||
await expect(
|
||||
sut.bulkUploadCheck(authStub.admin, {
|
||||
assets: [
|
||||
{ id: '1', checksum: file1.toString('hex') },
|
||||
{ id: '2', checksum: file2.toString('base64') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{
|
||||
id: '1',
|
||||
assetId: 'asset-1',
|
||||
action: AssetUploadAction.REJECT,
|
||||
reason: AssetRejectReason.DUPLICATE,
|
||||
isTrashed: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
action: AssetUploadAction.ACCEPT,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -185,9 +185,6 @@ export class AssetMediaService extends BaseService {
|
|||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset does not exist');
|
||||
}
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path: asset.originalPath,
|
||||
|
@ -223,9 +220,6 @@ export class AssetMediaService extends BaseService {
|
|||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset does not exist');
|
||||
}
|
||||
|
||||
if (asset.type !== AssetType.VIDEO) {
|
||||
throw new BadRequestException('Asset is not a video');
|
||||
|
|
Loading…
Add table
Reference in a new issue