From 9cfe2c8fb708cfbdff27e83f5eea84e2c2888d52 Mon Sep 17 00:00:00 2001 From: Luca Del Puppo Date: Mon, 17 Aug 2026 11:04:53 +0000 Subject: [PATCH 1/3] test: enforce 100% code coverage --- package.json | 2 +- test/code-generation-fallbacks.test.js | 158 +++++++++++++++++++++++++ test/debug-mode.test.js | 8 ++ test/string.test.js | 8 ++ 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 test/code-generation-fallbacks.test.js 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) From ab4f93ac07c4ddfe93876beebe2cf2581bb8443c Mon Sep 17 00:00:00 2001 From: Luca Del Puppo Date: Mon, 17 Aug 2026 12:31:08 +0000 Subject: [PATCH 2/3] test: move coverage settings to c8 config --- .c8rc.json | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .c8rc.json diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 00000000..7f4ecac0 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,15 @@ +{ + "all": true, + "check-coverage": true, + "lines": 100, + "functions": 100, + "branches": 100, + "statements": 100, + "include": [ + "index.js", + "lib/**/*.js" + ], + "exclude": [ + "lib/schema-validator.js" + ] +} diff --git a/package.json b/package.json index 859b702c..2b3106a1 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "eslint", "lint:fix": "eslint --fix", "test:typescript": "tstyche", - "test:unit": "c8 --100 --all --include=index.js --include=lib/**/*.js --exclude=lib/schema-validator.js node --test", + "test:unit": "c8 node --test", "test": "npm run test:unit && npm run test:typescript" }, "repository": { From fe73cd769f93be5eb7da29cfd0330cc7faadd430 Mon Sep 17 00:00:00 2001 From: Luca Del Puppo Date: Mon, 17 Aug 2026 13:21:08 +0000 Subject: [PATCH 3/3] test: keep coverage settings in test script --- .c8rc.json | 15 --------------- package.json | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 .c8rc.json diff --git a/.c8rc.json b/.c8rc.json deleted file mode 100644 index 7f4ecac0..00000000 --- a/.c8rc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "all": true, - "check-coverage": true, - "lines": 100, - "functions": 100, - "branches": 100, - "statements": 100, - "include": [ - "index.js", - "lib/**/*.js" - ], - "exclude": [ - "lib/schema-validator.js" - ] -} 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": {