Skip to content

Commit 76edb03

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 76edb03

File tree

6 files changed

+604
-8
lines changed

6 files changed

+604
-8
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

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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), "not a string");
46+
assert.deepEqual(
47+
console_errors,
48+
[
49+
'JSON validation error for : Not a string: 12'
50+
],
51+
"console errors"
52+
);
53+
});
54+
55+
QUnit.test("import_number", function(assert) {
56+
assert.equal(import_number(12), 12, "number");
57+
assert.throws(() => import_number("foo"), "not a number");
58+
assert.deepEqual(
59+
console_errors,
60+
[
61+
'JSON validation error for : Not a number: "foo"'
62+
],
63+
"console errors"
64+
);
65+
});
66+
67+
QUnit.test("import_boolean", function(assert) {
68+
assert.equal(import_boolean(true), true, "boolean");
69+
assert.throws(() => import_boolean("foo"), "not a boolean");
70+
assert.deepEqual(
71+
console_errors,
72+
[
73+
'JSON validation error for : Not a boolean: "foo"'
74+
],
75+
"console errors"
76+
);
77+
});
78+
79+
QUnit.test("import_array", function(assert) {
80+
assert.deepEqual(import_array(["a", "b", "c"], import_string), ["a", "b", "c"], "array of strings");
81+
assert.deepEqual(import_array([1, 2, 3], import_number), [1, 2, 3], "array of numbers");
82+
assert.deepEqual(import_array([1, 2, "c"], import_number), [1, 2], "array of numbers with a string");
83+
assert.throws(() => import_array("foo", import_string), "not an array");
84+
assert.deepEqual(
85+
console_errors,
86+
[
87+
'JSON validation error for [2]: Not a number: "c"',
88+
'JSON validation error for : Not an array: "foo"'
89+
],
90+
"console errors"
91+
);
92+
});
93+
94+
QUnit.test("import_record", function(assert) {
95+
assert.deepEqual(
96+
import_record({ a: "a", b: "b", c: "c" }, import_string),
97+
{ a: "a", b: "b", c: "c" },
98+
"record of strings");
99+
assert.deepEqual(
100+
import_record({ a: 1, b: 2, c: 3 }, import_number),
101+
{ a: 1, b: 2, c: 3 },
102+
"record of numbers");
103+
assert.deepEqual(
104+
import_record({ a: 1, b: 2, c: "c" }, import_number),
105+
{ a: 1, b: 2 },
106+
"record of numbers with a string");
107+
assert.throws(() => import_record("foo", import_string), "not a record");
108+
assert.deepEqual(
109+
console_errors,
110+
[
111+
'JSON validation error for .c: Not a number: "c"',
112+
'JSON validation error for : Not an object: "foo"'
113+
],
114+
"console errors"
115+
);
116+
});
117+
118+
QUnit.test("validate", function(assert) {
119+
assert.equal(validate("test input", "foo", import_string, ""), "foo", "string");
120+
assert.equal(validate("test input", 12, import_string, ""), "", "not a string");
121+
assert.deepEqual(
122+
console_errors,
123+
[
124+
'JSON validation error for test input: Not a string: 12'
125+
],
126+
"console errors"
127+
);
128+
});
129+
130+
interface Player {
131+
name: string;
132+
age?: number;
133+
}
134+
135+
function import_Player(val: JsonValue): Player {
136+
const obj = import_json_object(val);
137+
const res: Player = {
138+
name: import_mandatory(obj, "name", import_string),
139+
};
140+
import_optional(res, obj, "age", import_number);
141+
return res;
142+
}
143+
144+
interface Team {
145+
name: string;
146+
players: Player[];
147+
}
148+
149+
function import_Team(val: JsonValue): Team {
150+
const obj = import_json_object(val);
151+
const res: Team = {
152+
name: import_mandatory(obj, "name", import_string),
153+
players: import_mandatory(obj, "players", v => import_array(v, import_Player))
154+
};
155+
return res;
156+
}
157+
158+
interface Teams {
159+
[name: string]: Team;
160+
}
161+
162+
function import_Teams(val: JsonValue): Teams {
163+
return import_record(val, import_Team);
164+
}
165+
166+
QUnit.test("objects", function(assert) {
167+
const checks: [JsonValue, Teams][] = [
168+
[
169+
{
170+
MAC: {
171+
name: "ManCity",
172+
players: [
173+
{ name: "Haaland", age: 24 },
174+
{ name: "De Bruyne", age: 33 }
175+
],
176+
stadium: "City of Manchester Stadium"
177+
},
178+
},
179+
{
180+
MAC: {
181+
name: "ManCity",
182+
players: [
183+
{ name: "Haaland", age: 24 },
184+
{ name: "De Bruyne", age: 33 }
185+
],
186+
},
187+
}
188+
189+
],
190+
[
191+
{
192+
MAC: {
193+
name: "ManCity",
194+
players: [
195+
{ name: "Haaland", age: "unknown" },
196+
{ name: "De Bruyne", age: 33 }
197+
],
198+
stadium: "City of Manchester Stadium"
199+
},
200+
},
201+
{
202+
MAC: {
203+
name: "ManCity",
204+
players: [
205+
{ name: "Haaland" },
206+
{ name: "De Bruyne", age: 33 }
207+
],
208+
},
209+
}
210+
211+
],
212+
[
213+
{
214+
MAC: {
215+
name: "ManCity",
216+
players: [
217+
{ name: ["Erling", "Braut", "Haaland"] },
218+
{ name: "De Bruyne", age: 33 }
219+
],
220+
stadium: "City of Manchester Stadium"
221+
},
222+
},
223+
{
224+
MAC: {
225+
name: "ManCity",
226+
players: [
227+
{ name: "De Bruyne", age: 33 }
228+
],
229+
},
230+
}
231+
],
232+
[
233+
{
234+
MAC: {
235+
name: "ManCity",
236+
players: "TBD",
237+
stadium: "City of Manchester Stadium"
238+
},
239+
},
240+
{}
241+
],
242+
[
243+
"...",
244+
{}
245+
]
246+
];
247+
248+
for (let i = 0; i < checks.length; i++) {
249+
assert.deepEqual(validate("test input", checks[i][0], import_Teams, {}), checks[i][1]);
250+
}
251+
252+
assert.deepEqual(
253+
console_errors,
254+
[
255+
'JSON validation error for test input.MAC.players[0].age: Not a number: "unknown"',
256+
'JSON validation error for test input.MAC.players[0].name: Not a string: ["Erling","Braut","Haaland"]',
257+
'JSON validation error for test input.MAC.players: Not an array: "TBD"',
258+
'JSON validation error for test input: Not an object: "..."'
259+
],
260+
"console errors"
261+
);
262+
});
263+
264+
QUnit.start();

0 commit comments

Comments
 (0)