-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
shell: Manifest validation upon import #21449
base: main
Are you sure you want to change the base?
Conversation
mvollmer
commented
Dec 18, 2024
•
edited
Loading
edited
- unit tests
This is quite WIPy still... |
c3fd607
to
e89ae83
Compare
e89ae83
to
5791647
Compare
This way, it is only validated once and not on every render.
5791647
to
a3dc6f2
Compare
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.
a3dc6f2
to
428e940
Compare
function validation_error(msg: string): never { | ||
console.error(`JSON validation error for ${validation_path.join("")}: ${msg}`); | ||
throw new ValidationError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 3 added lines are not executed by any test.
export function import_string(val: JsonValue): string { | ||
if (typeof val == "string") | ||
return val; | ||
validation_error(`Not a string: ${JSON.stringify(val)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
export function import_number(val: JsonValue): number { | ||
if (typeof val == "number") | ||
return val; | ||
validation_error(`Not a number: ${JSON.stringify(val)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
export function import_boolean(val: JsonValue): boolean { | ||
if (typeof val == "boolean") | ||
return val; | ||
validation_error(`Not a boolean: ${JSON.stringify(val)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 4 added lines are not executed by any test.
export function import_json_object(val: JsonValue): JsonObject { | ||
if (!!val && typeof val == "object" && val.length === undefined) | ||
return val as JsonObject; | ||
validation_error(`Not an object: ${JSON.stringify(val)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
} catch (e) { | ||
if (!(e instanceof ValidationError)) | ||
throw e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 3 added lines are not executed by any test.
if (obj[field as string] === undefined) { | ||
validation_error(`Field "${String(field)}" is missing`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 2 added lines are not executed by any test.
|
||
export function validate<T>(path: string, val: JsonValue | undefined, importer: (val: JsonValue) => T, fallback: T): T { | ||
if (val === undefined) | ||
return fallback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
} catch (e) { | ||
if (!(e instanceof ValidationError)) | ||
throw e; | ||
return fallback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 4 added lines are not executed by any test.
@@ -144,7 +142,7 @@ export const LangModal = ({ dialogResult }) => { | |||
<MenuList> | |||
{ | |||
(() => { | |||
const locales = manifest.locales || {}; | |||
const locales = state.config.manifest.locales || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This added line is not executed by any test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @mvollmer ! I like the approach a lot -- explicit, clean, no new dependencies, and pleasant to use.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this should also throw, instead of giving you a truncated array -- that just silently hides errors, and may make the rest of the entries invalid as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is the big philosophical question. Personally, I like the non-strict approach, but I have no problem with making this strict.
My best argument for making this strict is to amplify that something is wrong. If you make a mistake with your search terms, your whole page doesn't get loaded instead of some search terms being omitted.
|
||
QUnit.test("import_record", function(assert) { | ||
assert.deepEqual( | ||
import_record({ a: "a", b: "b", c: "c" }, import_string), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON parlance is "object", not "record". I suppose you want to give this a different name to point out that all values have to be of the same type. But a "record" (or "struct") is precisely the opposite -- fixed number and names of keys, and different values.
One term that comes to my mind that conveys this meaning is "dictionary" (they usually map one type to a single value type).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "record" term is from the TypeScript utility class Record<Keys, Type>
, which is indeed a Map or Dict, not a structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh -- this is opposite to pretty much everywhere else https://en.wikipedia.org/wiki/Record_(computer_science) -- shame on you, MS.
|
||
import { JsonValue } from 'cockpit'; | ||
import { | ||
import_json_object, import_optional, import_mandatory, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_() would be more consistent with our Python API; "import" sounds a bit exaggerated, but no strong opinion.
assert.deepEqual( | ||
import_record({ a: 1, b: 2, c: "c" }, import_number), | ||
{ a: 1, b: 2 }, | ||
"record of numbers with a string"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the inconsistent array above, IMHO this should throw and ignore the value in its entirety.
Validation is generally lenient: If a validation error occurs deep | ||
inside a nested structure, only the affected part of the structure | ||
is omitted. More conretely, if an element of an array is invalid, | ||
this element is omitted from the array. If a optional field of an | ||
object is invalid, it will be omitted. This doesn't happen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I mentioned above: IMHO this isn't safe in general. Think about "bridges": []
in the shell manifest. Silently ignoring a non-string argument from sudo or pkexec is potentially disastrous. It's of course less of an issue for docs or locales, but IMHO it would make more sense to be strict and completely ignore the value -- that will greatly increase the chance of catching bugs in our tests.
(Also, "conretely" typo)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, command lines are a very good example of something that better be rejected in its entirety.
const checks: [JsonValue, Teams][] = [ | ||
[ | ||
{ | ||
MAC: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I just have to ask: What does "MAC" mean in soccer context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's supposed to be the short form of "ManCity"...
{ name: "Haaland", age: 24 }, | ||
{ name: "De Bruyne", age: 33 } | ||
], | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add a description comment before each scenario? Like, this one is probably "No stadium (it's optional)" or so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
|
||
function validation_error(msg: string): never { | ||
console.error(`JSON validation error for ${validation_path.join("")}: ${msg}`); | ||
throw new ValidationError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't hurt to pass on msg
into the exception object?