2022-09-08 11:07:27 +02:00
|
|
|
import { IAssetRepository } from './asset-repository';
|
2022-08-27 07:53:37 +02:00
|
|
|
import { AssetService } from './asset.service';
|
2023-01-30 17:14:13 +01:00
|
|
|
import { QueryFailedError, Repository } from 'typeorm';
|
2023-05-06 03:33:30 +02:00
|
|
|
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
2022-09-16 23:47:45 +02:00
|
|
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
|
|
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
|
|
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
|
|
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
2022-11-15 16:51:56 +01:00
|
|
|
import { DownloadService } from '../../modules/download/download.service';
|
2023-01-25 17:35:28 +01:00
|
|
|
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
2023-05-15 19:30:53 +02:00
|
|
|
import {
|
|
|
|
ICryptoRepository,
|
|
|
|
IJobRepository,
|
|
|
|
IPartnerRepository,
|
|
|
|
ISharedLinkRepository,
|
|
|
|
IStorageRepository,
|
|
|
|
JobName,
|
|
|
|
} from '@app/domain';
|
2023-02-25 15:12:03 +01:00
|
|
|
import {
|
|
|
|
assetEntityStub,
|
2023-01-25 17:35:28 +01:00
|
|
|
authStub,
|
2023-02-25 15:12:03 +01:00
|
|
|
fileStub,
|
2023-01-25 17:35:28 +01:00
|
|
|
newCryptoRepositoryMock,
|
|
|
|
newJobRepositoryMock,
|
|
|
|
newSharedLinkRepositoryMock,
|
2023-02-03 16:16:25 +01:00
|
|
|
newStorageRepositoryMock,
|
2023-01-25 17:35:28 +01:00
|
|
|
sharedLinkResponseStub,
|
|
|
|
sharedLinkStub,
|
|
|
|
} from '@app/domain/../test';
|
|
|
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
|
|
|
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
2023-02-25 15:12:03 +01:00
|
|
|
import { when } from 'jest-when';
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
const _getCreateAssetDto = (): CreateAssetDto => {
|
|
|
|
const createAssetDto = new CreateAssetDto();
|
|
|
|
createAssetDto.deviceAssetId = 'deviceAssetId';
|
|
|
|
createAssetDto.deviceId = 'deviceId';
|
|
|
|
createAssetDto.assetType = AssetType.OTHER;
|
2023-02-19 17:44:53 +01:00
|
|
|
createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
|
|
|
createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
|
2023-01-30 17:14:13 +01:00
|
|
|
createAssetDto.isFavorite = false;
|
2023-04-12 17:37:52 +02:00
|
|
|
createAssetDto.isArchived = false;
|
2023-01-30 17:14:13 +01:00
|
|
|
createAssetDto.duration = '0:00:00.000000';
|
|
|
|
|
|
|
|
return createAssetDto;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAsset_1 = () => {
|
|
|
|
const asset_1 = new AssetEntity();
|
|
|
|
|
|
|
|
asset_1.id = 'id_1';
|
2023-02-19 17:44:53 +01:00
|
|
|
asset_1.ownerId = 'user_id_1';
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.deviceAssetId = 'device_asset_id_1';
|
|
|
|
asset_1.deviceId = 'device_id_1';
|
|
|
|
asset_1.type = AssetType.VIDEO;
|
|
|
|
asset_1.originalPath = 'fake_path/asset_1.jpeg';
|
|
|
|
asset_1.resizePath = '';
|
2023-02-19 17:44:53 +01:00
|
|
|
asset_1.fileModifiedAt = '2022-06-19T23:41:36.910Z';
|
|
|
|
asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
|
|
|
asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.isFavorite = false;
|
2023-04-12 17:37:52 +02:00
|
|
|
asset_1.isArchived = false;
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.mimeType = 'image/jpeg';
|
|
|
|
asset_1.webpPath = '';
|
|
|
|
asset_1.encodedVideoPath = '';
|
|
|
|
asset_1.duration = '0:00:00.000000';
|
2023-05-06 03:33:30 +02:00
|
|
|
asset_1.exifInfo = new ExifEntity();
|
|
|
|
asset_1.exifInfo.latitude = 49.533547;
|
|
|
|
asset_1.exifInfo.longitude = 10.703075;
|
2023-01-30 17:14:13 +01:00
|
|
|
return asset_1;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAsset_2 = () => {
|
|
|
|
const asset_2 = new AssetEntity();
|
|
|
|
|
|
|
|
asset_2.id = 'id_2';
|
2023-02-19 17:44:53 +01:00
|
|
|
asset_2.ownerId = 'user_id_1';
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_2.deviceAssetId = 'device_asset_id_2';
|
|
|
|
asset_2.deviceId = 'device_id_1';
|
|
|
|
asset_2.type = AssetType.VIDEO;
|
|
|
|
asset_2.originalPath = 'fake_path/asset_2.jpeg';
|
|
|
|
asset_2.resizePath = '';
|
2023-02-19 17:44:53 +01:00
|
|
|
asset_2.fileModifiedAt = '2022-06-19T23:41:36.910Z';
|
|
|
|
asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
|
|
|
asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_2.isFavorite = false;
|
2023-04-12 17:37:52 +02:00
|
|
|
asset_2.isArchived = false;
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_2.mimeType = 'image/jpeg';
|
|
|
|
asset_2.webpPath = '';
|
|
|
|
asset_2.encodedVideoPath = '';
|
|
|
|
asset_2.duration = '0:00:00.000000';
|
|
|
|
|
|
|
|
return asset_2;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAssets = () => {
|
|
|
|
return [_getAsset_1(), _getAsset_2()];
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
|
|
|
|
const result1 = new AssetCountByTimeBucket();
|
|
|
|
result1.count = 2;
|
|
|
|
result1.timeBucket = '2022-06-01T00:00:00.000Z';
|
|
|
|
|
|
|
|
const result2 = new AssetCountByTimeBucket();
|
|
|
|
result1.count = 5;
|
|
|
|
result1.timeBucket = '2022-07-01T00:00:00.000Z';
|
|
|
|
|
|
|
|
return [result1, result2];
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
|
|
|
|
const result = new AssetCountByUserIdResponseDto();
|
|
|
|
|
|
|
|
result.videos = 2;
|
|
|
|
result.photos = 2;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2023-04-12 17:37:52 +02:00
|
|
|
const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => {
|
|
|
|
const result = new AssetCountByUserIdResponseDto();
|
|
|
|
|
|
|
|
result.videos = 1;
|
|
|
|
result.photos = 2;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2022-08-27 07:53:37 +02:00
|
|
|
describe('AssetService', () => {
|
2023-01-30 17:14:13 +01:00
|
|
|
let sut: AssetService;
|
2022-08-27 07:53:37 +02:00
|
|
|
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
|
|
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
2022-12-04 18:42:36 +01:00
|
|
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
2022-11-15 16:51:56 +01:00
|
|
|
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
2023-05-15 19:30:53 +02:00
|
|
|
let partnerRepositoryMock: jest.Mocked<IPartnerRepository>;
|
2023-01-09 21:16:08 +01:00
|
|
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
2023-01-25 17:35:28 +01:00
|
|
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
2023-01-22 05:13:36 +01:00
|
|
|
let jobMock: jest.Mocked<IJobRepository>;
|
2023-02-03 16:16:25 +01:00
|
|
|
let storageMock: jest.Mocked<IStorageRepository>;
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
beforeEach(() => {
|
2022-08-27 07:53:37 +02:00
|
|
|
assetRepositoryMock = {
|
2023-01-30 17:14:13 +01:00
|
|
|
get: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
create: jest.fn(),
|
2023-01-30 17:14:13 +01:00
|
|
|
remove: jest.fn(),
|
2023-02-25 15:12:03 +01:00
|
|
|
save: jest.fn(),
|
2023-01-30 17:14:13 +01:00
|
|
|
|
2022-11-08 17:20:36 +01:00
|
|
|
update: jest.fn(),
|
2023-01-27 05:50:22 +01:00
|
|
|
getAll: jest.fn(),
|
|
|
|
getAllVideos: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
getAllByUserId: jest.fn(),
|
|
|
|
getAllByDeviceId: jest.fn(),
|
2022-09-04 15:34:39 +02:00
|
|
|
getAssetCountByTimeBucket: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
getById: jest.fn(),
|
|
|
|
getDetectedObjectsByUserId: jest.fn(),
|
|
|
|
getLocationsByUserId: jest.fn(),
|
|
|
|
getSearchPropertiesByUserId: jest.fn(),
|
2022-09-04 15:34:39 +02:00
|
|
|
getAssetByTimeBucket: jest.fn(),
|
2023-05-24 23:08:21 +02:00
|
|
|
getAssetsByChecksums: jest.fn(),
|
2022-09-07 22:16:18 +02:00
|
|
|
getAssetCountByUserId: jest.fn(),
|
2023-04-12 17:37:52 +02:00
|
|
|
getArchivedAssetCountByUserId: jest.fn(),
|
2022-10-25 16:51:03 +02:00
|
|
|
getExistingAssets: jest.fn(),
|
2022-12-04 18:42:36 +01:00
|
|
|
countByIdAndUser: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
};
|
|
|
|
|
2023-01-25 17:35:28 +01:00
|
|
|
albumRepositoryMock = {
|
|
|
|
getSharedWithUserAlbumCount: jest.fn(),
|
|
|
|
} as unknown as jest.Mocked<AlbumRepository>;
|
|
|
|
|
2022-11-15 16:51:56 +01:00
|
|
|
downloadServiceMock = {
|
|
|
|
downloadArchive: jest.fn(),
|
|
|
|
};
|
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
2023-01-22 05:13:36 +01:00
|
|
|
jobMock = newJobRepositoryMock();
|
2023-01-25 17:35:28 +01:00
|
|
|
cryptoMock = newCryptoRepositoryMock();
|
2023-02-03 16:16:25 +01:00
|
|
|
storageMock = newStorageRepositoryMock();
|
2023-01-22 05:13:36 +01:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
sut = new AssetService(
|
2022-11-19 06:12:54 +01:00
|
|
|
assetRepositoryMock,
|
2022-12-04 18:42:36 +01:00
|
|
|
albumRepositoryMock,
|
2022-11-19 06:12:54 +01:00
|
|
|
a,
|
|
|
|
downloadServiceMock as DownloadService,
|
2023-01-09 21:16:08 +01:00
|
|
|
sharedLinkRepositoryMock,
|
2023-01-22 05:13:36 +01:00
|
|
|
jobMock,
|
2023-01-25 17:35:28 +01:00
|
|
|
cryptoMock,
|
2023-02-03 16:16:25 +01:00
|
|
|
storageMock,
|
2023-05-15 19:30:53 +02:00
|
|
|
partnerRepositoryMock,
|
2022-11-19 06:12:54 +01:00
|
|
|
);
|
2023-02-25 15:12:03 +01:00
|
|
|
|
|
|
|
when(assetRepositoryMock.get)
|
|
|
|
.calledWith(assetEntityStub.livePhotoStillAsset.id)
|
|
|
|
.mockResolvedValue(assetEntityStub.livePhotoStillAsset);
|
|
|
|
when(assetRepositoryMock.get)
|
|
|
|
.calledWith(assetEntityStub.livePhotoMotionAsset.id)
|
|
|
|
.mockResolvedValue(assetEntityStub.livePhotoMotionAsset);
|
2022-08-27 07:53:37 +02:00
|
|
|
});
|
|
|
|
|
2023-01-25 17:35:28 +01:00
|
|
|
describe('createAssetsSharedLink', () => {
|
|
|
|
it('should create an individual share link', async () => {
|
|
|
|
const asset1 = _getAsset_1();
|
|
|
|
const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
|
|
|
|
|
|
|
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
|
|
|
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
|
|
|
sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
|
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
2023-01-25 17:35:28 +01:00
|
|
|
|
|
|
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
|
|
|
expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('updateAssetsInSharedLink', () => {
|
|
|
|
it('should require a valid shared link', async () => {
|
|
|
|
const asset1 = _getAsset_1();
|
|
|
|
|
|
|
|
const authDto = authStub.adminSharedLink;
|
|
|
|
const dto = { assetIds: [asset1.id] };
|
|
|
|
|
|
|
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
|
|
|
sharedLinkRepositoryMock.get.mockResolvedValue(null);
|
|
|
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
|
|
|
|
2023-02-15 22:21:22 +01:00
|
|
|
await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
2023-01-25 17:35:28 +01:00
|
|
|
|
|
|
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
|
|
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
|
|
|
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-02-15 22:21:22 +01:00
|
|
|
it('should add assets to a shared link', async () => {
|
|
|
|
const asset1 = _getAsset_1();
|
|
|
|
|
|
|
|
const authDto = authStub.adminSharedLink;
|
|
|
|
const dto = { assetIds: [asset1.id] };
|
|
|
|
|
|
|
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
|
|
|
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
|
|
|
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
|
|
|
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
|
|
|
|
|
|
|
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
|
|
|
|
|
|
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
|
|
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
|
|
|
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-01-25 17:35:28 +01:00
|
|
|
it('should remove assets from a shared link', async () => {
|
|
|
|
const asset1 = _getAsset_1();
|
|
|
|
|
|
|
|
const authDto = authStub.adminSharedLink;
|
|
|
|
const dto = { assetIds: [asset1.id] };
|
|
|
|
|
|
|
|
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
|
|
|
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
|
|
|
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
|
|
|
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
|
|
|
|
2023-02-15 22:21:22 +01:00
|
|
|
await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
2023-01-25 17:35:28 +01:00
|
|
|
|
|
|
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
|
|
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
2023-02-15 22:21:22 +01:00
|
|
|
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
|
2023-01-25 17:35:28 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
describe('uploadFile', () => {
|
|
|
|
it('should handle a file upload', async () => {
|
|
|
|
const assetEntity = _getAsset_1();
|
|
|
|
const file = {
|
|
|
|
originalPath: 'fake_path/asset_1.jpeg',
|
|
|
|
mimeType: 'image/jpeg',
|
|
|
|
checksum: Buffer.from('file hash', 'utf8'),
|
|
|
|
originalName: 'asset_1.jpeg',
|
|
|
|
};
|
|
|
|
const dto = _getCreateAssetDto();
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
assetRepositoryMock.create.mockResolvedValue(assetEntity);
|
|
|
|
assetRepositoryMock.save.mockResolvedValue(assetEntity);
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
2023-02-25 15:12:03 +01:00
|
|
|
|
|
|
|
expect(assetRepositoryMock.create).toHaveBeenCalled();
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
it('should handle a duplicate', async () => {
|
|
|
|
const file = {
|
|
|
|
originalPath: 'fake_path/asset_1.jpeg',
|
|
|
|
mimeType: 'image/jpeg',
|
|
|
|
checksum: Buffer.from('file hash', 'utf8'),
|
|
|
|
originalName: 'asset_1.jpeg',
|
|
|
|
};
|
|
|
|
const dto = _getCreateAssetDto();
|
|
|
|
const error = new QueryFailedError('', [], '');
|
|
|
|
(error as any).constraint = 'UQ_userid_checksum';
|
|
|
|
|
|
|
|
assetRepositoryMock.create.mockRejectedValue(error);
|
2023-05-24 23:08:21 +02:00
|
|
|
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.DELETE_FILES,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(storageMock.moveFile).not.toHaveBeenCalled();
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle a live photo', async () => {
|
|
|
|
const dto = _getCreateAssetDto();
|
|
|
|
const error = new QueryFailedError('', [], '');
|
|
|
|
(error as any).constraint = 'UQ_userid_checksum';
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
assetRepositoryMock.create.mockResolvedValueOnce(assetEntityStub.livePhotoMotionAsset);
|
|
|
|
assetRepositoryMock.save.mockResolvedValueOnce(assetEntityStub.livePhotoMotionAsset);
|
|
|
|
assetRepositoryMock.create.mockResolvedValueOnce(assetEntityStub.livePhotoStillAsset);
|
|
|
|
assetRepositoryMock.save.mockResolvedValueOnce(assetEntityStub.livePhotoStillAsset);
|
2023-01-30 17:14:13 +01:00
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
await expect(
|
|
|
|
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
|
|
|
|
).resolves.toEqual({
|
2023-01-30 17:14:13 +01:00
|
|
|
duplicate: false,
|
2023-02-25 15:12:03 +01:00
|
|
|
id: 'live-photo-still-asset',
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
2023-05-27 23:49:57 +02:00
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: { id: assetEntityStub.livePhotoMotionAsset.id, source: 'upload' },
|
|
|
|
},
|
|
|
|
],
|
2023-05-26 21:43:24 +02:00
|
|
|
[{ name: JobName.VIDEO_CONVERSION, data: { id: assetEntityStub.livePhotoMotionAsset.id } }],
|
2023-05-27 23:49:57 +02:00
|
|
|
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetEntityStub.livePhotoStillAsset.id, source: 'upload' } }],
|
2023-01-30 17:14:13 +01:00
|
|
|
]);
|
|
|
|
});
|
2022-09-16 23:47:45 +02:00
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
|
|
|
|
it('get assets by device id', async () => {
|
2022-09-16 23:47:45 +02:00
|
|
|
const assets = _getAssets();
|
|
|
|
|
|
|
|
assetRepositoryMock.getAllByDeviceId.mockImplementation(() =>
|
|
|
|
Promise.resolve<string[]>(Array.from(assets.map((asset) => asset.deviceAssetId))),
|
|
|
|
);
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2022-09-16 23:47:45 +02:00
|
|
|
const deviceId = 'device_id_1';
|
2023-01-30 17:14:13 +01:00
|
|
|
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2022-09-16 23:47:45 +02:00
|
|
|
expect(result.length).toEqual(2);
|
|
|
|
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
|
|
|
|
});
|
|
|
|
|
|
|
|
it('get assets count by time bucket', async () => {
|
|
|
|
const assetCountByTimeBucket = _getAssetCountByTimeBucket();
|
|
|
|
|
|
|
|
assetRepositoryMock.getAssetCountByTimeBucket.mockImplementation(() =>
|
|
|
|
Promise.resolve<AssetCountByTimeBucket[]>(assetCountByTimeBucket),
|
|
|
|
);
|
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
const result = await sut.getAssetCountByTimeBucket(authStub.user1, {
|
2022-09-16 23:47:45 +02:00
|
|
|
timeGroup: TimeGroupEnum.Month,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(result.totalCount).toEqual(assetCountByTimeBucket.reduce((a, b) => a + b.count, 0));
|
|
|
|
expect(result.buckets.length).toEqual(2);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('get asset count by user id', async () => {
|
|
|
|
const assetCount = _getAssetCountByUserId();
|
2023-04-12 17:37:52 +02:00
|
|
|
assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount);
|
2022-09-16 23:47:45 +02:00
|
|
|
|
2023-04-12 17:37:52 +02:00
|
|
|
await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
|
|
|
|
});
|
2022-09-16 23:47:45 +02:00
|
|
|
|
2023-04-12 17:37:52 +02:00
|
|
|
it('get archived asset count by user id', async () => {
|
|
|
|
const assetCount = _getArchivedAssetsCountByUserId();
|
|
|
|
assetRepositoryMock.getArchivedAssetCountByUserId.mockResolvedValue(assetCount);
|
2022-09-16 23:47:45 +02:00
|
|
|
|
2023-04-12 17:37:52 +02:00
|
|
|
await expect(sut.getArchivedAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
|
2022-08-27 07:53:37 +02:00
|
|
|
});
|
2023-01-25 17:35:28 +01:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
describe('deleteAll', () => {
|
|
|
|
it('should return failed status when an asset is missing', async () => {
|
|
|
|
assetRepositoryMock.get.mockResolvedValue(null);
|
|
|
|
|
|
|
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
|
|
|
|
{ id: 'asset1', status: 'FAILED' },
|
|
|
|
]);
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should return failed status a delete fails', async () => {
|
|
|
|
assetRepositoryMock.get.mockResolvedValue({ id: 'asset1' } as AssetEntity);
|
|
|
|
assetRepositoryMock.remove.mockRejectedValue('delete failed');
|
|
|
|
|
|
|
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
|
|
|
|
{ id: 'asset1', status: 'FAILED' },
|
|
|
|
]);
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should delete a live photo', async () => {
|
2023-02-25 15:12:03 +01:00
|
|
|
await expect(sut.deleteAll(authStub.user1, { ids: [assetEntityStub.livePhotoStillAsset.id] })).resolves.toEqual([
|
|
|
|
{ id: assetEntityStub.livePhotoStillAsset.id, status: 'SUCCESS' },
|
|
|
|
{ id: assetEntityStub.livePhotoMotionAsset.id, status: 'SUCCESS' },
|
2023-01-30 17:14:13 +01:00
|
|
|
]);
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.DELETE_FILES,
|
|
|
|
data: {
|
2023-03-13 18:42:05 +01:00
|
|
|
files: [
|
|
|
|
'fake_path/asset_1.jpeg',
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
undefined,
|
2023-03-13 18:42:05 +01:00
|
|
|
'fake_path/asset_1.mp4',
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
undefined,
|
2023-03-13 18:42:05 +01:00
|
|
|
],
|
2023-02-25 15:12:03 +01:00
|
|
|
},
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should delete a batch of assets', async () => {
|
2023-02-25 15:12:03 +01:00
|
|
|
const asset1 = {
|
|
|
|
id: 'asset1',
|
|
|
|
originalPath: 'original-path-1',
|
|
|
|
resizePath: 'resize-path-1',
|
|
|
|
webpPath: 'web-path-1',
|
|
|
|
};
|
|
|
|
|
|
|
|
const asset2 = {
|
|
|
|
id: 'asset2',
|
|
|
|
originalPath: 'original-path-2',
|
|
|
|
resizePath: 'resize-path-2',
|
|
|
|
webpPath: 'web-path-2',
|
2023-03-13 18:42:05 +01:00
|
|
|
encodedVideoPath: 'encoded-video-path-2',
|
2023-02-25 15:12:03 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
when(assetRepositoryMock.get)
|
|
|
|
.calledWith(asset1.id)
|
|
|
|
.mockResolvedValue(asset1 as AssetEntity);
|
|
|
|
when(assetRepositoryMock.get)
|
|
|
|
.calledWith(asset2.id)
|
|
|
|
.mockResolvedValue(asset2 as AssetEntity);
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
|
|
|
|
{ id: 'asset1', status: 'SUCCESS' },
|
|
|
|
{ id: 'asset2', status: 'SUCCESS' },
|
|
|
|
]);
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
2023-03-18 14:44:42 +01:00
|
|
|
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: ['asset1'] } }],
|
|
|
|
[{ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: ['asset2'] } }],
|
2023-02-25 15:12:03 +01:00
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.DELETE_FILES,
|
|
|
|
data: {
|
|
|
|
files: [
|
|
|
|
'original-path-1',
|
|
|
|
'web-path-1',
|
|
|
|
'resize-path-1',
|
2023-03-13 18:42:05 +01:00
|
|
|
undefined,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
undefined,
|
2023-02-25 15:12:03 +01:00
|
|
|
'original-path-2',
|
|
|
|
'web-path-2',
|
|
|
|
'resize-path-2',
|
2023-03-13 18:42:05 +01:00
|
|
|
'encoded-video-path-2',
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
undefined,
|
2023-02-25 15:12:03 +01:00
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2023-01-30 17:14:13 +01:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-01-25 17:35:28 +01:00
|
|
|
describe('checkDownloadAccess', () => {
|
|
|
|
it('should validate download access', async () => {
|
2023-01-30 17:14:13 +01:00
|
|
|
await sut.checkDownloadAccess(authStub.adminSharedLink);
|
2023-01-25 17:35:28 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should not allow when user is not allowed to download', async () => {
|
2023-01-30 17:14:13 +01:00
|
|
|
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
2023-01-25 17:35:28 +01:00
|
|
|
});
|
|
|
|
});
|
2023-02-03 16:16:25 +01:00
|
|
|
|
|
|
|
describe('downloadFile', () => {
|
|
|
|
it('should download a single file', async () => {
|
|
|
|
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
|
|
|
assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
|
|
|
|
|
|
|
|
await sut.downloadFile(authStub.admin, 'id_1');
|
|
|
|
|
|
|
|
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
|
|
|
|
});
|
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
});
|