Skip to content

Commit ff67683

Browse files
fix(CaaSMapper): handle broken references (#252)
* fix(CaaSMapper): handle broken references by returning null for entries with null identifiers --------- Co-authored-by: jan-philipp.nierlein <jan-philipp.nierlein@crownpeak.com>
1 parent 5b7ed41 commit ff67683

3 files changed

Lines changed: 148 additions & 2 deletions

File tree

integrationtests/FSXAProxyApiRemoteProjects.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describe('FSXAProxyAPIRemoteProjects should resolve references', () => {
155155
return res.status === 200
156156
},
157157
{
158-
timeoutMs: 10000,
158+
timeoutMs: 20000,
159159
pollIntervalMs: 500,
160160
errorMessage: 'PageRef not available in CaaS after creation',
161161
}

src/modules/CaaSMapper.spec.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
CaaSApi_CMSInputTextArea,
1919
CaaSApi_CMSInputToggle,
2020
CaaSApi_Content2Section,
21+
CaaSApi_DataEntries,
2122
CaaSApi_FSCatalog,
2223
CaaSApi_FSDataset,
2324
CaaSApi_FSIndex,
@@ -701,6 +702,28 @@ describe('CaaSMapper', () => {
701702
expect(mapper.mapDataEntries).not.toHaveBeenCalled()
702703
expect(mapper.registerReferencedItem).not.toHaveBeenCalled()
703704
})
705+
it('should return null and not register a reference if the DatasetReference target has a null identifier (broken reference)', async () => {
706+
const api = createApi()
707+
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
708+
mapper.registerReferencedItem = jest.fn()
709+
const entry = createDatasetReference()
710+
;(entry.value as any).target.identifier = null
711+
await expect(
712+
mapper.mapDataEntry(entry, createPath())
713+
).resolves.toBeNull()
714+
expect(mapper.registerReferencedItem).not.toHaveBeenCalled()
715+
})
716+
it('should return null and not register a reference if the DatasetReference target itself is null (broken reference)', async () => {
717+
const api = createApi()
718+
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
719+
mapper.registerReferencedItem = jest.fn()
720+
const entry = createDatasetReference()
721+
;(entry.value as any).target = null
722+
await expect(
723+
mapper.mapDataEntry(entry, createPath())
724+
).resolves.toBeNull()
725+
expect(mapper.registerReferencedItem).not.toHaveBeenCalled()
726+
})
704727
})
705728

