See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace quizaccess_seb; 18 19 /** 20 * PHPUnit for property_list class. 21 * 22 * @package quizaccess_seb 23 * @author Andrew Madden <andrewmadden@catalyst-au.net> 24 * @copyright 2020 Catalyst IT 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 class property_list_test extends \advanced_testcase { 28 29 /** 30 * Test that an empty PList with a root dictionary is created. 31 */ 32 public function test_create_empty_plist() { 33 $emptyplist = new property_list(); 34 $xml = trim($emptyplist->to_xml()); 35 $this->assertEquals('<?xml version="1.0" encoding="UTF-8"?> 36 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 37 <plist version="1.0"><dict/></plist>', $xml); 38 } 39 40 /** 41 * Test that a Plist is constructed from an XML string. 42 */ 43 public function test_construct_plist_from_xml() { 44 $xml = $this->get_plist_xml_header() 45 . "<key>testKey</key>" 46 . "<string>testValue</string>" 47 . $this->get_plist_xml_footer(); 48 $plist = new property_list($xml); 49 $generatedxml = trim($plist->to_xml()); 50 $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?> 51 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> 52 <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml); 53 } 54 55 /** 56 * Test that an element can be added to the root dictionary. 57 */ 58 public function test_add_element_to_root() { 59 $plist = new property_list(); 60 $newelement = new \CFPropertyList\CFString('testValue'); 61 $plist->add_element_to_root('testKey', $newelement); 62 $generatedxml = trim($plist->to_xml()); 63 $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?> 64 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> 65 <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml); 66 } 67 68 /** 69 * Test that an element's value can be retrieved. 70 */ 71 public function test_get_element_value() { 72 $xml = $this->get_plist_xml_header() 73 . "<key>testKey</key>" 74 . "<string>testValue</string>" 75 . $this->get_plist_xml_footer(); 76 $plist = new property_list($xml); 77 $this->assertEquals('testValue', $plist->get_element_value('testKey')); 78 } 79 80 /** 81 * Test that an element's value can be retrieved. 82 */ 83 public function test_get_element_value_if_not_exists() { 84 $plist = new property_list(); 85 $this->assertEmpty($plist->get_element_value('testKey')); 86 } 87 88 /** 89 * Test an element's value can be retrieved if it is an array. 90 */ 91 public function test_get_element_value_if_array() { 92 $xml = $this->get_plist_xml_header() 93 . "<key>testDict</key>" 94 . "<dict>" 95 . "<key>testKey</key>" 96 . "<string>testValue</string>" 97 . "</dict>" 98 . $this->get_plist_xml_footer(); 99 $plist = new property_list($xml); 100 $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict')); 101 } 102 103 /** 104 * Test that a element's value can be updated that is not an array or dictionary. 105 * 106 * @param string $xml XML to create PList. 107 * @param string $key Key of element to try and update. 108 * @param mixed $value Value to try to update with. 109 * 110 * @dataProvider good_update_data_provider 111 */ 112 public function test_updating_element_value($xml, $key, $value) { 113 $xml = $this->get_plist_xml_header() 114 . $xml 115 . $this->get_plist_xml_footer(); 116 $plist = new property_list($xml); 117 $plist->update_element_value($key, $value); 118 $this->assertEquals($value, $plist->get_element_value($key)); 119 } 120 121 /** 122 * Test that a element's value can be updated that is not an array or dictionary. 123 * 124 * @param string $xml XML to create PList. 125 * @param string $key Key of element to try and update. 126 * @param mixed $value Bad value to try to update with. 127 * @param mixed $expected Expected value of element after update is called. 128 * @param string $exceptionmessage Message of exception expected to be thrown. 129 130 * @dataProvider bad_update_data_provider 131 */ 132 public function test_updating_element_value_with_bad_data(string $xml, string $key, $value, $expected, $exceptionmessage) { 133 $xml = $this->get_plist_xml_header() 134 . $xml 135 . $this->get_plist_xml_footer(); 136 $plist = new property_list($xml); 137 138 $this->expectException(\invalid_parameter_exception::class); 139 $this->expectExceptionMessage($exceptionmessage); 140 141 $plist->update_element_value($key, $value); 142 $plistarray = json_decode($plist->to_json()); // Export elements. 143 $this->assertEquals($expected, $plistarray->$key); 144 } 145 146 /** 147 * Test that a dictionary can have it's value (array) updated. 148 */ 149 public function test_updating_element_array_if_dictionary() { 150 $xml = $this->get_plist_xml_header() 151 . "<key>testDict</key>" 152 . "<dict>" 153 . "<key>testKey</key>" 154 . "<string>testValue</string>" 155 . "</dict>" 156 . $this->get_plist_xml_footer(); 157 $plist = new property_list($xml); 158 $plist->update_element_array('testDict', ['newKey' => new \CFPropertyList\CFString('newValue')]); 159 $this->assertEquals(['newKey' => 'newValue'], $plist->get_element_value('testDict')); 160 } 161 162 /** 163 * Test that a dictionary can have it's value (array) updated. 164 */ 165 public function test_updating_element_array_if_dictionary_with_bad_data() { 166 $xml = $this->get_plist_xml_header() 167 . "<key>testDict</key>" 168 . "<dict>" 169 . "<key>testKey</key>" 170 . "<string>testValue</string>" 171 . "</dict>" 172 . $this->get_plist_xml_footer(); 173 $plist = new property_list($xml); 174 175 $this->expectException(\invalid_parameter_exception::class); 176 $this->expectExceptionMessage('New array must only contain CFType objects.'); 177 178 $plist->update_element_array('testDict', [false]); 179 $this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict')); 180 $this->assertDebuggingCalled('property_list: If updating an array in PList, it must only contain CFType objects.', 181 DEBUG_DEVELOPER); 182 } 183 184 /** 185 * Test that an element can be deleted. 186 */ 187 public function test_delete_element() { 188 $xml = $this->get_plist_xml_header() 189 . "<key>testKey</key>" 190 . "<string>testValue</string>" 191 . $this->get_plist_xml_footer(); 192 $plist = new property_list($xml); 193 $plist->delete_element('testKey'); 194 $generatedxml = trim($plist->to_xml()); 195 $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?> 196 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> 197 <plist version=\"1.0\"><dict/></plist>", $generatedxml); 198 } 199 200 /** 201 * Test that an element can be deleted. 202 */ 203 public function test_delete_element_if_not_exists() { 204 $xml = $this->get_plist_xml_header() 205 . "<key>testKey</key>" 206 . "<string>testValue</string>" 207 . $this->get_plist_xml_footer(); 208 $plist = new property_list($xml); 209 $plist->delete_element('nonExistentKey'); 210 $generatedxml = trim($plist->to_xml()); 211 // The xml should be unaltered. 212 $this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?> 213 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> 214 <plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml); 215 } 216 217 /** 218 * Test that json is exported correctly according to SEB Config Key requirements. 219 * 220 * @param string $xml PList XML used to generate CFPropertyList. 221 * @param string $expectedjson Expected JSON output. 222 * 223 * @dataProvider json_data_provider 224 */ 225 public function test_export_to_json($xml, $expectedjson) { 226 $xml = $this->get_plist_xml_header() 227 . $xml 228 . $this->get_plist_xml_footer(); 229 $plist = new property_list($xml); 230 $generatedjson = $plist->to_json(); 231 $this->assertEquals($expectedjson, $generatedjson); 232 } 233 234 /** 235 * Test that the xml is exported to JSON from a real SEB config file. Expected JSON extracted from SEB logs. 236 */ 237 public function test_export_to_json_full_file() { 238 $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted_mac_001.seb'); 239 $plist = new property_list($xml); 240 $plist->delete_element('originatorVersion'); // JSON should not contain originatorVersion key. 241 $generatedjson = $plist->to_json(); 242 $json = trim(file_get_contents(__DIR__ . '/fixtures/JSON_unencrypted_mac_001.txt')); 243 $this->assertEquals($json, $generatedjson); 244 } 245 246 /** 247 * Test the set_or_update_value function. 248 */ 249 public function test_set_or_update_value() { 250 $plist = new property_list(); 251 252 $this->assertEmpty($plist->get_element_value('string')); 253 $this->assertEmpty($plist->get_element_value('bool')); 254 $this->assertEmpty($plist->get_element_value('number')); 255 256 // Setting values. 257 $plist->set_or_update_value('string', new \CFPropertyList\CFString('initial string')); 258 $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(true)); 259 $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('10')); 260 261 $this->assertEquals('initial string', $plist->get_element_value('string')); 262 $this->assertEquals(true, $plist->get_element_value('bool')); 263 $this->assertEquals(10, $plist->get_element_value('number')); 264 265 // Updating values. 266 $plist->set_or_update_value('string', new \CFPropertyList\CFString('new string')); 267 $plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(false)); 268 $plist->set_or_update_value('number', new \CFPropertyList\CFNumber('42')); 269 270 $this->assertEquals('new string', $plist->get_element_value('string')); 271 $this->assertEquals(false, $plist->get_element_value('bool')); 272 $this->assertEquals(42, $plist->get_element_value('number')); 273 274 // Type exception. 275 $this->expectException(\TypeError::class); 276 $plist->set_or_update_value('someKey', 'We really need to pass in CFTypes here'); 277 } 278 279 /** 280 * Get a valid PList header. Must also use footer. 281 * 282 * @return string 283 */ 284 private function get_plist_xml_header() : string { 285 return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 286 . "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" " 287 . "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" 288 . "<plist version=\"1.0\">\n" 289 . " <dict>"; 290 } 291 292 /** 293 * Get a valid PList footer. Must also use header. 294 * 295 * @return string 296 */ 297 private function get_plist_xml_footer() : string { 298 return " </dict>\n" 299 . "</plist>"; 300 } 301 302 /** 303 * Data provider for good data on update. 304 * 305 * @return array Array with test data. 306 */ 307 public function good_update_data_provider() : array { 308 return [ 309 'Update string' => ['<key>testKey</key><string>testValue</string>', 'testKey', 'newValue'], 310 'Update bool' => ['<key>testKey</key><true/>', 'testKey', false], 311 'Update number' => ['<key>testKey</key><real>888</real>', 'testKey', 123.4], 312 ]; 313 } 314 315 /** 316 * Data provider for bad data on update. 317 * 318 * @return array Array with test data. 319 */ 320 public function bad_update_data_provider() : array { 321 322 return [ 323 'Update string with bool' => ['<key>testKey</key><string>testValue</string>', 'testKey', true, 'testValue', 324 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 325 . 'or value type does not match element type: CFPropertyList\CFString'], 326 'Update string with number' => ['<key>testKey</key><string>testValue</string>', 'testKey', 999, 'testValue', 327 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 328 . 'or value type does not match element type: CFPropertyList\CFString'], 329 'Update string with null' => ['<key>testKey</key><string>testValue</string>', 'testKey', null, 'testValue', 330 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 331 . 'or value type does not match element type: CFPropertyList\CFString'], 332 'Update string with array' => ['<key>testKey</key><string>testValue</string>', 'testKey', ['arrayValue'], 'testValue', 333 'Use update_element_array to update a collection.'], 334 'Update bool with string' => ['<key>testKey</key><true/>', 'testKey', 'testValue', true, 335 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 336 . 'or value type does not match element type: CFPropertyList\CFBool'], 337 'Update bool with number' => ['<key>testKey</key><true/>', 'testKey', 999, true, 338 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 339 . 'or value type does not match element type: CFPropertyList\CFBool'], 340 'Update bool with null' => ['<key>testKey</key><true/>', 'testKey', null, true, 341 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 342 . 'or value type does not match element type: CFPropertyList\CFBool'], 343 'Update bool with array' => ['<key>testKey</key><true/>', 'testKey', ['testValue'], true, 344 'Use update_element_array to update a collection.'], 345 'Update number with string' => ['<key>testKey</key><real>888</real>', 'testKey', 'string', 888, 346 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 347 . 'or value type does not match element type: CFPropertyList\CFNumber'], 348 'Update number with bool' => ['<key>testKey</key><real>888</real>', 'testKey', true, 888, 349 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 350 . 'or value type does not match element type: CFPropertyList\CFNumber'], 351 'Update number with null' => ['<key>testKey</key><real>888</real>', 'testKey', null, 888, 352 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 353 . 'or value type does not match element type: CFPropertyList\CFNumber'], 354 'Update number with array' => ['<key>testKey</key><real>888</real>', 'testKey', ['testValue'], 888, 355 'Use update_element_array to update a collection.'], 356 'Update date with string' => ['<key>testKey</key><date>1940-10-09T22:13:56Z</date>', 'testKey', 'string', 357 '1940-10-10T06:13:56+08:00', 358 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 359 . 'or value type does not match element type: CFPropertyList\CFDate'], 360 'Update data with number' => ['<key>testKey</key><data>testData</data>', 'testKey', 789, 'testData', 361 'Invalid parameter value detected (Only string, number and boolean elements can be updated, ' 362 . 'or value type does not match element type: CFPropertyList\CFData'], 363 ]; 364 } 365 366 /** 367 * Data provider for expected JSON from PList. 368 * 369 * Examples extracted from requirements listed in SEB Config Key documents. 370 * https://safeexambrowser.org/developer/seb-config-key.html 371 * 372 * 1. Date should be in ISO 8601 format. 373 * 2. Data should be base 64 encoded. 374 * 3. String should be UTF-8 encoded. 375 * 4, 5, 6, 7. No requirements for bools, arrays or dicts. 376 * 8. Empty dicts should not be included. 377 * 9. JSON key ordering should be case insensitive, and use string ordering. 378 * 10. URL forward slashes should not be escaped. 379 * 380 * @return array 381 */ 382 public function json_data_provider() : array { 383 $data = "blahblah"; 384 $base64data = base64_encode($data); 385 386 return [ 387 'date' => ["<key>date</key><date>1940-10-09T22:13:56Z</date>", "{\"date\":\"1940-10-09T22:13:56+00:00\"}"], 388 'data' => ["<key>data</key><data>$base64data</data>", "{\"data\":\"$base64data\"}"], 389 'string' => ["<key>string</key><string>hello wörld</string>", "{\"string\":\"hello wörld\"}"], 390 'string with 1 backslash' => ["<key>string</key><string>ws:\localhost</string>", "{\"string\":\"ws:\localhost\"}"], 391 'string with 2 backslashes' => ["<key>string</key><string>ws:\\localhost</string>", 392 '{"string":"ws:\\localhost"}'], 393 'string with 3 backslashes' => ["<key>string</key><string>ws:\\\localhost</string>", 394 '{"string":"ws:\\\localhost"}'], 395 'string with 4 backslashes' => ["<key>string</key><string>ws:\\\\localhost</string>", 396 '{"string":"ws:\\\\localhost"}'], 397 'string with 5 backslashes' => ["<key>string</key><string>ws:\\\\\localhost</string>", 398 '{"string":"ws:\\\\\localhost"}'], 399 'bool' => ["<key>bool</key><true/>", "{\"bool\":true}"], 400 'array' => ["<key>array</key><array><key>arraybool</key><false/><key>arraybool2</key><true/></array>" 401 , "{\"array\":[false,true]}"], 402 'empty array' => ["<key>bool</key><true/><key>array</key><array/>" 403 , "{\"array\":[],\"bool\":true}"], 404 'dict' => ["<key>dict</key><dict><key>dictbool</key><false/><key>dictbool2</key><true/></dict>" 405 , "{\"dict\":{\"dictbool\":false,\"dictbool2\":true}}"], 406 'empty dict' => ["<key>bool</key><true/><key>emptydict</key><dict/>", "{\"bool\":true}"], 407 'unordered elements' => ["<key>testKey</key>" 408 . "<string>testValue</string>" 409 . "<key>allowWLAN</key>" 410 . "<string>testValue2</string>" 411 . "<key>allowWlan</key>" 412 . "<string>testValue3</string>" 413 , "{\"allowWlan\":\"testValue3\",\"allowWLAN\":\"testValue2\",\"testKey\":\"testValue\"}"], 414 'url' => ["<key>url</key><string>http://test.com</string>", "{\"url\":\"http://test.com\"}"], 415 'assoc dict' => ["<key>dict</key><dict><key>banana</key><false/><key>apple</key><true/></dict>", 416 "{\"dict\":{\"apple\":true,\"banana\":false}}"], 417 'seq array' => ["<key>array</key><array><key>1</key><false/><key>2</key><true/> 418 <key>3</key><true/><key>4</key><true/><key>5</key><true/><key>6</key><true/> 419 <key>7</key><true/><key>8</key><true/><key>9</key><true/><key>10</key><true/></array>", 420 "{\"array\":[false,true,true,true,true,true,true,true,true,true]}"], 421 ]; 422 } 423 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body