-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
shell: Validate manifests before exposing them to TypeScript
This make sure that typed code never sees values that don't conform to the claimed types, no matter what people put into their manifests.
- Loading branch information
Showing
6 changed files
with
604 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
/* | ||
* This file is part of Cockpit. | ||
* | ||
* Copyright (C) 2024 Red Hat, Inc. | ||
* | ||
* Cockpit is free software; you can redistribute it and/or modify it | ||
* under the terms of the GNU Lesser General Public License as published by | ||
* the Free Software Foundation; either version 2.1 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* Cockpit 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 | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
import { JsonValue } from 'cockpit'; | ||
import { | ||
import_json_object, import_optional, import_mandatory, | ||
import_string, import_number, import_boolean, | ||
import_array, import_record, | ||
validate | ||
} from "import-json"; | ||
|
||
import QUnit from 'qunit-tests'; | ||
|
||
// Hook into console.error | ||
|
||
let console_errors: string[] = []; | ||
const console_error = console.error; | ||
console.error = function (msg) { | ||
console_errors.push(msg); | ||
console_error(msg); | ||
}; | ||
|
||
QUnit.hooks.beforeEach(() => { | ||
console_errors = []; | ||
}); | ||
|
||
QUnit.test("import_string", function(assert) { | ||
assert.equal(import_string("foo"), "foo", "string"); | ||
assert.throws(() => import_string(12), "not a string"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for : Not a string: 12' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.test("import_number", function(assert) { | ||
assert.equal(import_number(12), 12, "number"); | ||
assert.throws(() => import_number("foo"), "not a number"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for : Not a number: "foo"' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.test("import_boolean", function(assert) { | ||
assert.equal(import_boolean(true), true, "boolean"); | ||
assert.throws(() => import_boolean("foo"), "not a boolean"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for : Not a boolean: "foo"' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.test("import_array", function(assert) { | ||
assert.deepEqual(import_array(["a", "b", "c"], import_string), ["a", "b", "c"], "array of strings"); | ||
assert.deepEqual(import_array([1, 2, 3], import_number), [1, 2, 3], "array of numbers"); | ||
assert.deepEqual(import_array([1, 2, "c"], import_number), [1, 2], "array of numbers with a string"); | ||
assert.throws(() => import_array("foo", import_string), "not an array"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for [2]: Not a number: "c"', | ||
'JSON validation error for : Not an array: "foo"' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.test("import_record", function(assert) { | ||
assert.deepEqual( | ||
import_record({ a: "a", b: "b", c: "c" }, import_string), | ||
{ a: "a", b: "b", c: "c" }, | ||
"record of strings"); | ||
assert.deepEqual( | ||
import_record({ a: 1, b: 2, c: 3 }, import_number), | ||
{ a: 1, b: 2, c: 3 }, | ||
"record of numbers"); | ||
assert.deepEqual( | ||
import_record({ a: 1, b: 2, c: "c" }, import_number), | ||
{ a: 1, b: 2 }, | ||
"record of numbers with a string"); | ||
assert.throws(() => import_record("foo", import_string), "not a record"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for .c: Not a number: "c"', | ||
'JSON validation error for : Not an object: "foo"' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.test("validate", function(assert) { | ||
assert.equal(validate("test input", "foo", import_string, ""), "foo", "string"); | ||
assert.equal(validate("test input", 12, import_string, ""), "", "not a string"); | ||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for test input: Not a string: 12' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
interface Player { | ||
name: string; | ||
age?: number; | ||
} | ||
|
||
function import_Player(val: JsonValue): Player { | ||
const obj = import_json_object(val); | ||
const res: Player = { | ||
name: import_mandatory(obj, "name", import_string), | ||
}; | ||
import_optional(res, obj, "age", import_number); | ||
return res; | ||
} | ||
|
||
interface Team { | ||
name: string; | ||
players: Player[]; | ||
} | ||
|
||
function import_Team(val: JsonValue): Team { | ||
const obj = import_json_object(val); | ||
const res: Team = { | ||
name: import_mandatory(obj, "name", import_string), | ||
players: import_mandatory(obj, "players", v => import_array(v, import_Player)) | ||
}; | ||
return res; | ||
} | ||
|
||
interface Teams { | ||
[name: string]: Team; | ||
} | ||
|
||
function import_Teams(val: JsonValue): Teams { | ||
return import_record(val, import_Team); | ||
} | ||
|
||
QUnit.test("objects", function(assert) { | ||
const checks: [JsonValue, Teams][] = [ | ||
[ | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: "Haaland", age: 24 }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
stadium: "City of Manchester Stadium" | ||
}, | ||
}, | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: "Haaland", age: 24 }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
}, | ||
} | ||
|
||
], | ||
[ | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: "Haaland", age: "unknown" }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
stadium: "City of Manchester Stadium" | ||
}, | ||
}, | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: "Haaland" }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
}, | ||
} | ||
|
||
], | ||
[ | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: ["Erling", "Braut", "Haaland"] }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
stadium: "City of Manchester Stadium" | ||
}, | ||
}, | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: [ | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
}, | ||
} | ||
], | ||
[ | ||
{ | ||
MAC: { | ||
name: "ManCity", | ||
players: "TBD", | ||
stadium: "City of Manchester Stadium" | ||
}, | ||
}, | ||
{} | ||
], | ||
[ | ||
"...", | ||
{} | ||
] | ||
]; | ||
|
||
for (let i = 0; i < checks.length; i++) { | ||
assert.deepEqual(validate("test input", checks[i][0], import_Teams, {}), checks[i][1]); | ||
} | ||
|
||
assert.deepEqual( | ||
console_errors, | ||
[ | ||
'JSON validation error for test input.MAC.players[0].age: Not a number: "unknown"', | ||
'JSON validation error for test input.MAC.players[0].name: Not a string: ["Erling","Braut","Haaland"]', | ||
'JSON validation error for test input.MAC.players: Not an array: "TBD"', | ||
'JSON validation error for test input: Not an object: "..."' | ||
], | ||
"console errors" | ||
); | ||
}); | ||
|
||
QUnit.start(); |
Oops, something went wrong.