Skip to content

Commit

Permalink
shell: Validate manifests before exposing them to TypeScript
Browse files Browse the repository at this point in the history
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
mvollmer committed Jan 7, 2025
1 parent 5dbcb6b commit a3dc6f2
Show file tree
Hide file tree
Showing 6 changed files with 604 additions and 8 deletions.
1 change: 1 addition & 0 deletions files.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const info = {
"base1/test-types.ts",
"base1/test-user.js",
"base1/test-websocket.js",
"base1/test-import-json.ts",

"kdump/test-config-client.js",

Expand Down
264 changes: 264 additions & 0 deletions pkg/base1/test-import-json.ts
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();
Loading

0 comments on commit a3dc6f2

Please sign in to comment.