Skip to content

Commit 9cb2f09

Browse files
committed
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.
1 parent 02cbc42 commit 9cb2f09

File tree

7 files changed

+513
-27
lines changed

7 files changed

+513
-27
lines changed

files.js

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const info = {
7777
"base1/test-types.ts",
7878
"base1/test-user.js",
7979
"base1/test-websocket.js",
80+
"base1/test-import-json.ts",
8081

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

pkg/base1/test-import-json.ts

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* This file is part of Cockpit.
3+
*
4+
* Copyright (C) 2024 Red Hat, Inc.
5+
*
6+
* Cockpit is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation; either version 2.1 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* Cockpit is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import { JsonValue } from 'cockpit';
21+
import {
22+
import_json_object, import_optional, import_mandatory,
23+
import_string, import_number, import_boolean,
24+
import_array, import_record,
25+
validate
26+
} from "import-json";
27+
28+
import QUnit from 'qunit-tests';
29+
30+
// Hook into console.error
31+
32+
let console_errors: string[] = [];
33+
const console_error = console.error;
34+
console.error = function (msg) {
35+
console_errors.push(msg);
36+
console_error(msg);
37+
};
38+
39+
QUnit.hooks.beforeEach(() => {
40+
console_errors = [];
41+
});
42+
43+
QUnit.test("import_string", function(assert) {
44+
assert.equal(import_string("foo"), "foo", "string");
45+
assert.throws(() => import_string(12), /JSON validation error for : Not a string: 12/, "not a string");
46+
});
47+
48+
QUnit.test("import_number", function(assert) {
49+
assert.equal(import_number(12), 12, "number");
50+
assert.throws(
51+
() => import_number("foo"),
52+
/JSON validation error for : Not a number: "foo"/,
53+
"not a number");
54+
});
55+
56+
QUnit.test("import_boolean", function(assert) {
57+
assert.equal(import_boolean(true), true, "boolean");
58+
assert.throws(
59+
() => import_boolean("foo"),
60+
/JSON validation error for : Not a boolean: "foo"/,
61+
"not a boolean");
62+
});
63+
64+
QUnit.test("import_array", function(assert) {
65+
assert.deepEqual(import_array(["a", "b", "c"], import_string), ["a", "b", "c"], "array of strings");
66+
assert.deepEqual(import_array([1, 2, 3], import_number), [1, 2, 3], "array of numbers");
67+
assert.throws(
68+
() => import_array([1, 2, "c"], import_number),
69+
/JSON validation error for \[2\]: Not a number: "c"/,
70+
"array of numbers with a string");
71+
assert.throws(
72+
() => import_array("foo", import_string),
73+
/JSON validation error for : Not an array: "foo"/,
74+
"not an array");
75+
});
76+
77+
QUnit.test("import_record", function(assert) {
78+
assert.deepEqual(
79+
import_record({ a: "a", b: "b", c: "c" }, import_string),
80+
{ a: "a", b: "b", c: "c" },
81+
"record of strings");
82+
assert.deepEqual(
83+
import_record({ a: 1, b: 2, c: 3 }, import_number),
84+
{ a: 1, b: 2, c: 3 },
85+
"record of numbers");
86+
assert.throws(
87+
() => import_record({ a: 1, b: 2, c: "c" }, import_number),
88+
/JSON validation error for \.c: Not a number: "c"/,
89+
"record of numbers with a string");
90+
assert.throws(
91+
() => import_record("foo", import_string),
92+
/JSON validation error for : Not an object: "foo"/,
93+
"not a record");
94+
});
95+
96+
QUnit.test("validate", function(assert) {
97+
assert.equal(validate("test input", "foo", import_string, "default"), "foo", "string");
98+
assert.equal(validate("test input", 12, import_string, "default"), "default", "not a string");
99+
assert.deepEqual(
100+
console_errors,
101+
[
102+
'JSON validation error for test input: Not a string: 12'
103+
],
104+
"console errors"
105+
);
106+
});
107+
108+
interface Player {
109+
name: string;
110+
age: number | undefined;
111+
}
112+
113+
function import_Player(val: JsonValue): Player {
114+
const obj = import_json_object(val);
115+
return {
116+
name: import_mandatory(obj, "name", import_string),
117+
age: import_optional(obj, "age", import_number),
118+
};
119+
}
120+
121+
interface Team {
122+
name: string;
123+
players: Player[];
124+
}
125+
126+
function import_Team(val: JsonValue): Team {
127+
const obj = import_json_object(val);
128+
return {
129+
name: import_mandatory(obj, "name", import_string),
130+
players: import_mandatory(obj, "players", v => import_array(v, import_Player)),
131+
};
132+
}
133+
134+
interface Teams {
135+
[name: string]: Team;
136+
}
137+
138+
function import_Teams(val: JsonValue): Teams {
139+
return import_record(val, import_Team);
140+
}
141+
142+
QUnit.test("objects", function(assert) {
143+
const valid_checks: [JsonValue, Teams][] = [
144+
// "stadium" should be omitted
145+
[
146+
{
147+
MAC: {
148+
name: "ManCity",
149+
players: [
150+
{ name: "Haaland", age: 24 },
151+
{ name: "De Bruyne", age: 33 }
152+
],
153+
stadium: "City of Manchester Stadium"
154+
},
155+
},
156+
{
157+
MAC: {
158+
name: "ManCity",
159+
players: [
160+
{ name: "Haaland", age: 24 },
161+
{ name: "De Bruyne", age: 33 }
162+
],
163+
},
164+
}
165+
166+
],
167+
];
168+
169+
const invalid_checks: [JsonValue, string][] = [
170+
// invalid optional field
171+
[
172+
{
173+
MAC: {
174+
name: "ManCity",
175+
players: [
176+
{ name: "Haaland", age: "unknown" },
177+
{ name: "De Bruyne", age: 33 }
178+
],
179+
},
180+
},
181+
'JSON validation error for test input.MAC.players[0].age: Not a number: "unknown"',
182+
],
183+
184+
// invalid mandatory field
185+
[
186+
{
187+
MAC: {
188+
name: "ManCity",
189+
players: [
190+
{ name: ["Erling", "Braut", "Haaland"] },
191+
{ name: "De Bruyne", age: 33 }
192+
],
193+
},
194+
},
195+
'JSON validation error for test input.MAC.players[0].name: Not a string: ["Erling","Braut","Haaland"]',
196+
],
197+
198+
// invalid mandatory array
199+
[
200+
{
201+
MAC: {
202+
name: "ManCity",
203+
players: "TBD",
204+
},
205+
},
206+
'JSON validation error for test input.MAC.players: Not an array: "TBD"',
207+
],
208+
209+
// invalid object
210+
[
211+
"...",
212+
'JSON validation error for test input: Not an object: "..."'
213+
]
214+
];
215+
216+
for (let i = 0; i < valid_checks.length; i++) {
217+
assert.deepEqual(validate("test input", valid_checks[i][0], import_Teams, {}), valid_checks[i][1]);
218+
}
219+
220+
for (let i = 0; i < invalid_checks.length; i++) {
221+
console_errors = [];
222+
assert.deepEqual(validate("test input", invalid_checks[i][0], import_Teams, {}), {});
223+
assert.deepEqual(console_errors, [invalid_checks[i][1]]);
224+
}
225+
});
226+
227+
QUnit.start();

0 commit comments

Comments
 (0)