@@ -7,6 +7,10 @@ const { PolicySchemaValidator } = ChromeUtils.importESModule(
77 "resource://gre/modules/policies/PolicySchemaValidator.sys.mjs"
88) ;
99
10+ const { TestUtils } = ChromeUtils . importESModule (
11+ "resource://testing-common/TestUtils.sys.mjs"
12+ ) ;
13+
1014function assertValid ( value , schema , options ) {
1115 const result = PolicySchemaValidator . validate ( value , schema , options ) ;
1216 Assert . ok ( result . valid , `Expected ${ JSON . stringify ( value ) } to validate` ) ;
@@ -39,7 +43,27 @@ add_task(function test_boolean_coercion_from_gpo_dword() {
3943 Assert . strictEqual ( assertValid ( 0 , schema ) , false , "0 coerces to false" ) ;
4044 Assert . strictEqual ( assertValid ( true , schema ) , true , "true stays true" ) ;
4145 assertInvalid ( 2 , schema ) ;
42- assertInvalid ( "true" , schema ) ;
46+ } ) ;
47+
48+ add_task ( function test_boolean_coercion_from_string ( ) {
49+ const schema = { type : "boolean" } ;
50+
51+ Assert . strictEqual ( assertValid ( "true" , schema ) , true , '"true" coerces' ) ;
52+ Assert . strictEqual ( assertValid ( "false" , schema ) , false , '"false" coerces' ) ;
53+ Assert . strictEqual (
54+ assertValid ( "True" , schema ) ,
55+ true ,
56+ "the match is case insensitive"
57+ ) ;
58+ assertInvalid ( "yes" , schema ) ;
59+ assertInvalid ( "" , schema ) ;
60+ } ) ;
61+
62+ add_task ( function test_string_field_keeps_its_quoted_boolean ( ) {
63+ // A field that accepts a string must not have "true" turned into a boolean.
64+ const schema = { type : [ "string" , "boolean" ] } ;
65+
66+ Assert . strictEqual ( assertValid ( "true" , schema ) , "true" , '"true" stays text' ) ;
4367} ) ;
4468
4569add_task ( function test_numeric_field_keeps_its_integer ( ) {
@@ -168,3 +192,101 @@ add_task(function test_additional_properties_rejected_by_default() {
168192
169193 assertInvalid ( { known : "value" , extra : "x" } , schema ) ;
170194} ) ;
195+
196+ // ExtensionSettings shape: a "*" entry holding the defaults, plus open-ended
197+ // per-extension-ID entries.
198+ const EXTENSION_SETTINGS_SHAPE = {
199+ type : "object" ,
200+ properties : {
201+ "*" : {
202+ type : "object" ,
203+ properties : {
204+ installation_mode : { type : "string" } ,
205+ } ,
206+ } ,
207+ } ,
208+ patternProperties : {
209+ "^(?!\\*$).*$" : {
210+ type : "object" ,
211+ properties : {
212+ installation_mode : { type : "string" } ,
213+ updates_disabled : { type : "boolean" } ,
214+ } ,
215+ } ,
216+ } ,
217+ } ;
218+
219+ add_task ( function test_map_drops_invalid_entries_and_keeps_the_rest ( ) {
220+ // One malformed entry must not discard every other extension's settings.
221+ Assert . deepEqual (
222+ assertValid (
223+ {
224+ "*" : { installation_mode : "blocked" } ,
225+ "good@example.com" : { installation_mode : "force_installed" } ,
226+ "bad@example.com" : { updates_disabled : 42 } ,
227+ } ,
228+ EXTENSION_SETTINGS_SHAPE
229+ ) ,
230+ {
231+ "*" : { installation_mode : "blocked" } ,
232+ "good@example.com" : { installation_mode : "force_installed" } ,
233+ } ,
234+ "the invalid entry is dropped and the rest of the policy applies"
235+ ) ;
236+ } ) ;
237+
238+ add_task ( function test_map_never_drops_a_fixed_property ( ) {
239+ // "*" is not an independently droppable entry: dropping it would turn a
240+ // global block into a policy that looks healthy and installs everything.
241+ assertInvalid (
242+ {
243+ "*" : { installation_mode : 42 } ,
244+ "ok@example.com" : { installation_mode : "force_installed" } ,
245+ } ,
246+ EXTENSION_SETTINGS_SHAPE
247+ ) ;
248+ } ) ;
249+
250+ add_task ( function test_fixed_properties_are_not_dropped_individually ( ) {
251+ // An object with only fixed properties is one value, not a collection: a bad
252+ // property fails the policy rather than being silently discarded.
253+ const schema = {
254+ type : "object" ,
255+ properties : { host : { type : "string" } , port : { type : "number" } } ,
256+ } ;
257+
258+ assertInvalid ( { host : "example.com" , port : "not a number" } , schema ) ;
259+ } ) ;
260+
261+ add_task ( async function test_dropped_entry_names_the_failing_field ( ) {
262+ // The log line is the only signal an admin gets that an entry went missing,
263+ // so it must name the field that failed, including when that field's name
264+ // needs unescaping out of the JSON pointer the validator reports.
265+ const schema = {
266+ type : "object" ,
267+ patternProperties : {
268+ "^.*$" : {
269+ type : "object" ,
270+ properties : { "application/pdf" : { type : "string" } } ,
271+ } ,
272+ } ,
273+ } ;
274+
275+ const stopListening = TestUtils . listenForConsoleMessages ( ) ;
276+ assertValid ( { mimeTypes : { "application/pdf" : 3 } } , schema , {
277+ policyName : "Handlers" ,
278+ } ) ;
279+ const messages = await stopListening ( ) ;
280+
281+ Assert . ok (
282+ messages . some (
283+ msg =>
284+ msg . level == "error" &&
285+ msg . arguments [ 0 ] . includes (
286+ 'Handlers["mimeTypes"]["application/pdf"]: Instance type "number"'
287+ ) &&
288+ msg . arguments [ 0 ] . includes ( "Received 3" )
289+ ) ,
290+ "the dropped entry names the failing field and the value received"
291+ ) ;
292+ } ) ;
0 commit comments