706729
describe('CMS_INPUT_TOGGLE', () => {
@@ -929,6 +952,30 @@ describe('CaaSMapper', () => {
929952
entry.value!.remoteProject
930953
)
931954
})
955+
it('should return null and not register a reference on Media entries with a null identifier (broken reference)', async () => {
956+
const api = createApi()
957+
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
958+
mapper.registerReferencedItem = jest.fn()
959+
const path = createPath()
960+
const entry: CaaSApi_FSReference = {
961+
name: faker.lorem.word(),
962+
value: {
963+
fsType: 'Media',
964+
name: faker.lorem.word(),
965+
identifier: null as any,
966+
uid: faker.lorem.word(),
967+
uidType: 'MEDIASTORE_LEAF',
968+
url: faker.lorem.word(),
969+
mediaType: 'PICTURE',
970+
remoteProject: 'main',
971+
} as any,
972+
fsType: 'FS_REFERENCE',
973+
}
974+
await expect(
975+
mapper.mapDataEntry(entry, path)
976+
).resolves.toBeNull()
977+
expect(mapper.registerReferencedItem).not.toHaveBeenCalled()
978+
})
932979
it('should handle PageRef & GCAPage separately', async () => {
933980
const api = createApi()
934981
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
@@ -971,6 +1018,27 @@ describe('CaaSMapper', () => {
9711018
expectedGCARef
9721019
)
9731020
})
1021+
it('should return null on PageRef/GCAPage entries with a null identifier (broken reference)', async () => {
1022+
const api = createApi()
1023+
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
1024+
const path = createPath()
1025+
const entry: CaaSApi_FSReference = {
1026+
name: faker.lorem.word(),
1027+
value: {
1028+
fsType: 'PageRef',
1029+
name: faker.lorem.word(),
1030+
identifier: null as any,
1031+
uid: faker.lorem.word(),
1032+
uidType: 'SITESTORE_LEAF',
1033+
url: faker.lorem.word(),
1034+
remoteProject: 'remote-project',
1035+
},
1036+
fsType: 'FS_REFERENCE',
1037+
}
1038+
await expect(mapper.mapDataEntry(entry, path)).resolves.toBeNull()
1039+
entry.value!.fsType = 'GCAPage'
1040+
await expect(mapper.mapDataEntry(entry, path)).resolves.toBeNull()
1041+
})
9741042
it('should return corrupted entries as-is', async () => {
9751043
const api = createApi()
9761044
const mapper = new CaaSMapper(api, 'de', {}, createLogger())
@@ -1170,6 +1238,57 @@ describe('CaaSMapper', () => {
11701238
{}
11711239
)
11721240
})
1241+
// Regression test for CAAS-2680: a single broken Media reference anywhere in
1242+
// an element used to throw synchronously (unifyId -> null.indexOf) and reject
1243+
// the whole document. It must now be skipped (mapped to null) while the rest
1244+
// of the element is returned normally. Uses the real mapper (no mocks) so an
1245+
// unguarded null identifier would actually throw here.
1246+
it('should skip a broken/null Media reference and still return the rest of the element', async () => {
1247+
const mapper = new CaaSMapper(createApi(), 'de', {}, createLogger())
1248+
const entries = {
1249+
headline: {
1250+
fsType: 'CMS_INPUT_TEXT',
1251+
name: 'headline',
1252+
value: 'Weihnachtssocken',
1253+
},
1254+
brokenImage: {
1255+
fsType: 'FS_REFERENCE',
1256+
name: 'brokenImage',
1257+
value: {
1258+
fsType: 'Media',
1259+
name: faker.lorem.word(),
1260+
identifier: null as any,
1261+
uid: '01_221021_4f3_weihnachtssocken_stage_b_1',
1262+
uidType: 'MEDIASTORE_LEAF',
1263+
url: faker.lorem.word(),
1264+
mediaType: 'PICTURE',
1265+
remoteProject: 'main',
1266+
},
1267+
},
1268+
validImage: {
1269+
fsType: 'FS_REFERENCE',
1270+
name: 'validImage',
1271+
value: {
1272+
fsType: 'Media',
1273+
name: faker.lorem.word(),
1274+
identifier: faker.string.uuid(),
1275+
uid: faker.lorem.word(),
1276+
uidType: 'MEDIASTORE_LEAF',
1277+
url: faker.lorem.word(),
1278+
mediaType: 'PICTURE',
1279+
},
1280+
},
1281+
} as any as CaaSApi_DataEntries
1282+
1283+
const result = await mapper.mapDataEntries(entries, createPath())
1284+
1285+
// the broken reference is dropped, not thrown
1286+
expect(result.brokenImage).toBeNull()
1287+
// the rest of the element survives
1288+
expect(result.headline).toBe('Weihnachtssocken')
1289+
expect(result.validImage).not.toBeNull()
1290+
expect(typeof result.validImage).toBe('string')
1291+
})
11731292
})
11741293

11751294
describe('mapSection', () => {

src/modules/CaaSMapper.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ export class CaaSMapper {
364364
)
365365
)
366366
} else if (entry.value.fsType === 'DatasetReference') {
367+
if (!entry.value.target?.identifier) {
368+
this.logger.warn(
369+
'Skipping DatasetReference with a broken/null identifier',
370+
{ path: path.join('/') }
371+
)
372+
return null
373+
}
367374
return this.registerReferencedItem(
368375
entry.value.target.identifier,
369376
path,
@@ -413,12 +420,26 @@ export class CaaSMapper {
413420
case 'FS_REFERENCE':
414421
if (!entry.value) return null
415422
if (entry.value.fsType === 'Media') {
423+
if (!entry.value.identifier) {
424+
this.logger.warn(
425+
'Skipping Media reference with a broken/null identifier',
426+
{ path: path.join('/') }
427+
)
428+
return null
429+
}
416430
return this.registerReferencedItem(
417431
entry.value.identifier,
418432
path,
419433
entry.value.remoteProject || remoteProjectId
420434
)
421435
} else if (['PageRef', 'GCAPage'].includes(entry.value.fsType)) {
436+
if (!entry.value.identifier) {
437+
this.logger.warn(
438+
'Skipping PageRef/GCAPage reference with a broken/null identifier',
439+
{ path: path.join('/') }
440+
)
441+
return null
442+
}
422443
const reference: Reference = {
423444
type: 'Reference',
424445
referenceId: entry.value.identifier,
@@ -437,7 +458,13 @@ export class CaaSMapper {
437458
.map((record, index) => {
438459
const identifier: string | undefined =
439460
record?.value?.target?.identifier
440-
if (!identifier) return null
461+
if (!identifier) {
462+
this.logger.warn(
463+
'Skipping FS_INDEX record with a broken/null identifier',
464+
{ path: [...path, index].join('/') }
465+
)
466+
return null
467+
}
441468
return this.registerReferencedItem(
442469
identifier,
443470
[...path, index],

0 commit comments

Comments
 (0)