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