@contrail/flexplm 1.5.0-alpha.98b8b06 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.github/pull_request_template.md +31 -0
  2. package/.github/workflows/flexplm-lib.yml +27 -0
  3. package/.github/workflows/publish-to-npm.yml +121 -0
  4. package/CHANGELOG.md +45 -0
  5. package/lib/entity-processor/base-entity-processor.d.ts +0 -65
  6. package/lib/entity-processor/base-entity-processor.js +0 -71
  7. package/lib/entity-processor/base-entity-processor.spec.js +0 -1
  8. package/lib/index.d.ts +0 -1
  9. package/lib/index.js +0 -1
  10. package/lib/publish/base-process-publish-assortment.d.ts +0 -25
  11. package/lib/publish/base-process-publish-assortment.js +6 -60
  12. package/lib/publish/base-process-publish-assortment.spec.js +4 -22
  13. package/lib/publish/mockData.js +0 -5
  14. package/lib/transform/identifier-conversion-spec-mockData.js +6 -34
  15. package/lib/transform/identifier-conversion.d.ts +0 -36
  16. package/lib/transform/identifier-conversion.js +0 -36
  17. package/lib/transform/identifier-conversion.spec.js +0 -4
  18. package/lib/util/config-defaults.js +0 -3
  19. package/lib/util/config-defaults.spec.js +0 -9
  20. package/lib/util/data-converter-spec-mockData.js +3 -17
  21. package/lib/util/data-converter.d.ts +0 -97
  22. package/lib/util/data-converter.js +1 -127
  23. package/lib/util/data-converter.spec.js +0 -2
  24. package/lib/util/error-response-object.d.ts +0 -5
  25. package/lib/util/error-response-object.js +0 -7
  26. package/lib/util/event-short-message-status.js +0 -1
  27. package/lib/util/federation.js +0 -8
  28. package/lib/util/flexplm-connect.d.ts +0 -7
  29. package/lib/util/flexplm-connect.js +0 -14
  30. package/lib/util/logger-config.js +0 -1
  31. package/lib/util/map-util-spec-mockData.js +3 -17
  32. package/lib/util/map-utils.d.ts +0 -27
  33. package/lib/util/map-utils.js +0 -27
  34. package/lib/util/thumbnail-util.d.ts +0 -21
  35. package/lib/util/thumbnail-util.js +1 -28
  36. package/lib/util/thumbnail-util.spec.js +0 -6
  37. package/lib/util/type-conversion-utils-spec-mockData.js +3 -3
  38. package/lib/util/type-conversion-utils.d.ts +1 -152
  39. package/lib/util/type-conversion-utils.js +1 -155
  40. package/lib/util/type-defaults.d.ts +0 -66
  41. package/lib/util/type-defaults.js +0 -66
  42. package/lib/util/type-defaults.spec.js +5 -5
  43. package/lib/util/type-utils.d.ts +0 -21
  44. package/lib/util/type-utils.js +0 -23
  45. package/lib/util/type-utils.spec.js +0 -2
  46. package/package.json +6 -21
  47. package/publish.bat +5 -0
  48. package/publish.sh +5 -0
  49. package/src/entity-processor/base-entity-processor.spec.ts +689 -0
  50. package/src/entity-processor/base-entity-processor.ts +583 -0
  51. package/src/flexplm-request.ts +28 -0
  52. package/src/flexplm-utils.spec.ts +27 -0
  53. package/src/flexplm-utils.ts +29 -0
  54. package/src/index.ts +22 -0
  55. package/src/interfaces/interfaces.ts +122 -0
  56. package/src/interfaces/item-family-changes.ts +67 -0
  57. package/src/interfaces/publish-change-data.ts +43 -0
  58. package/src/publish/base-process-publish-assortment-callback.ts +50 -0
  59. package/src/publish/base-process-publish-assortment.spec.ts +1992 -0
  60. package/src/publish/base-process-publish-assortment.ts +1134 -0
  61. package/src/publish/mockData.ts +4561 -0
  62. package/src/transform/identifier-conversion-spec-mockData.ts +496 -0
  63. package/src/transform/identifier-conversion.spec.ts +386 -0
  64. package/src/transform/identifier-conversion.ts +282 -0
  65. package/src/util/config-defaults.spec.ts +445 -0
  66. package/src/util/config-defaults.ts +106 -0
  67. package/src/util/data-converter-spec-mockData.ts +231 -0
  68. package/src/util/data-converter.spec.ts +1622 -0
  69. package/src/util/data-converter.ts +819 -0
  70. package/src/util/error-response-object.spec.ts +116 -0
  71. package/src/util/error-response-object.ts +50 -0
  72. package/src/util/event-short-message-status.ts +22 -0
  73. package/src/util/federation.ts +172 -0
  74. package/src/util/flexplm-connect.spec.ts +132 -0
  75. package/src/util/flexplm-connect.ts +208 -0
  76. package/src/util/logger-config.ts +20 -0
  77. package/src/util/map-util-spec-mockData.ts +231 -0
  78. package/src/util/map-utils.spec.ts +103 -0
  79. package/src/util/map-utils.ts +41 -0
  80. package/src/util/mockData.ts +101 -0
  81. package/src/util/thumbnail-util.spec.ts +508 -0
  82. package/src/util/thumbnail-util.ts +272 -0
  83. package/src/util/type-conversion-utils-spec-mockData.ts +272 -0
  84. package/src/util/type-conversion-utils.spec.ts +1031 -0
  85. package/src/util/type-conversion-utils.ts +490 -0
  86. package/src/util/type-defaults.spec.ts +797 -0
  87. package/src/util/type-defaults.ts +320 -0
  88. package/src/util/type-utils.spec.ts +227 -0
  89. package/src/util/type-utils.ts +144 -0
  90. package/tsconfig.json +24 -0
  91. package/tslint.json +57 -0
  92. package/lib/cli/commands/compile.d.ts +0 -4
  93. package/lib/cli/commands/compile.js +0 -73
  94. package/lib/cli/commands/compile.spec.d.ts +0 -1
  95. package/lib/cli/commands/compile.spec.js +0 -80
  96. package/lib/cli/commands/create.d.ts +0 -5
  97. package/lib/cli/commands/create.js +0 -77
  98. package/lib/cli/commands/create.spec.d.ts +0 -1
  99. package/lib/cli/commands/create.spec.js +0 -78
  100. package/lib/cli/commands/upload.d.ts +0 -17
  101. package/lib/cli/commands/upload.js +0 -228
  102. package/lib/cli/commands/upload.spec.d.ts +0 -1
  103. package/lib/cli/commands/upload.spec.js +0 -88
  104. package/lib/cli/index.d.ts +0 -5
  105. package/lib/cli/index.js +0 -70
  106. package/lib/cli/index.spec.d.ts +0 -1
  107. package/lib/cli/index.spec.js +0 -85
  108. package/lib/cli/template/mapping-template.ts.template +0 -62
  109. package/lib/interfaces/mapping-file.d.ts +0 -460
  110. package/lib/interfaces/mapping-file.js +0 -2
  111. package/scripts/copy-template.js +0 -10
