diff --git a/package.json b/package.json index 2b3106a1..859b702c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "eslint", "lint:fix": "eslint --fix", "test:typescript": "tstyche", - "test:unit": "c8 node --test", + "test:unit": "c8 --100 --all --include=index.js --include=lib/**/*.js --exclude=lib/schema-validator.js node --test", "test": "npm run test:unit && npm run test:typescript" }, "repository": { diff --git a/test/code-generation-fallbacks.test.js b/test/code-generation-fallbacks.test.js new file mode 100644 index 00000000..6dbcf703 --- /dev/null +++ b/test/code-generation-fallbacks.test.js @@ -0,0 +1,158 @@ +'use strict' + +const { test } = require('node:test') + +const OriginalLocation = require('../lib/location') + +const buildPath = require.resolve('..') +const locationPath = require.resolve('../lib/location') + +function loadBuildWithLocation (Location) { + require.cache[locationPath].exports = Location + delete require.cache[buildPath] + + try { + return require(buildPath) + } finally { + require.cache[locationPath].exports = OriginalLocation + delete require.cache[buildPath] + } +} + +class LocationWithoutSchemaId { + constructor (schema, _schemaId, jsonPointer = '#') { + this.schema = schema + this.schemaId = '' + this.jsonPointer = _schemaId ? _schemaId + jsonPointer : jsonPointer + } + + getPropertyLocation (propertyName) { + return new LocationWithoutSchemaId( + this.schema[propertyName], + '', + this.jsonPointer + '/' + propertyName + ) + } + + getSchemaRef () { + return this.schemaId + this.jsonPointer + } +} + +class LocationWithoutJsonPointer { + constructor (schema, schemaId) { + this.schema = schema + this.schemaId = schemaId + this.jsonPointer = '' + } + + getPropertyLocation (propertyName) { + return new LocationWithoutJsonPointer(this.schema[propertyName], this.schemaId) + } + + getSchemaRef () { + return this.schemaId + } +} + +class LocationWithoutSchemaRef extends OriginalLocation { + getSchemaRef () {} +} + +test('inline object generation without schema IDs', t => { + t.plan(3) + + const build = loadBuildWithLocation(LocationWithoutSchemaId) + const stringify = build({ + type: 'object', + properties: { + value: { type: 'string' } + } + }) + const stringifyNullable = build({ type: 'object', nullable: true }) + + t.assert.equal(stringify({ value: 'test' }), '{"value":"test"}') + t.assert.equal(stringify(null), '{}') + t.assert.equal(stringifyNullable(null), 'null') +}) + +test('inline array generation without schema IDs', t => { + t.plan(6) + + const build = loadBuildWithLocation(LocationWithoutSchemaId) + const stringify = build({ type: 'array', items: { type: 'string' } }, { largeArrayMechanism: 'default' }) + const stringifyNullable = build({ type: 'array', nullable: true }) + const stringifyTuple = build({ + type: 'array', + items: [{ $ref: 'item' }, { type: 'number' }], + additionalItems: true + }, { + schema: { + item: { type: 'string' } + } + }) + const stringifyFixedTuple = build({ + type: 'array', + items: [{ type: 'string' }], + additionalItems: false + }) + const stringifyLargeArray = build({ type: 'array', items: { type: 'number' } }, { + largeArrayMechanism: 'json-stringify', + largeArraySize: 1 + }) + + t.assert.equal(stringify(['one', 'two']), '["one","two"]') + t.assert.equal(stringifyNullable(null), 'null') + t.assert.equal(stringifyTuple(['one', 2, true]), '["one",2,true]') + t.assert.throws(() => stringifyFixedTuple(['one', 'two']), /Item at 1/) + t.assert.equal(stringifyLargeArray([1, 2]), '[1,2]') + t.assert.throws(() => stringify('not-an-array'), /does not match schema definition/) +}) + +test('code generation reference fallbacks', t => { + t.plan(3) + + const buildWithoutPointer = loadBuildWithLocation(LocationWithoutJsonPointer) + const stringifyObject = buildWithoutPointer({ type: 'object' }) + const stringifyArray = buildWithoutPointer({ type: 'array' }) + + const buildWithoutRef = loadBuildWithLocation(LocationWithoutSchemaRef) + const stringifyWithoutRef = buildWithoutRef({ type: 'object' }) + + t.assert.equal(stringifyObject({}), '{}') + t.assert.equal(stringifyArray([]), '[]') + t.assert.equal(stringifyWithoutRef({}), '{}') +}) + +test('required-property fallback tolerates unexpected property ordering', t => { + t.plan(2) + + const originalSort = Array.prototype.sort + // Deliberately fault-inject unexpected ordering to exercise the defensive branch. + // eslint-disable-next-line no-extend-native + Array.prototype.sort = function (compareFn) { + const result = originalSort.call(this, compareFn) + if (this.length === 2 && this.includes('optional') && this.includes('required')) { + result.reverse() + } + return result + } + + try { + const build = loadBuildWithLocation(OriginalLocation) + const stringify = build({ + type: 'object', + properties: { + optional: { type: 'string' }, + required: { type: 'string' } + }, + required: ['required'] + }) + + t.assert.equal(stringify({ optional: 'one', required: 'two' }), '{"optional":"one","required":"two"}') + t.assert.throws(() => stringify({ optional: 'one' }), /"required" is required/) + } finally { + // eslint-disable-next-line no-extend-native + Array.prototype.sort = originalSort + } +}) diff --git a/test/debug-mode.test.js b/test/debug-mode.test.js index 5000adb9..0607f7b2 100644 --- a/test/debug-mode.test.js +++ b/test/debug-mode.test.js @@ -128,3 +128,11 @@ test('Validator restoreFromState', t => { const restored = Validator.restoreFromState(state) t.assert.ok(restored instanceof Validator) }) + +test('Validator ignores null schemas while converting nested values', t => { + t.plan(1) + + const validator = new Validator() + + t.assert.doesNotThrow(() => validator.convertSchemaToAjvFormat(null)) +}) diff --git a/test/string.test.js b/test/string.test.js index 518513da..6c968065 100644 --- a/test/string.test.js +++ b/test/string.test.js @@ -4,6 +4,14 @@ const { test } = require('node:test') const build = require('..') +test('serialize an empty string', (t) => { + t.plan(1) + + const stringify = build({ type: 'string' }) + + t.assert.equal(stringify(''), '""') +}) + test('serialize short string', (t) => { t.plan(2)