yomichan/test/test-json-schema.js

1011 lines
30 KiB
JavaScript
Raw Normal View History

2020-02-23 18:05:48 +00:00
/*
* Copyright (C) 2020-2022 Yomichan Authors
2020-02-23 18:05:48 +00:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const assert = require('assert');
const {testMain} = require('../dev/util');
const {VM} = require('../dev/vm');
const vm = new VM();
vm.execute([
Move mixed/js (#1383) * Move mixed/js/core.js to js/core.js * Move mixed/js/yomichan.js to js/yomichan.js * Move mixed/js/timer.js to js/debug/timer.js * Move mixed/js/hotkey-handler.js to js/input/hotkey-handler.js * Move mixed/js/hotkey-help-controller.js to js/input/hotkey-help-controller.js * Move mixed/js/hotkey-util.js to js/input/hotkey-util.js * Move mixed/js/audio-system.js to js/input/audio-system.js * Move mixed/js/media-loader.js to js/input/media-loader.js * Move mixed/js/text-to-speech-audio.js to js/input/text-to-speech-audio.js * Move mixed/js/comm.js to js/comm/cross-frame-api.js * Move mixed/js/api.js to js/comm/api.js * Move mixed/js/frame-client.js to js/comm/frame-client.js * Move mixed/js/frame-endpoint.js to js/comm/frame-endpoint.js * Move mixed/js/display.js to js/display/display.js * Move mixed/js/display-audio.js to js/display/display-audio.js * Move mixed/js/display-generator.js to js/display/display-generator.js * Move mixed/js/display-history.js to js/display/display-history.js * Move mixed/js/display-notification.js to js/display/display-notification.js * Move mixed/js/display-profile-selection.js to js/display/display-profile-selection.js * Move mixed/js/japanese.js to js/language/japanese-util.js * Move mixed/js/dictionary-data-util.js to js/language/dictionary-data-util.js * Move mixed/js/document-focus-controller.js to js/dom/document-focus-controller.js * Move mixed/js/document-util.js to js/dom/document-util.js * Move mixed/js/dom-data-binder.js to js/dom/dom-data-binder.js * Move mixed/js/html-template-collection.js to js/dom/html-template-collection.js * Move mixed/js/panel-element.js to js/dom/panel-element.js * Move mixed/js/popup-menu.js to js/dom/popup-menu.js * Move mixed/js/selector-observer.js to js/dom/selector-observer.js * Move mixed/js/scroll.js to js/dom/window-scroll.js * Move mixed/js/text-scanner.js to js/language/text-scanner.js * Move mixed/js/cache-map.js to js/general/cache-map.js * Move mixed/js/object-property-accessor.js to js/general/object-property-accessor.js * Move mixed/js/task-accumulator.js to js/general/task-accumulator.js * Move mixed/js/environment.js to js/background/environment.js * Move mixed/js/dynamic-loader.js to js/scripting/dynamic-loader.js * Move mixed/js/dynamic-loader-sentinel.js to js/scripting/dynamic-loader-sentinel.js
2021-02-14 03:52:28 +00:00
'js/core.js',
'js/general/cache-map.js',
Move bg/js (#1387) * Move bg/js/anki.js to js/comm/anki.js * Move bg/js/mecab.js to js/comm/mecab.js * Move bg/js/search-main.js to js/display/search-main.js * Move bg/js/template-patcher.js to js/templates/template-patcher.js * Move bg/js/template-renderer-frame-api.js to js/templates/template-renderer-frame-api.js * Move bg/js/template-renderer-frame-main.js to js/templates/template-renderer-frame-main.js * Move bg/js/template-renderer-proxy.js to js/templates/template-renderer-proxy.js * Move bg/js/template-renderer.js to js/templates/template-renderer.js * Move bg/js/media-utility.js to js/media/media-utility.js * Move bg/js/native-simple-dom-parser.js to js/dom/native-simple-dom-parser.js * Move bg/js/simple-dom-parser.js to js/dom/simple-dom-parser.js * Move bg/js/audio-downloader.js to js/media/audio-downloader.js * Move bg/js/deinflector.js to js/language/deinflector.js * Move bg/js/backend.js to js/background/backend.js * Move bg/js/translator.js to js/language/translator.js * Move bg/js/search-display-controller.js to js/display/search-display-controller.js * Move bg/js/request-builder.js to js/background/request-builder.js * Move bg/js/text-source-map.js to js/general/text-source-map.js * Move bg/js/clipboard-reader.js to js/comm/clipboard-reader.js * Move bg/js/clipboard-monitor.js to js/comm/clipboard-monitor.js * Move bg/js/query-parser.js to js/display/query-parser.js * Move bg/js/profile-conditions.js to js/background/profile-conditions.js * Move bg/js/dictionary-database.js to js/language/dictionary-database.js * Move bg/js/dictionary-importer.js to js/language/dictionary-importer.js * Move bg/js/anki-note-builder.js to js/data/anki-note-builder.js * Move bg/js/anki-note-data.js to js/data/anki-note-data.js * Move bg/js/database.js to js/data/database.js * Move bg/js/json-schema.js to js/data/json-schema.js * Move bg/js/options.js to js/data/options-util.js * Move bg/js/background-main.js to js/background/background-main.js * Move bg/js/permissions-util.js to js/data/permissions-util.js * Move bg/js/context-main.js to js/pages/action-popup-main.js * Move bg/js/generic-page-main.js to js/pages/generic-page-main.js * Move bg/js/info-main.js to js/pages/info-main.js * Move bg/js/permissions-main.js to js/pages/permissions-main.js * Move bg/js/welcome-main.js to js/pages/welcome-main.js
2021-02-14 16:19:54 +00:00
'js/data/json-schema.js'
]);
const JsonSchema = vm.get('JsonSchema');
function schemaValidate(schema, value) {
return new JsonSchema(schema).isValid(value);
}
function getValidValueOrDefault(schema, value) {
return new JsonSchema(schema).getValidValueOrDefault(value);
}
function createProxy(schema, value) {
return new JsonSchema(schema).createProxy(value);
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function testValidate1() {
const schema = {
allOf: [
{
type: 'number'
},
{
anyOf: [
{minimum: 10, maximum: 100},
{minimum: -100, maximum: -10}
]
},
{
oneOf: [
{multipleOf: 3},
{multipleOf: 5}
]
},
{
not: [
{multipleOf: 20}
]
}
]
};
const jsValidate = (value) => {
return (
typeof value === 'number' &&
(
(value >= 10 && value <= 100) ||
(value >= -100 && value <= -10)
) &&
(
(
2020-02-17 20:36:58 +00:00
(value % 3) === 0 ||
(value % 5) === 0
) &&
(value % 15) !== 0
) &&
(value % 20) !== 0
);
};
for (let i = -111; i <= 111; i++) {
const actual = schemaValidate(schema, i);
const expected = jsValidate(i);
assert.strictEqual(actual, expected);
}
}
function testValidate2() {
const data = [
// String tests
{
schema: {
type: 'string'
},
inputs: [
{expected: false, value: null},
{expected: false, value: void 0},
{expected: false, value: 0},
{expected: false, value: {}},
{expected: false, value: []},
{expected: true, value: ''}
]
},
{
schema: {
type: 'string',
minLength: 2
},
inputs: [
{expected: false, value: ''},
{expected: false, value: '1'},
{expected: true, value: '12'},
{expected: true, value: '123'}
]
},
{
schema: {
type: 'string',
maxLength: 2
},
inputs: [
{expected: true, value: ''},
{expected: true, value: '1'},
{expected: true, value: '12'},
{expected: false, value: '123'}
]
},
{
schema: {
type: 'string',
pattern: 'test'
},
inputs: [
{expected: false, value: ''},
{expected: true, value: 'test'},
{expected: false, value: 'TEST'},
{expected: true, value: 'ABCtestDEF'},
{expected: false, value: 'ABCTESTDEF'}
]
},
{
schema: {
type: 'string',
pattern: '^test$'
},
inputs: [
{expected: false, value: ''},
{expected: true, value: 'test'},
{expected: false, value: 'TEST'},
{expected: false, value: 'ABCtestDEF'},
{expected: false, value: 'ABCTESTDEF'}
]
},
{
schema: {
type: 'string',
pattern: '^test$',
patternFlags: 'i'
},
inputs: [
{expected: false, value: ''},
{expected: true, value: 'test'},
{expected: true, value: 'TEST'},
{expected: false, value: 'ABCtestDEF'},
{expected: false, value: 'ABCTESTDEF'}
]
},
{
schema: {
type: 'string',
pattern: '*'
},
inputs: [
{expected: false, value: ''}
]
},
{
schema: {
type: 'string',
pattern: '.',
patternFlags: '?'
},
inputs: [
{expected: false, value: ''}
]
},
// Const tests
{
schema: {
const: 32
},
inputs: [
{expected: true, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: '32'
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: true, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: null
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: true, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: {a: 'b'}
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: [1, 2, 3]
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
// Array contains tests
{
schema: {
type: 'array',
contains: {const: 32}
},
inputs: [
{expected: false, value: []},
{expected: true, value: [32]},
{expected: true, value: [1, 32]},
{expected: true, value: [1, 32, 1]},
{expected: false, value: [33]},
{expected: false, value: [1, 33]},
{expected: false, value: [1, 33, 1]}
]
},
// Number limits tests
{
schema: {
type: 'number',
minimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: true, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'number',
exclusiveMinimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: false, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'number',
maximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: true, value: 0},
{expected: false, value: 1}
]
},
{
schema: {
type: 'number',
exclusiveMaximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: false, value: 0},
{expected: false, value: 1}
]
},
// Integer limits tests
{
schema: {
type: 'integer',
minimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: true, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'integer',
exclusiveMinimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: false, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'integer',
maximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: true, value: 0},
{expected: false, value: 1}
]
},
{
schema: {
type: 'integer',
exclusiveMaximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: false, value: 0},
{expected: false, value: 1}
]
},
{
schema: {
type: 'integer',
multipleOf: 2
},
inputs: [
{expected: true, value: -2},
{expected: false, value: -1},
{expected: true, value: 0},
{expected: false, value: 1},
{expected: true, value: 2}
]
},
// Numeric type tests
{
schema: {
type: 'number'
},
inputs: [
{expected: true, value: 0},
{expected: true, value: 0.5},
{expected: true, value: 1},
{expected: false, value: '0'},
{expected: false, value: null},
{expected: false, value: []},
{expected: false, value: {}}
]
},
{
schema: {
type: 'integer'
},
inputs: [
{expected: true, value: 0},
{expected: false, value: 0.5},
{expected: true, value: 1},
{expected: false, value: '0'},
{expected: false, value: null},
{expected: false, value: []},
{expected: false, value: {}}
]
},
// Reference tests
{
schema: {
definitions: {
example: {
type: 'number'
}
},
$ref: '#/definitions/example'
},
inputs: [
{expected: true, value: 0},
{expected: true, value: 0.5},
{expected: true, value: 1},
{expected: false, value: '0'},
{expected: false, value: null},
{expected: false, value: []},
{expected: false, value: {}}
]
},
{
schema: {
definitions: {
example: {
type: 'integer'
}
},
$ref: '#/definitions/example'
},
inputs: [
{expected: true, value: 0},
{expected: false, value: 0.5},
{expected: true, value: 1},
{expected: false, value: '0'},
{expected: false, value: null},
{expected: false, value: []},
{expected: false, value: {}}
]
},
{
schema: {
definitions: {
example: {
type: 'object',
additionalProperties: false,
properties: {
test: {
$ref: '#/definitions/example'
}
}
}
},
$ref: '#/definitions/example'
},
inputs: [
{expected: false, value: 0},
{expected: false, value: 0.5},
{expected: false, value: 1},
{expected: false, value: '0'},
{expected: false, value: null},
{expected: false, value: []},
{expected: true, value: {}},
{expected: false, value: {test: 0}},
{expected: false, value: {test: 0.5}},
{expected: false, value: {test: 1}},
{expected: false, value: {test: '0'}},
{expected: false, value: {test: null}},
{expected: false, value: {test: []}},
{expected: true, value: {test: {}}},
{expected: true, value: {test: {test: {}}}},
{expected: true, value: {test: {test: {test: {}}}}}
]
}
];
for (const {schema, inputs} of data) {
for (const {expected, value} of inputs) {
const actual = schemaValidate(schema, value);
assert.strictEqual(actual, expected);
}
}
}
function testGetValidValueOrDefault1() {
const data = [
// Test value defaulting on objects with additionalProperties=false
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: false
},
inputs: [
[
void 0,
{test: 'default'}
],
[
null,
{test: 'default'}
],
[
0,
{test: 'default'}
],
[
'',
{test: 'default'}
],
[
[],
{test: 'default'}
],
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value'}
]
]
},
// Test value defaulting on objects with additionalProperties=true
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: true
},
inputs: [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 'value2'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 'value2'}
]
]
},
// Test value defaulting on objects with additionalProperties={schema}
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: {
type: 'number',
default: 10
}
},
inputs: [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 10}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 10}
],
[
{test2: 2},
{test: 'default', test2: 2}
],
[
{test: 'value', test2: 2},
{test: 'value', test2: 2}
],
[
{test: 'value', test2: 2, test3: null},
{test: 'value', test2: 2, test3: 10}
],
[
{test: 'value', test2: 2, test3: void 0},
{test: 'value', test2: 2, test3: 10}
]
]
},
// Test value defaulting where hasOwnProperty is false
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
}
},
inputs: [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
Object.create({test: 'value'}),
{test: 'default'}
]
]
},
{
schema: {
type: 'object',
required: ['toString'],
properties: {
toString: {
type: 'string',
default: 'default'
}
}
},
inputs: [
[
{},
{toString: 'default'}
],
[
{toString: 'value'},
{toString: 'value'}
],
[
Object.create({toString: 'value'}),
{toString: 'default'}
]
]
},
// Test enum
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'value1',
enum: ['value1', 'value2', 'value3']
}
}
},
inputs: [
[
{test: 'value1'},
{test: 'value1'}
],
[
{test: 'value2'},
{test: 'value2'}
],
[
{test: 'value3'},
{test: 'value3'}
],
[
{test: 'value4'},
{test: 'value1'}
]
]
},
// Test valid vs invalid default
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'integer',
default: 2,
minimum: 1
}
}
},
inputs: [
[
{test: -1},
{test: 2}
]
]
},
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'integer',
default: 1,
minimum: 2
}
}
},
inputs: [
[
{test: -1},
{test: -1}
]
]
},
// Test references
{
schema: {
definitions: {
example: {
type: 'number',
default: 0
}
},
$ref: '#/definitions/example'
},
inputs: [
[
1,
1
],
[
null,
0
],
[
'test',
0
],
[
{test: 'value'},
0
]
]
},
{
schema: {
definitions: {
example: {
type: 'object',
additionalProperties: false,
properties: {
test: {
$ref: '#/definitions/example'
}
}
}
},
$ref: '#/definitions/example'
},
inputs: [
[
1,
{}
],
[
null,
{}
],
[
'test',
{}
],
[
{},
{}
],
[
{test: {}},
{test: {}}
],
[
{test: 'value'},
{test: {}}
],
[
{test: {test: {}}},
{test: {test: {}}}
]
]
}
];
for (const {schema, inputs} of data) {
for (const [value, expected] of inputs) {
const actual = getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
}
}
}
function testProxy1() {
const data = [
// Object tests
{
schema: {
type: 'object',
required: ['test'],
additionalProperties: false,
properties: {
test: {
type: 'string',
default: 'default'
}
}
},
tests: [
{error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
{error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
{error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
{error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
{error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
]
},
{
schema: {
type: 'object',
required: ['test'],
additionalProperties: true,
properties: {
test: {
type: 'string',
default: 'default'
}
}
},
tests: [
{error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
{error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
{error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
{error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
{error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
]
},
{
schema: {
type: 'object',
required: ['test1'],
additionalProperties: false,
properties: {
test1: {
type: 'object',
required: ['test2'],
additionalProperties: false,
properties: {
test2: {
type: 'object',
required: ['test3'],
additionalProperties: false,
properties: {
test3: {
type: 'string',
default: 'default'
}
}
}
}
}
}
},
tests: [
{error: false, action: (value) => { value.test1.test2.test3 = 'string'; }},
{error: true, action: (value) => { value.test1.test2.test3 = null; }},
{error: true, action: (value) => { delete value.test1.test2.test3; }},
{error: true, action: (value) => { value.test1.test2 = null; }},
{error: true, action: (value) => { value.test1 = null; }},
{error: true, action: (value) => { value.test4 = 'string'; }},
{error: false, action: (value) => { delete value.test4; }}
]
},
// Array tests
{
schema: {
type: 'array',
items: {
type: 'string',
default: 'default'
}
},
tests: [
{error: false, value: ['default'], action: (value) => { value[0] = 'string'; }},
{error: true, value: ['default'], action: (value) => { value[0] = null; }},
{error: false, value: ['default'], action: (value) => { delete value[0]; }},
{error: false, value: ['default'], action: (value) => { value[1] = 'string'; }},
{error: false, value: ['default'], action: (value) => {
value[1] = 'string';
if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); }
if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); }
}}
]
},
// Reference tests
{
schema: {
definitions: {
example: {
type: 'object',
additionalProperties: false,
properties: {
test: {
$ref: '#/definitions/example'
}
}
}
},
$ref: '#/definitions/example'
},
tests: [
{error: false, value: {}, action: (value) => { value.test = {}; }},
{error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }},
{error: false, value: {}, action: (value) => { value.test = {test: {}}; }},
{error: true, value: {}, action: (value) => { value.test = null; }},
{error: true, value: {}, action: (value) => { value.test = 'string'; }},
{error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }},
{error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }}
]
}
];
for (const {schema, tests} of data) {
for (let {error, value, action} of tests) {
if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); }
value = clone(value);
assert.ok(schemaValidate(schema, value));
const valueProxy = createProxy(schema, value);
if (error) {
assert.throws(() => action(valueProxy));
} else {
assert.doesNotThrow(() => action(valueProxy));
}
}
}
}
function main() {
testValidate1();
testValidate2();
testGetValidValueOrDefault1();
testProxy1();
}
if (require.main === module) { testMain(main); }