mirror of
https://github.com/immich-app/immich.git
synced 2026-01-21 11:26:08 -05:00
607 lines
24 KiB
TypeScript
607 lines
24 KiB
TypeScript
import { Kysely } from 'kysely';
|
|
import { AssetFileType, AssetMetadataKey, JobName, SharedLinkType } from 'src/enum';
|
|
import { AccessRepository } from 'src/repositories/access.repository';
|
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
|
import { EventRepository } from 'src/repositories/event.repository';
|
|
import { JobRepository } from 'src/repositories/job.repository';
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
|
import { StackRepository } from 'src/repositories/stack.repository';
|
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
|
import { UserRepository } from 'src/repositories/user.repository';
|
|
import { DB } from 'src/schema';
|
|
import { AssetService } from 'src/services/asset.service';
|
|
import { newMediumService } from 'test/medium.factory';
|
|
import { factory } from 'test/small.factory';
|
|
import { getKyselyDB } from 'test/utils';
|
|
|
|
let defaultDatabase: Kysely<DB>;
|
|
|
|
const setup = (db?: Kysely<DB>) => {
|
|
return newMediumService(AssetService, {
|
|
database: db || defaultDatabase,
|
|
real: [
|
|
AssetRepository,
|
|
AssetJobRepository,
|
|
AlbumRepository,
|
|
AccessRepository,
|
|
SharedLinkAssetRepository,
|
|
StackRepository,
|
|
UserRepository,
|
|
],
|
|
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository],
|
|
});
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
defaultDatabase = await getKyselyDB();
|
|
});
|
|
|
|
describe(AssetService.name, () => {
|
|
describe('getStatistics', () => {
|
|
it('should return stats as numbers, not strings', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 });
|
|
});
|
|
});
|
|
|
|
describe('copy', () => {
|
|
it('should copy albums', async () => {
|
|
const { sut, ctx } = setup();
|
|
const albumRepo = ctx.get(AlbumRepository);
|
|
|
|
const { user } = await ctx.newUser();
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: oldAsset.id });
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
|
|
await expect(albumRepo.getAssetIds(album.id, [oldAsset.id, newAsset.id])).resolves.toEqual(
|
|
new Set([oldAsset.id, newAsset.id]),
|
|
);
|
|
});
|
|
|
|
it('should copy shared links', async () => {
|
|
const { sut, ctx } = setup();
|
|
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
|
|
|
const { user } = await ctx.newUser();
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
|
|
|
const { id: sharedLinkId } = await sharedLinkRepo.create({
|
|
allowUpload: false,
|
|
key: Buffer.from('123'),
|
|
type: SharedLinkType.Individual,
|
|
userId: user.id,
|
|
assetIds: [oldAsset.id],
|
|
});
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
await expect(sharedLinkRepo.get(user.id, sharedLinkId)).resolves.toEqual(
|
|
expect.objectContaining({
|
|
assets: [expect.objectContaining({ id: oldAsset.id }), expect.objectContaining({ id: newAsset.id })],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should merge stacks', async () => {
|
|
const { sut, ctx } = setup();
|
|
const stackRepo = ctx.get(StackRepository);
|
|
|
|
const { user } = await ctx.newUser();
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
|
await ctx.newExif({ assetId: asset1.id, description: 'bar' });
|
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
|
await ctx.newExif({ assetId: asset2.id, description: 'foo' });
|
|
|
|
await ctx.newStack({ ownerId: user.id }, [oldAsset.id, asset1.id]);
|
|
|
|
const {
|
|
stack: { id: newStackId },
|
|
} = await ctx.newStack({ ownerId: user.id }, [newAsset.id, asset2.id]);
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
|
|
await expect(stackRepo.getById(oldAsset.id)).resolves.toEqual(undefined);
|
|
|
|
const newStack = await stackRepo.getById(newStackId);
|
|
expect(newStack).toEqual(
|
|
expect.objectContaining({
|
|
primaryAssetId: newAsset.id,
|
|
assets: expect.arrayContaining([expect.objectContaining({ id: asset2.id })]),
|
|
}),
|
|
);
|
|
expect(newStack!.assets.length).toEqual(4);
|
|
});
|
|
|
|
it('should copy stack', async () => {
|
|
const { sut, ctx } = setup();
|
|
const stackRepo = ctx.get(StackRepository);
|
|
|
|
const { user } = await ctx.newUser();
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
|
await ctx.newExif({ assetId: asset1.id, description: 'bar' });
|
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
|
|
|
const {
|
|
stack: { id: stackId },
|
|
} = await ctx.newStack({ ownerId: user.id }, [oldAsset.id, asset1.id]);
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
|
|
const stack = await stackRepo.getById(stackId);
|
|
expect(stack).toEqual(
|
|
expect.objectContaining({
|
|
primaryAssetId: oldAsset.id,
|
|
assets: expect.arrayContaining([expect.objectContaining({ id: newAsset.id })]),
|
|
}),
|
|
);
|
|
expect(stack!.assets.length).toEqual(3);
|
|
});
|
|
|
|
it('should copy favorite status', async () => {
|
|
const { sut, ctx } = setup();
|
|
const assetRepo = ctx.get(AssetRepository);
|
|
|
|
const { user } = await ctx.newUser();
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
|
|
await expect(assetRepo.getById(newAsset.id)).resolves.toEqual(expect.objectContaining({ isFavorite: true }));
|
|
});
|
|
|
|
it('should copy sidecar file', async () => {
|
|
const { sut, ctx } = setup();
|
|
const storageRepo = ctx.getMock(StorageRepository);
|
|
const jobRepo = ctx.getMock(JobRepository);
|
|
|
|
storageRepo.copyFile.mockResolvedValue();
|
|
jobRepo.queue.mockResolvedValue();
|
|
|
|
const { user } = await ctx.newUser();
|
|
|
|
const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newAssetFile({
|
|
assetId: oldAsset.id,
|
|
path: '/path/to/my/sidecar.xmp',
|
|
type: AssetFileType.Sidecar,
|
|
});
|
|
|
|
const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await ctx.newExif({ assetId: oldAsset.id, description: 'foo' });
|
|
await ctx.newExif({ assetId: newAsset.id, description: 'bar' });
|
|
|
|
const auth = factory.auth({ user: { id: user.id } });
|
|
|
|
await sut.copy(auth, { sourceId: oldAsset.id, targetId: newAsset.id });
|
|
|
|
expect(storageRepo.copyFile).toHaveBeenCalledWith('/path/to/my/sidecar.xmp', `${newAsset.originalPath}.xmp`);
|
|
|
|
expect(jobRepo.queue).toHaveBeenCalledWith({
|
|
name: JobName.AssetExtractMetadata,
|
|
data: { id: newAsset.id },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should delete asset', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
const thumbnailPath = '/path/to/thumbnail.jpg';
|
|
const previewPath = '/path/to/preview.jpg';
|
|
const sidecarPath = '/path/to/sidecar.xmp';
|
|
await Promise.all([
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }),
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }),
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath }),
|
|
]);
|
|
|
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
|
|
|
expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({
|
|
name: JobName.FileDelete,
|
|
data: { files: [thumbnailPath, previewPath, sidecarPath, asset.originalPath] },
|
|
});
|
|
});
|
|
|
|
it('should not delete offline assets', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id, isOffline: true });
|
|
const thumbnailPath = '/path/to/thumbnail.jpg';
|
|
const previewPath = '/path/to/preview.jpg';
|
|
await Promise.all([
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }),
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }),
|
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: `/path/to/sidecar.xmp` }),
|
|
]);
|
|
|
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
|
|
|
expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({
|
|
name: JobName.FileDelete,
|
|
data: { files: [thumbnailPath, previewPath] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('should automatically lock lockable columns', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
|
|
|
await expect(
|
|
ctx.database
|
|
.selectFrom('asset_exif')
|
|
.select('lockedProperties')
|
|
.where('assetId', '=', asset.id)
|
|
.executeTakeFirstOrThrow(),
|
|
).resolves.toEqual({ lockedProperties: null });
|
|
|
|
await sut.update(auth, asset.id, {
|
|
latitude: 42,
|
|
longitude: 42,
|
|
rating: 3,
|
|
description: 'foo',
|
|
dateTimeOriginal: '2023-11-19T18:11:00+01:00',
|
|
});
|
|
|
|
await expect(
|
|
ctx.database
|
|
.selectFrom('asset_exif')
|
|
.select('lockedProperties')
|
|
.where('assetId', '=', asset.id)
|
|
.executeTakeFirstOrThrow(),
|
|
).resolves.toEqual({
|
|
lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'],
|
|
});
|
|
});
|
|
|
|
it('should update dateTimeOriginal', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
|
|
|
await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00' });
|
|
|
|
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
|
expect.objectContaining({
|
|
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should update dateTimeOriginal with time zone', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
|
|
|
await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
|
|
|
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
|
expect.objectContaining({
|
|
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('updateAll', () => {
|
|
it('should automatically lock lockable columns', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
|
|
|
await expect(
|
|
ctx.database
|
|
.selectFrom('asset_exif')
|
|
.select('lockedProperties')
|
|
.where('assetId', '=', asset.id)
|
|
.executeTakeFirstOrThrow(),
|
|
).resolves.toEqual({ lockedProperties: null });
|
|
|
|
await sut.updateAll(auth, {
|
|
ids: [asset.id],
|
|
latitude: 42,
|
|
description: 'foo',
|
|
longitude: 42,
|
|
rating: 3,
|
|
dateTimeOriginal: '2023-11-19T18:11:00+01:00',
|
|
});
|
|
|
|
await expect(
|
|
ctx.database
|
|
.selectFrom('asset_exif')
|
|
.select('lockedProperties')
|
|
.where('assetId', '=', asset.id)
|
|
.executeTakeFirstOrThrow(),
|
|
).resolves.toEqual({
|
|
lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'],
|
|
});
|
|
});
|
|
|
|
it('should relatively update assets', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
|
|
|
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11 });
|
|
|
|
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
|
expect.objectContaining({
|
|
exifInfo: expect.objectContaining({
|
|
dateTimeOriginal: '2023-11-19T18:00:00+00:00',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should update dateTimeOriginal', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
|
|
|
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00' });
|
|
|
|
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
|
expect.objectContaining({
|
|
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should update dateTimeOriginal with time zone', async () => {
|
|
const { sut, ctx } = setup();
|
|
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newExif({ assetId: asset.id, description: 'test' });
|
|
|
|
await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
|
|
|
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
|
expect.objectContaining({
|
|
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('upsertBulkMetadata', () => {
|
|
it('should work', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
const items = [{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } }];
|
|
|
|
await sut.upsertBulkMetadata(auth, { items });
|
|
|
|
const metadata = await ctx.get(AssetRepository).getMetadata(asset.id);
|
|
expect(metadata.length).toEqual(1);
|
|
expect(metadata[0]).toEqual(
|
|
expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } }),
|
|
);
|
|
});
|
|
|
|
it('should work on conflict', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'old-id' } });
|
|
|
|
// verify existing metadata
|
|
await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([
|
|
expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'old-id' } }),
|
|
]);
|
|
|
|
const items = [{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'new-id' } }];
|
|
await sut.upsertBulkMetadata(auth, { items });
|
|
|
|
// verify updated metadata
|
|
await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([
|
|
expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'new-id' } }),
|
|
]);
|
|
});
|
|
|
|
it('should work with multiple assets', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
|
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
const items = [
|
|
{ assetId: asset1.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },
|
|
{ assetId: asset2.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } },
|
|
];
|
|
|
|
await sut.upsertBulkMetadata(auth, { items });
|
|
|
|
const metadata1 = await ctx.get(AssetRepository).getMetadata(asset1.id);
|
|
expect(metadata1).toEqual([
|
|
expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }),
|
|
]);
|
|
|
|
const metadata2 = await ctx.get(AssetRepository).getMetadata(asset2.id);
|
|
expect(metadata2).toEqual([
|
|
expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } }),
|
|
]);
|
|
});
|
|
|
|
it('should work with multiple metadata for the same asset', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
const items = [
|
|
{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },
|
|
{ assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } },
|
|
];
|
|
|
|
await sut.upsertBulkMetadata(auth, { items });
|
|
|
|
const metadata = await ctx.get(AssetRepository).getMetadata(asset.id);
|
|
expect(metadata).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
key: AssetMetadataKey.MobileApp,
|
|
value: { iCloudId: 'id1' },
|
|
}),
|
|
expect.objectContaining({
|
|
key: 'some-other-key',
|
|
value: { foo: 'bar' },
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('deleteBulkMetadata', () => {
|
|
it('should work', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } });
|
|
|
|
await sut.deleteBulkMetadata(auth, { items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }] });
|
|
|
|
const metadata = await ctx.get(AssetRepository).getMetadata(asset.id);
|
|
expect(metadata.length).toEqual(0);
|
|
});
|
|
|
|
it('should work even if the item does not exist', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
|
|
await sut.deleteBulkMetadata(auth, { items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }] });
|
|
|
|
const metadata = await ctx.get(AssetRepository).getMetadata(asset.id);
|
|
expect(metadata.length).toEqual(0);
|
|
});
|
|
|
|
it('should work with multiple assets', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset1.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } });
|
|
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset2.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } });
|
|
|
|
await sut.deleteBulkMetadata(auth, {
|
|
items: [
|
|
{ assetId: asset1.id, key: AssetMetadataKey.MobileApp },
|
|
{ assetId: asset2.id, key: AssetMetadataKey.MobileApp },
|
|
],
|
|
});
|
|
|
|
await expect(ctx.get(AssetRepository).getMetadata(asset1.id)).resolves.toEqual([]);
|
|
await expect(ctx.get(AssetRepository).getMetadata(asset2.id)).resolves.toEqual([]);
|
|
});
|
|
|
|
it('should work with multiple metadata for the same asset', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } });
|
|
await ctx.newMetadata({ assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } });
|
|
|
|
await sut.deleteBulkMetadata(auth, {
|
|
items: [
|
|
{ assetId: asset.id, key: AssetMetadataKey.MobileApp },
|
|
{ assetId: asset.id, key: 'some-other-key' },
|
|
],
|
|
});
|
|
|
|
await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([]);
|
|
});
|
|
|
|
it('should not delete unspecified keys', async () => {
|
|
const { sut, ctx } = setup();
|
|
const { user } = await ctx.newUser();
|
|
const auth = factory.auth({ user });
|
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
|
await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } });
|
|
await ctx.newMetadata({ assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } });
|
|
|
|
await sut.deleteBulkMetadata(auth, {
|
|
items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }],
|
|
});
|
|
|
|
const metadata = await ctx.get(AssetRepository).getMetadata(asset.id);
|
|
expect(metadata).toEqual([expect.objectContaining({ key: 'some-other-key', value: { foo: 'bar' } })]);
|
|
});
|
|
});
|
|
});
|