diff --git a/src/rules/data-quality/openactive-urls-correct-rule-spec.js b/src/rules/data-quality/openactive-urls-correct-rule-spec.js index ef9e625..7f75634 100644 --- a/src/rules/data-quality/openactive-urls-correct-rule-spec.js +++ b/src/rules/data-quality/openactive-urls-correct-rule-spec.js @@ -76,6 +76,80 @@ describe('OpenactiveUrlsCorrectRule', () => { } }); + it('should return no errors if the context array contains non-string entries', async () => { + // JSON-LD permits an @context array to combine IRIs with inline context + // definitions. Those entries are not URLs, so this rule has nothing to + // check and must not throw. ContextInRootNodeRule reports them instead. + const dataItems = [ + { + '@context': [metaData.contextUrl, { schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [{ schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, null], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, 42], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, ['https://schema.org/']], + '@type': 'Event', + }, + ]; + + for (const data of dataItems) { + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + } + }); + + it('should still return an error for an incorrect OpenActive URL alongside a non-string entry', async () => { + const dataItems = [ + { + '@context': [{ schema: 'https://schema.org/' }, 'http://openactive.io/'], + '@type': 'Event', + }, + { + '@context': ['http://www.openactive.io/', { schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [null, 'https://www.openactive.io/'], + '@type': 'Event', + }, + ]; + + for (const data of dataItems) { + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + + for (const error of errors) { + expect(error.type).toBe(ValidationErrorType.INVALID_FORMAT); + expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); + } + } + }); + it('should return an error if the context is present, but contains a field not matching the correct scheme / domain', async () => { const dataItems = [ { diff --git a/src/rules/data-quality/openactive-urls-correct-rule.js b/src/rules/data-quality/openactive-urls-correct-rule.js index 8af047a..6159921 100644 --- a/src/rules/data-quality/openactive-urls-correct-rule.js +++ b/src/rules/data-quality/openactive-urls-correct-rule.js @@ -34,7 +34,13 @@ module.exports = class OpenactiveUrlsCorrectRule extends Rule { if (typeof fieldValue === 'string') { contexts.push(fieldValue); } else if (fieldValue instanceof Array) { - contexts = fieldValue.slice(); + // A JSON-LD @context array may legitimately combine IRIs with inline + // context definitions (maps). This rule can only inspect IRIs, so + // non-string entries are filtered out here. They are reported + // separately by ContextInRootNodeRule, which raises a failure for any + // @context that is not a URL or an array of URLs. Filtering keeps that + // error reachable rather than throwing before it can be raised. + contexts = fieldValue.filter((context) => typeof context === 'string'); } for (const context of contexts) { diff --git a/src/validate-spec.js b/src/validate-spec.js index 99d85bb..d4fe631 100644 --- a/src/validate-spec.js +++ b/src/validate-spec.js @@ -619,6 +619,28 @@ describe('validate', () => { expect(typeof result).toBe('object'); }); + it('should not throw if @context contains an inline context object', async () => { + // Regression test: an @context array combining an IRI with an inline + // context definition is valid JSON-LD but not permitted by OpenActive. + // It must be reported as a conformance failure, not crash the run. + const data = { + '@context': [metaData.contextUrl, { schema: 'https://schema.org/' }], + '@type': 'Event', + name: 'Tai chi Class', + }; + + const result = await validate(data, options); + + const contextErrors = result.filter( + (error) => error.rule === 'ContextInRootNodeRule', + ); + + expect(contextErrors.length).toBe(1); + expect(contextErrors[0].type).toBe(ValidationErrorType.INVALID_TYPE); + expect(contextErrors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + expect(contextErrors[0].path).toBe('$["@context"]'); + }); + it('should return an unsupported warning if nested arrays are passed', async () => { const event = { ...validSessionSeries };