diff --git a/json-schema/schema.json b/json-schema/schema.json index d701230..03fe0b1 100644 --- a/json-schema/schema.json +++ b/json-schema/schema.json @@ -17,14 +17,7 @@ "const": "Feature" }, "properties": { - "allOf": [ - { - "$ref": "#/definitions/require_any_field" - }, - { - "$ref": "#/definitions/fields" - } - ] + "$ref": "#/definitions/fields" }, "assets": { "$comment": "This validates the fields in Item Assets, but does not require them.", @@ -75,9 +68,7 @@ "allOf": [ { "$ref": "#/definitions/stac_extensions" - } - ], - "anyOf": [ + }, { "$comment": "Requires at least one provider to contain processing fields.", "type": "object", @@ -108,7 +99,7 @@ } }, { - "$ref": "#/definitions/require_any_field" + "$ref": "#/definitions/fields" } ] } @@ -118,18 +109,11 @@ { "$comment": "Requires at least one asset to contain processing fields.", "type": "object", - "required": [ - "assets" - ], "properties": { "assets": { "type": "object", - "not": { - "additionalProperties": { - "not": { - "$ref": "#/definitions/require_any_field" - } - } + "additionalProperties": { + "$ref": "#/definitions/fields" } } } @@ -137,18 +121,11 @@ { "$comment": "Requires at least one item asset definition to contain processing fields.", "type": "object", - "required": [ - "item_assets" - ], "properties": { "item_assets": { "type": "object", - "not": { - "additionalProperties": { - "not": { - "$ref": "#/definitions/require_any_field" - } - } + "additionalProperties": { + "$ref": "#/definitions/fields" } } } @@ -156,12 +133,46 @@ { "type": "object", "$comment": "Requires at least one summary to be a processing field.", - "required": [ - "summaries" - ], "properties": { "summaries": { - "$ref": "#/definitions/require_any_field" + "type": "object", + "properties": { + "processing:expression": { + "type": "array", + "items": { + "$ref": "#/definitions/fields/properties/processing:expression" + }, + "minItems": 1 + }, + "processing:facility": { + "type": "array", + "items": { + "$ref": "#/definitions/fields/properties/processing:facility" + }, + "minItems": 1 + }, + "processing:level": { + "type": "array", + "items": { + "$ref": "#/definitions/fields/properties/processing:level" + }, + "minItems": 1 + }, + "processing:lineage": { + "type": "array", + "items": { + "$ref": "#/definitions/fields/properties/processing:lineage" + }, + "minItems": 1 + }, + "processing:software": { + "type": "array", + "items": { + "$ref": "#/definitions/fields/properties/processing:software" + }, + "minItems": 1 + } + } } } } @@ -200,15 +211,6 @@ } } }, - "require_any_field": { - "anyOf": [ - {"type": "object", "required": ["processing:expression"]}, - {"type": "object", "required": ["processing:lineage"]}, - {"type": "object", "required": ["processing:level"]}, - {"type": "object", "required": ["processing:facility"]}, - {"type": "object", "required": ["processing:software"]} - ] - }, "fields": { "type": "object", "properties": { diff --git a/package.json b/package.json index df44906..16e7cb8 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,15 @@ "name": "stac-extensions", "version": "1.1.0", "scripts": { - "test": "npm run check-markdown && npm run check-examples", + "test": "jest && npm run check-markdown && npm run check-examples", "check-markdown": "remark . -f -r .github/remark.yaml", "check-examples": "stac-node-validator . --lint --verbose --schemaMap https://stac-extensions.github.io/processing/v1.1.0/schema.json=./json-schema/schema.json", "format-examples": "stac-node-validator . --format --schemaMap https://stac-extensions.github.io/processing/v1.1.0/schema.json=./json-schema/schema.json" }, + "type": "module", "dependencies": { + "ajv": "^8.8.2", + "jest": "^27.4.5", "remark-cli": "^8.0.0", "remark-lint": "^7.0.0", "remark-lint-no-html": "^2.0.0", diff --git a/tests/collection.test.js b/tests/collection.test.js new file mode 100644 index 0000000..9efb2ed --- /dev/null +++ b/tests/collection.test.js @@ -0,0 +1,179 @@ +const {join} = require('path'); +const {promises} = require('fs'); +const {AjvOptions, rootDirectory, schemaPath} = require('./validation.js'); +const ajv = new (require('ajv'))(AjvOptions); + +const examplePath = join(rootDirectory, 'examples/collection.json'); + +let validate; +beforeAll(async () => { + const data = JSON.parse(await promises.readFile(schemaPath)); + validate = await ajv.compileAsync(data); +}); + +let example; +beforeEach(async () => { + example = JSON.parse(await promises.readFile(examplePath)); +}); + +describe('Collection example', () => { + it('should pass validation', async () => { + let valid = validate(example); + + expect(valid).toBeTruthy(); + }); + + it('should fail validation when providers processing expression is invalid', async () => { + // given + example.providers[0]['processing:expression'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/providers/0/processing:expression' + && error.message === 'must be object', + ) + ).toBeTruthy(); + }); + + it('should fail validation when asset processing expression is invalid', async () => { + // given + example.assets = { + 'example': { + 'href': 'https://example.org/file.xyz', + 'processing:expression': null, + } + }; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/assets/example/processing:expression' + && error.message === 'must be object', + ) + ).toBeTruthy(); + }); + + it('should fail validation when item asset processing expression is invalid', async () => { + // given + example.item_assets = { + 'example': { + 'href': 'https://example.org/file.xyz', + 'processing:expression': null, + } + }; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/item_assets/example/processing:expression' + && error.message === 'must be object', + ) + ).toBeTruthy(); + }); + + it('should fail validation when summary processing expression is invalid', async () => { + // given + example.summaries['processing:expression'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/summaries/processing:expression' + && error.message === 'must be array', + ) + ).toBeTruthy(); + }); + + it('should fail validation when summary processing facility is invalid', async () => { + // given + example.summaries['processing:facility'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/summaries/processing:facility' + && error.message === 'must be array', + ) + ).toBeTruthy(); + }); + + it('should fail validation when summary processing level is invalid', async () => { + // given + example.summaries['processing:level'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/summaries/processing:level' + && error.message === 'must be array', + ) + ).toBeTruthy(); + }); + + it('should fail validation when summary processing lineage is invalid', async () => { + // given + example.summaries['processing:lineage'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/summaries/processing:lineage' + && error.message === 'must be array', + ) + ).toBeTruthy(); + }); + + it('should fail validation when summary processing software is invalid', async () => { + // given + example.summaries['processing:software'] = null; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/summaries/processing:software' + && error.message === 'must be array', + ) + ).toBeTruthy(); + }); +}); diff --git a/tests/item.test.js b/tests/item.test.js new file mode 100644 index 0000000..1cc0a94 --- /dev/null +++ b/tests/item.test.js @@ -0,0 +1,43 @@ +const {join} = require('path'); +const {promises} = require('fs'); +const {AjvOptions, rootDirectory, schemaPath} = require('./validation.js'); +const ajv = new (require('ajv'))(AjvOptions); + +const examplePath = join(rootDirectory, 'examples/item.json'); + +let validate; +beforeAll(async () => { + const data = JSON.parse(await promises.readFile(schemaPath)); + validate = await ajv.compileAsync(data); +}); + +let example; +beforeEach(async () => { + example = JSON.parse(await promises.readFile(examplePath)); +}); + +describe('Item example', () => { + it('should pass validation', async () => { + let valid = validate(example); + + expect(valid).toBeTruthy(); + }); + + it('should fail validation when processing expression is invalid', async () => { + // given + example.properties = {'processing:expression': null}; + + // when + let valid = validate(example); + + // then + expect(valid).toBeFalsy(); + expect( + validate.errors.some( + (error) => + error.instancePath === '/properties/processing:expression' + && error.message === 'must be object', + ) + ).toBeTruthy(); + }); +}); diff --git a/tests/validation.js b/tests/validation.js new file mode 100644 index 0000000..bfe02cf --- /dev/null +++ b/tests/validation.js @@ -0,0 +1,29 @@ +const axios = require('axios'); +const { dirname, join } = require('path'); +const iriFormats = require('stac-node-validator/iri.js'); + +const Schemas = new Map(); +const loadSchema = function (uri) { + let existing = Schemas.get(uri); + if (existing == null) { + existing = loadSchemaFromUri(uri); + Schemas.set(uri, existing); + } + return existing; +} + +/** + * function passed in to Ajv instance which allows us to load schemas from a url at run time. + */ +module.exports.loadSchemaFromUri = async function (uri) { + try { + let response = await axios.get(uri); + return response.data; + } catch (error) { + throw new Error(`-- Schema at '${uri}' not found. Please ensure all entries in 'stac_extensions' are valid.`); + } +} + +module.exports.AjvOptions = {loadSchema, formats: Object.assign(iriFormats)}; +module.exports.rootDirectory = dirname(__dirname); +module.exports.schemaPath = join(module.exports.rootDirectory, 'json-schema/schema.json');