@@ -0,0 +1,508 @@
1
+ import { FCConfig } from '../interfaces/interfaces';
2
+ import { ThumbnailUtil } from './thumbnail-util';
3
+
4
+ import { empty_custom_sizes, four_custom_sizes, thumbnail_content_entity } from './mockData';
5
+
6
+ const mockEntitiesGet = jest.fn();
7
+ const mockEntitiesUpdate = jest.fn();
8
+ const mockEntitiesDelete = jest.fn();
9
+ const mockContentCreate = jest.fn();
10
+
11
+ jest.mock('@contrail/sdk', () => {
12
+ return {
13
+ Entities: jest.fn().mockImplementation(() => ({
14
+ get: mockEntitiesGet,
15
+ update: mockEntitiesUpdate,
16
+ delete: mockEntitiesDelete,
17
+ })),
18
+ Content: jest.fn().mockImplementation(() => ({
19
+ create: mockContentCreate,
20
+ })),
21
+ };
22
+ });
23
+
24
+ const mockGetRequest = jest.fn();
25
+ jest.mock('./flexplm-connect', () => {
26
+ return {
27
+ FlexPLMConnect: jest.fn().mockImplementation(() => ({
28
+ getRequest: mockGetRequest,
29
+ })),
30
+ };
31
+ });
32
+
33
+ describe('ThumbnailUtil Tests', () =>{
34
+ const config = {} as FCConfig;
35
+ describe('setOutboundThumbnail()', () =>{
36
+ const tu = new ThumbnailUtil(config);
37
+ beforeEach(() =>{
38
+ jest.clearAllMocks();
39
+ });
40
+
41
+ it('no data', async () =>{
42
+ const data = {};
43
+ const event = {};
44
+ const results = await tu.setOutboundThumbnail(data, event);
45
+ const keys = Object.keys(results);
46
+
47
+ expect(results).toEqual(data);
48
+ expect(keys.length).toEqual(0);
49
+ });
50
+
51
+ it('remove thumbnail', async () =>{
52
+ const data = {};
53
+ const event = {
54
+ propertyDiffs: {
55
+ largeViewableDownloadUrl: {
56
+ oldValue: '123',
57
+ newValue: null
58
+ }
59
+ }
60
+ };
61
+ const results = await tu.setOutboundThumbnail(data, event);
62
+ const keys = Object.keys(results);
63
+
64
+ expect(keys.length).toEqual(1);
65
+ expect(keys[0]).toEqual(ThumbnailUtil.NEW_THUMBNAIL_ID);
66
+ expect(results[ThumbnailUtil.NEW_THUMBNAIL_ID]).toEqual(ThumbnailUtil.REMOVE_THUMBNAIL);
67
+
68
+ });
69
+
70
+ it('calling getFileId - new thumbnail', async () =>{
71
+ const data = {};
72
+ const newDownloadUrl = 'https://api.vibeiq.com/dev/api/files/downloadUrl/w3ckfeGAD8ViOZj-%2Fcontent:E1iBQuWbr74lcdcw%2F9e30ds9o-d34b-451e-ae59-b4km36018d5a.png';
73
+ const oldDownloadUrl = 'xxx';
74
+ const thumbnailId = 'Eey3ZOiqdrUA84F8';
75
+ const event = {
76
+ newData: {
77
+ primaryViewableId: 'm5bJa4RtTLUtKBP5',
78
+ largeViewableDownloadUrl: newDownloadUrl
79
+ },
80
+ propertyDiffs: {
81
+ largeViewableDownloadUrl :{
82
+ newValue: newDownloadUrl,
83
+ oldValue: oldDownloadUrl
84
+ }
85
+ }
86
+ };
87
+
88
+ const spyCustomSizes = jest.spyOn(tu, 'getCustomSizes');
89
+ spyCustomSizes.mockReturnValue(Promise.resolve(empty_custom_sizes));
90
+ const spyGetFileId = jest.spyOn(tu, 'getFileId');
91
+ spyGetFileId.mockReturnValue(Promise.resolve(thumbnailId));
92
+ const results = await tu.setOutboundThumbnail(data, event);
93
+ const keys = Object.keys(results);
94
+ expect(keys.length).toEqual(1);
95
+ expect(keys[0]).toEqual(ThumbnailUtil.NEW_THUMBNAIL_ID);
96
+ const thumbnailValue = results[ThumbnailUtil.NEW_THUMBNAIL_ID];
97
+ expect(thumbnailValue).toEqual(thumbnailId);
98
+ });
99
+
100
+ it('calling getFileId - existing thumbnail', async () =>{
101
+ const data = {};
102
+ const newDownloadUrl = 'https://api.vibeiq.com/dev/api/files/downloadUrl/w3ckfeGAD8ViOZj-%2Fcontent:E1iBQuWbr74lcdcw%2F9e30ds9o-d34b-451e-ae59-b4km36018d5a.png';
103
+ const thumbnailId = 'Eey3ZOiqdrUA84F8';
104
+ const event = {
105
+ newData: {
106
+ primaryViewableId: 'm5bJa4RtTLUtKBP5',
107
+ largeViewableDownloadUrl: newDownloadUrl
108
+ },
109
+ propertyDiffs: {}
110
+ };
111
+
112
+ const spyCustomSizes = jest.spyOn(tu, 'getCustomSizes');
113
+ spyCustomSizes.mockReturnValue(Promise.resolve(empty_custom_sizes));
114
+ const spyGetFileId = jest.spyOn(tu, 'getFileId');
115
+ spyGetFileId.mockReturnValue(Promise.resolve(thumbnailId));
116
+ const results = await tu.setOutboundThumbnail(data, event);
117
+ const keys = Object.keys(results);
118
+ expect(keys.length).toEqual(1);
119
+ expect(keys[0]).toEqual(ThumbnailUtil.EXISTING_THUMBNAIL_ID);
120
+ const thumbnailValue = results[ThumbnailUtil.EXISTING_THUMBNAIL_ID];
121
+ expect(thumbnailValue).toEqual(thumbnailId);
122
+ });
123
+ });
124
+
125
+ describe('getFileId', () =>{
126
+ const primaryFileId = 'yn2d5oHD4rXHRzyB';
127
+ beforeEach(() =>{
128
+ jest.clearAllMocks();
129
+ });
130
+ it('no custom sizes, default max_thumbnail_size - result large', async () =>{
131
+ const tu = new ThumbnailUtil(config);
132
+ const spyCustomSizes = jest.spyOn(tu, 'getCustomSizes');
133
+ spyCustomSizes.mockReturnValue(Promise.resolve(empty_custom_sizes));
134
+ const spyContentEntity = jest.spyOn(tu, 'getContentEntity');
135
+ const content = JSON.parse(JSON.stringify(thumbnail_content_entity));
136
+
137
+ spyContentEntity.mockReturnValue(Promise.resolve(content));
138
+
139
+ const results = await tu.getFileId(primaryFileId);
140
+ expect(results).toEqual('vo4N4mCd-tFrw101');
141
+ });
142
+
143
+ it('no custom sizes, 750 * 1_024 max_thumbnail_size - result medium', async () =>{
144
+ const config1 = {
145
+ max_thumbnail_size: 750 * 1_024
146
+ } as any as FCConfig;
147
+ const tu = new ThumbnailUtil(config1);
148
+ const spyCustomSizes = jest.spyOn(tu, 'getCustomSizes');
149
+ spyCustomSizes.mockReturnValue(Promise.resolve(empty_custom_sizes));
150
+ const spyContentEntity = jest.spyOn(tu, 'getContentEntity');
151
+ const content = thumbnail_content_entity;
152
+ spyContentEntity.mockReturnValue(Promise.resolve(content));
153
+
154
+ const results = await tu.getFileId(primaryFileId);
155
+ expect(results).toEqual('AsRvJenpeqxksUNW');
156
+ });
157
+
158
+ it('custom sizes, 750 * 1_024 max_thumbnail_size - result CS_500Id', async () =>{
159
+ const config1 = {
160
+ max_thumbnail_size: 750 * 1_024
161
+ } as any as FCConfig;
162
+ const tu = new ThumbnailUtil(config1);
163
+ const spyCustomSizes = jest.spyOn(tu, 'getCustomSizes');
164
+ spyCustomSizes.mockReturnValue(Promise.resolve(four_custom_sizes));
165
+ const spyContentEntity = jest.spyOn(tu, 'getContentEntity');
166
+ const content = thumbnail_content_entity;
167
+ spyContentEntity.mockReturnValue(Promise.resolve(content));
168
+
169
+ const results = await tu.getFileId(primaryFileId);
170
+ expect(results).toEqual('rBaHc1J2xdOWdbhI');
171
+ });
172
+
173
+ });
174
+
175
+ describe('Test isThumbnailNew', () =>{
176
+ const tu = new ThumbnailUtil(config);
177
+ it('no propertyDiffs', () =>{
178
+ const event = {
179
+ newData: {
180
+ name: 'Test'
181
+ },
182
+ oldData:{
183
+ name: 'Test'
184
+ }
185
+ };
186
+ const results = tu.isThumbnailNew(event, undefined);
187
+ expect(results).toBeFalsy();
188
+ });
189
+
190
+ it('empty propertyDiffs', () =>{
191
+ const event = {
192
+ newData: {
193
+ name: 'Test'
194
+ },
195
+ oldData:{
196
+ name: 'Test'
197
+ },
198
+ propertyDiffs: {}
199
+ };
200
+ const results = tu.isThumbnailNew(event, undefined);
201
+ expect(results).toBeFalsy();
202
+ });
203
+
204
+ it('propertyDiffs diff property', () =>{
205
+ const event = {
206
+ newData: {
207
+ name: 'Test1'
208
+ },
209
+ oldData:{
210
+ name: 'Test'
211
+ },
212
+ propertyDiffs: {
213
+ name: {
214
+ propertyName: 'name',
215
+ oldValue: 'Test',
216
+ newValue: 'Test1'
217
+ }
218
+ }
219
+ };
220
+ const results = tu.isThumbnailNew(event, undefined);
221
+ expect(results).toBeFalsy();
222
+ });
223
+
224
+ it('propertyDiffs OOB property', () =>{
225
+ const event = {
226
+ newData: {
227
+ name: 'Test1'
228
+ },
229
+ oldData:{
230
+ name: 'Test'
231
+ },
232
+ propertyDiffs: {
233
+ mediumLargeViewableDownloadUrl: {
234
+ propertyName: 'mediumLargeViewableDownloadUrl',
235
+ oldValue: '',
236
+ newValue: 'Test1'
237
+ }
238
+ }
239
+ };
240
+ const results = tu.isThumbnailNew(event, undefined);
241
+ expect(results).toBeTruthy();
242
+ });
243
+
244
+ it('propertyDiffs custom property', () =>{
245
+ const event = {
246
+ newData: {
247
+ name: 'Test1'
248
+ },
249
+ oldData:{
250
+ name: 'Test'
251
+ },
252
+ propertyDiffs: {
253
+ RL_1000Url: {
254
+ propertyName: 'RL_1000Url',
255
+ oldValue: '',
256
+ newValue: 'Test1'
257
+ }
258
+ }
259
+ };
260
+ const results = tu.isThumbnailNew(event, [{slug: 'RL_1000'}]);
261
+ expect(results).toBeTruthy();
262
+ });
263
+ });
264
+
265
+ describe('logContentResults', () =>{
266
+ it('no content - doesnt error', () =>{
267
+ const tu = new ThumbnailUtil(config);
268
+ const content = undefined;
269
+ const relations = [];
270
+ tu.logContentResults(content, relations);
271
+ });
272
+ it('no relations - doesnt error', () =>{
273
+ const tu = new ThumbnailUtil(config);
274
+ const content = undefined;
275
+ const relations = undefined;
276
+ tu.logContentResults(content, relations);
277
+ });
278
+
279
+ it('content and relations - logs', () =>{
280
+ const tu = new ThumbnailUtil(config);
281
+ const content = {
282
+ id: '123',
283
+ name: 'Test',
284
+ primaryFile: { id:'file123'}
285
+ };
286
+ const relations = ['primaryFile'];
287
+ tu.logContentResults(content, relations);
288
+
289
+ expect(Object.keys(content)).toHaveLength(3);
290
+ expect(content).toHaveProperty('primaryFile');
291
+ expect(content.primaryFile).toHaveProperty('id');
292
+ expect(content.primaryFile.id).toEqual('file123');
293
+ });
294
+
295
+ });
296
+
297
+ describe('syncThumbnailToVibeIQ', () => {
298
+ let tu: ThumbnailUtil;
299
+
300
+ beforeEach(() => {
301
+ jest.clearAllMocks();
302
+ tu = new ThumbnailUtil(config);
303
+ mockEntitiesGet.mockImplementation((opts) => {
304
+ if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
305
+ return Promise.resolve({});
306
+ });
307
+ mockEntitiesUpdate.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
308
+ mockEntitiesDelete.mockImplementation((opts) => Promise.resolve({ id: opts.id }));
309
+ });
310
+
311
+ it('does not update when no thumbnail IDs in event data', async () => {
312
+ const event = { data: {} };
313
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
314
+
315
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
316
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
317
+ expect(mockContentCreate).not.toHaveBeenCalled();
318
+ });
319
+
320
+ it('does not update when event.data is undefined', async () => {
321
+ const event = {};
322
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
323
+
324
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
325
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it('REMOVE_THUMBNAIL with existing primaryViewableId deletes content and updates entity', async () => {
329
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: ThumbnailUtil.REMOVE_THUMBNAIL } };
330
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
331
+
332
+ expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'pv1' });
333
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'color', id: 'entity1' }));
334
+ });
335
+
336
+ it('REMOVE_THUMBNAIL with no primaryViewableId updates entity without deleting', async () => {
337
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: ThumbnailUtil.REMOVE_THUMBNAIL } };
338
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
339
+
340
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
341
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(expect.objectContaining({ entityName: 'item', id: 'entity1' }));
342
+ });
343
+
344
+ it('creates new content when no primaryViewableId exists and updates entity', async () => {
345
+ const mockResponse = {
346
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
347
+ headers: { get: jest.fn().mockReturnValue('image/png') },
348
+ };
349
+ mockGetRequest.mockResolvedValue(mockResponse);
350
+
351
+ const createdContent = {
352
+ id: 'newContent1',
353
+ contentType: 'image/png',
354
+ fileName: 'thumb.png',
355
+ primaryFileUrl: 'https://files/primary.png',
356
+ largeViewableUrl: 'https://files/large.png',
357
+ mediumLargeViewableUrl: null,
358
+ mediumViewableUrl: null,
359
+ smallViewableUrl: null,
360
+ tinyViewableUrl: null,
361
+ };
362
+ mockContentCreate.mockResolvedValue(createdContent);
363
+
364
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/thumb.png' } };
365
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
366
+
367
+ expect(mockGetRequest).toHaveBeenCalledWith({ urlPath: '/rest/thumbnail/thumb.png', includeUrlContext: false, returnFullResponse: true });
368
+ expect(mockContentCreate).toHaveBeenCalledWith(
369
+ expect.objectContaining({
370
+ fileName: 'thumb.png',
371
+ contentType: 'image/png',
372
+ contentHolderReference: 'color:entity1',
373
+ }),
374
+ );
375
+ // Updates content with flexplmThumbnailUrl
376
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
377
+ expect.objectContaining({
378
+ entityName: 'content',
379
+ id: 'newContent1',
380
+ object: { flexplmThumbnailUrl: '/rest/thumbnail/thumb.png' },
381
+ }),
382
+ );
383
+ // Updates the main entity
384
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
385
+ expect.objectContaining({
386
+ entityName: 'color',
387
+ id: 'entity1',
388
+ }),
389
+ );
390
+ });
391
+
392
+ it('encodes URL path segments with special characters', async () => {
393
+ const mockResponse = {
394
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
395
+ headers: { get: jest.fn().mockReturnValue('image/png') },
396
+ };
397
+ mockGetRequest.mockResolvedValue(mockResponse);
398
+ mockContentCreate.mockResolvedValue({
399
+ id: 'c1', contentType: 'image/png', fileName: 'my image.png',
400
+ primaryFileUrl: 'https://files/primary.png', largeViewableUrl: null,
401
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
402
+ });
403
+
404
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/my image.png' } };
405
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'color' });
406
+
407
+ expect(mockGetRequest).toHaveBeenCalledWith({
408
+ urlPath: '/rest/thumbnail/my%20image.png',
409
+ includeUrlContext: false,
410
+ returnFullResponse: true,
411
+ });
412
+ expect(mockContentCreate).toHaveBeenCalledWith(
413
+ expect.objectContaining({ fileName: 'my image.png' }),
414
+ );
415
+ });
416
+
417
+ it('encodes URL path segments with unicode characters', async () => {
418
+ const mockResponse = {
419
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
420
+ headers: { get: jest.fn().mockReturnValue('image/jpeg') },
421
+ };
422
+ mockGetRequest.mockResolvedValue(mockResponse);
423
+ mockContentCreate.mockResolvedValue({
424
+ id: 'c2', contentType: 'image/jpeg', fileName: 'café-logo.jpg',
425
+ primaryFileUrl: 'https://files/primary.jpg', largeViewableUrl: null,
426
+ mediumLargeViewableUrl: null, mediumViewableUrl: null, smallViewableUrl: null, tinyViewableUrl: null,
427
+ });
428
+
429
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/café-logo.jpg' } };
430
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', event, entityName: 'item' });
431
+
432
+ expect(mockGetRequest).toHaveBeenCalledWith({
433
+ urlPath: '/rest/thumbnail/caf%C3%A9-logo.jpg',
434
+ includeUrlContext: false,
435
+ returnFullResponse: true,
436
+ });
437
+ expect(mockContentCreate).toHaveBeenCalledWith(
438
+ expect.objectContaining({ fileName: 'café-logo.jpg' }),
439
+ );
440
+ });
441
+
442
+ it('replaces content when primaryViewable.flexplmThumbnailUrl differs and updates entity', async () => {
443
+ mockEntitiesGet.mockImplementation((opts) => {
444
+ if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
445
+ if (opts.entityName === 'content' && opts.id === 'oldPv') {
446
+ return Promise.resolve({ id: 'oldPv', flexplmThumbnailUrl: '/rest/thumbnail/old.png' });
447
+ }
448
+ return Promise.resolve({});
449
+ });
450
+
451
+ const mockResponse = {
452
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(8)),
453
+ headers: { get: jest.fn().mockReturnValue('image/jpeg') },
454
+ };
455
+ mockGetRequest.mockResolvedValue(mockResponse);
456
+
457
+ const createdContent = {
458
+ id: 'newContent2',
459
+ contentType: 'image/jpeg',
460
+ fileName: 'new.jpg',
461
+ primaryFileUrl: 'https://files/new-primary.jpg',
462
+ largeViewableUrl: null,
463
+ mediumLargeViewableUrl: null,
464
+ mediumViewableUrl: null,
465
+ smallViewableUrl: null,
466
+ tinyViewableUrl: null,
467
+ };
468
+ mockContentCreate.mockResolvedValue(createdContent);
469
+
470
+ const event = { data: { [ThumbnailUtil.NEW_THUMBNAIL_ID]: '/rest/thumbnail/new.jpg' } };
471
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'oldPv', event, entityName: 'item' });
472
+
473
+ // Creates new content
474
+ expect(mockContentCreate).toHaveBeenCalled();
475
+ // Updates new content with flexplmThumbnailUrl
476
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
477
+ expect.objectContaining({
478
+ entityName: 'content',
479
+ id: 'newContent2',
480
+ object: { flexplmThumbnailUrl: '/rest/thumbnail/new.jpg' },
481
+ }),
482
+ );
483
+ // Updates the main entity
484
+ expect(mockEntitiesUpdate).toHaveBeenCalledWith(
485
+ expect.objectContaining({ entityName: 'item', id: 'entity1' }),
486
+ );
487
+ // Deletes old content
488
+ expect(mockEntitiesDelete).toHaveBeenCalledWith({ entityName: 'content', id: 'oldPv' });
489
+ });
490
+
491
+ it('does not update when primaryViewable.flexplmThumbnailUrl matches', async () => {
492
+ const thumbnailUrl = '/rest/thumbnail/same.png';
493
+ mockEntitiesGet.mockImplementation((opts) => {
494
+ if (opts.entityName === 'content-custom-size') return Promise.resolve([]);
495
+ if (opts.entityName === 'content' && opts.id === 'pv1') {
496
+ return Promise.resolve({ id: 'pv1', flexplmThumbnailUrl: thumbnailUrl });
497
+ }
498
+ return Promise.resolve({});
499
+ });
500
+
501
+ const event = { data: { [ThumbnailUtil.EXISTING_THUMBNAIL_ID]: thumbnailUrl } };
502
+ await tu.syncThumbnailToVibeIQ({ entityId: 'entity1', primaryViewableId: 'pv1', event, entityName: 'color' });
503
+ expect(mockContentCreate).not.toHaveBeenCalled();
504
+ expect(mockEntitiesUpdate).not.toHaveBeenCalled();
505
+ expect(mockEntitiesDelete).not.toHaveBeenCalled();
506
+ });
507
+ });
508
+ });