diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index eb594fbe47..bd5a4579ec 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -53,7 +53,7 @@ describe(AssetMediaController.name, () => { }); it('should accept metadata', async () => { - const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } }; + const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123', iCloudIdETag: 'etag123' } }; const { status } = await request(ctx.getHttpServer()) .post('/assets') .attach('assetData', assetData, filename) @@ -98,6 +98,19 @@ describe(AssetMediaController.name, () => { expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudId must be a string'])); }); + it('should validate iCloudIdETag is a string', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ + ...makeUploadDto(), + metadata: JSON.stringify([{ key: AssetMetadataKey.MobileApp, value: { iCloudIdETag: 123 } }]), + }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudIdETag must be a string'])); + }); + it('should require `deviceAssetId`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 649c80e850..8e84bc9e46 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -190,6 +190,28 @@ describe(AssetController.name, () => { describe(AssetMetadataKey.MobileApp, () => { it('should accept valid data and pass to service correctly', async () => { + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()) + .put(`/assets/${assetId}/metadata`) + .send({ items: [{ key: 'mobile-app', value: { iCloudId: '123', iCloudIdETag: 'etag123' } }] }); + expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { + items: [{ key: 'mobile-app', value: { iCloudId: '123', iCloudIdETag: 'etag123' } }], + }); + expect(status).toBe(200); + }); + + it('should work without iCloudId', async () => { + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()) + .put(`/assets/${assetId}/metadata`) + .send({ items: [{ key: 'mobile-app', value: { iCloudIdETag: 'etag123' } }] }); + expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { + items: [{ key: 'mobile-app', value: { iCloudIdETag: 'etag123' } }], + }); + expect(status).toBe(200); + }); + + it('should work without iCloudIdETag', async () => { const assetId = factory.uuid(); const { status } = await request(ctx.getHttpServer()) .put(`/assets/${assetId}/metadata`) @@ -200,7 +222,7 @@ describe(AssetController.name, () => { expect(status).toBe(200); }); - it('should work without iCloudId', async () => { + it('should work with empty object', async () => { const assetId = factory.uuid(); const { status } = await request(ctx.getHttpServer()) .put(`/assets/${assetId}/metadata`) diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index dc43a0200c..215c9e85dc 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -177,6 +177,10 @@ export class AssetMetadataMobileAppDto { @IsString() @Optional() iCloudId?: string; + + @IsString() + @Optional() + iCloudIdETag?: string; } export class AssetMetadataResponseDto { diff --git a/server/src/types.ts b/server/src/types.ts index afc72480e8..742f162e56 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -551,5 +551,5 @@ export type AssetMetadataItem }; export interface AssetMetadata extends Record> { - [AssetMetadataKey.MobileApp]: { iCloudId: string }; + [AssetMetadataKey.MobileApp]: { iCloudId: string; iCloudIdETag: string }; } diff --git a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts index 8ba9630520..99455ffdc5 100644 --- a/server/test/medium/specs/sync/sync-asset-metadata.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-metadata.spec.ts @@ -23,7 +23,9 @@ describe(SyncEntityType.AssetMetadataV1, () => { const assetRepo = ctx.get(AssetRepository); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + await assetRepo.upsertMetadata(asset.id, [ + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' } }, + ]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); expect(response).toEqual([ @@ -32,7 +34,7 @@ describe(SyncEntityType.AssetMetadataV1, () => { data: { key: AssetMetadataKey.MobileApp, assetId: asset.id, - value: { iCloudId: 'abc123' }, + value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' }, }, type: 'AssetMetadataV1', }, @@ -48,7 +50,9 @@ describe(SyncEntityType.AssetMetadataV1, () => { const assetRepo = ctx.get(AssetRepository); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + await assetRepo.upsertMetadata(asset.id, [ + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' } }, + ]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); expect(response).toEqual([ @@ -57,7 +61,7 @@ describe(SyncEntityType.AssetMetadataV1, () => { data: { key: AssetMetadataKey.MobileApp, assetId: asset.id, - value: { iCloudId: 'abc123' }, + value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' }, }, type: 'AssetMetadataV1', }, @@ -66,7 +70,9 @@ describe(SyncEntityType.AssetMetadataV1, () => { await ctx.syncAckAll(auth, response); - await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc456' } }]); + await assetRepo.upsertMetadata(asset.id, [ + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc456', iCloudIdETag: 'etag456' } }, + ]); const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); expect(updatedResponse).toEqual([ @@ -75,7 +81,7 @@ describe(SyncEntityType.AssetMetadataV1, () => { data: { key: AssetMetadataKey.MobileApp, assetId: asset.id, - value: { iCloudId: 'abc456' }, + value: { iCloudId: 'abc456', iCloudIdETag: 'etag456' }, }, type: 'AssetMetadataV1', }, @@ -93,7 +99,9 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { const assetRepo = ctx.get(AssetRepository); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); + await assetRepo.upsertMetadata(asset.id, [ + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' } }, + ]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); expect(response).toEqual([ @@ -102,7 +110,7 @@ describe(SyncEntityType.AssetMetadataDeleteV1, () => { data: { key: AssetMetadataKey.MobileApp, assetId: asset.id, - value: { iCloudId: 'abc123' }, + value: { iCloudId: 'abc123', iCloudIdETag: 'etag123' }, }, type: 'AssetMetadataV1', },