<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
> namespace quizaccess_seb;
/**
>
* PHPUnit for property_list class.
*
* @package quizaccess_seb
* @author Andrew Madden <andrewmadden@catalyst-au.net>
< * @copyright 2019 Catalyst IT
< * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
< */
<
< use quizaccess_seb\property_list;
<
< defined('MOODLE_INTERNAL') || die();
<
< /**
< * PHPUnit for property_list class.
< *
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
< class quizaccess_seb_property_list_testcase extends advanced_testcase {
> class property_list_test extends \advanced_testcase {
/**
* Test that an empty PList with a root dictionary is created.
*/
public function test_create_empty_plist() {
$emptyplist = new property_list();
$xml = trim($emptyplist->to_xml());
$this->assertEquals('<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict/></plist>', $xml);
}
/**
* Test that a Plist is constructed from an XML string.
*/
public function test_construct_plist_from_xml() {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that an element can be added to the root dictionary.
*/
public function test_add_element_to_root() {
$plist = new property_list();
$newelement = new \CFPropertyList\CFString('testValue');
$plist->add_element_to_root('testKey', $newelement);
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that an element's value can be retrieved.
*/
public function test_get_element_value() {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->assertEquals('testValue', $plist->get_element_value('testKey'));
}
/**
* Test that an element's value can be retrieved.
*/
public function test_get_element_value_if_not_exists() {
$plist = new property_list();
$this->assertEmpty($plist->get_element_value('testKey'));
}
/**
* Test an element's value can be retrieved if it is an array.
*/
public function test_get_element_value_if_array() {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
}
/**
* Test that a element's value can be updated that is not an array or dictionary.
*
* @param string $xml XML to create PList.
* @param string $key Key of element to try and update.
* @param mixed $value Value to try to update with.
*
* @dataProvider good_update_data_provider
*/
public function test_updating_element_value($xml, $key, $value) {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->update_element_value($key, $value);
$this->assertEquals($value, $plist->get_element_value($key));
}
/**
* Test that a element's value can be updated that is not an array or dictionary.
*
* @param string $xml XML to create PList.
* @param string $key Key of element to try and update.
* @param mixed $value Bad value to try to update with.
* @param mixed $expected Expected value of element after update is called.
* @param string $exceptionmessage Message of exception expected to be thrown.
* @dataProvider bad_update_data_provider
*/
public function test_updating_element_value_with_bad_data(string $xml, string $key, $value, $expected, $exceptionmessage) {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
< $this->expectException(invalid_parameter_exception::class);
> $this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage($exceptionmessage);
$plist->update_element_value($key, $value);
$plistarray = json_decode($plist->to_json()); // Export elements.
$this->assertEquals($expected, $plistarray->$key);
}
/**
* Test that a dictionary can have it's value (array) updated.
*/
public function test_updating_element_array_if_dictionary() {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->update_element_array('testDict', ['newKey' => new \CFPropertyList\CFString('newValue')]);
$this->assertEquals(['newKey' => 'newValue'], $plist->get_element_value('testDict'));
}
/**
* Test that a dictionary can have it's value (array) updated.
*/
public function test_updating_element_array_if_dictionary_with_bad_data() {
$xml = $this->get_plist_xml_header()
. "<key>testDict</key>"
. "<dict>"
. "<key>testKey</key>"
. "<string>testValue</string>"
. "</dict>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
< $this->expectException(invalid_parameter_exception::class);
> $this->expectException(\invalid_parameter_exception::class);
$this->expectExceptionMessage('New array must only contain CFType objects.');
$plist->update_element_array('testDict', [false]);
$this->assertEquals(['testKey' => 'testValue'], $plist->get_element_value('testDict'));
$this->assertDebuggingCalled('property_list: If updating an array in PList, it must only contain CFType objects.',
DEBUG_DEVELOPER);
}
/**
* Test that an element can be deleted.
*/
public function test_delete_element() {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->delete_element('testKey');
$generatedxml = trim($plist->to_xml());
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict/></plist>", $generatedxml);
}
/**
* Test that an element can be deleted.
*/
public function test_delete_element_if_not_exists() {
$xml = $this->get_plist_xml_header()
. "<key>testKey</key>"
. "<string>testValue</string>"
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$plist->delete_element('nonExistentKey');
$generatedxml = trim($plist->to_xml());
// The xml should be unaltered.
$this->assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\"><dict><key>testKey</key><string>testValue</string></dict></plist>", $generatedxml);
}
/**
* Test that json is exported correctly according to SEB Config Key requirements.
*
* @param string $xml PList XML used to generate CFPropertyList.
* @param string $expectedjson Expected JSON output.
*
* @dataProvider json_data_provider
*/
public function test_export_to_json($xml, $expectedjson) {
$xml = $this->get_plist_xml_header()
. $xml
. $this->get_plist_xml_footer();
$plist = new property_list($xml);
$generatedjson = $plist->to_json();
$this->assertEquals($expectedjson, $generatedjson);
}
/**
* Test that the xml is exported to JSON from a real SEB config file. Expected JSON extracted from SEB logs.
*/
public function test_export_to_json_full_file() {
$xml = file_get_contents(__DIR__ . '/fixtures/unencrypted_mac_001.seb');
$plist = new property_list($xml);
$plist->delete_element('originatorVersion'); // JSON should not contain originatorVersion key.
$generatedjson = $plist->to_json();
$json = trim(file_get_contents(__DIR__ . '/fixtures/JSON_unencrypted_mac_001.txt'));
$this->assertEquals($json, $generatedjson);
}
/**
* Test the set_or_update_value function.
*/
public function test_set_or_update_value() {
$plist = new property_list();
$this->assertEmpty($plist->get_element_value('string'));
$this->assertEmpty($plist->get_element_value('bool'));
$this->assertEmpty($plist->get_element_value('number'));
// Setting values.
$plist->set_or_update_value('string', new \CFPropertyList\CFString('initial string'));
$plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(true));
$plist->set_or_update_value('number', new \CFPropertyList\CFNumber('10'));
$this->assertEquals('initial string', $plist->get_element_value('string'));
$this->assertEquals(true, $plist->get_element_value('bool'));
$this->assertEquals(10, $plist->get_element_value('number'));
// Updating values.
$plist->set_or_update_value('string', new \CFPropertyList\CFString('new string'));
$plist->set_or_update_value('bool', new \CFPropertyList\CFBoolean(false));
$plist->set_or_update_value('number', new \CFPropertyList\CFNumber('42'));
$this->assertEquals('new string', $plist->get_element_value('string'));
$this->assertEquals(false, $plist->get_element_value('bool'));
$this->assertEquals(42, $plist->get_element_value('number'));
// Type exception.
< $this->expectException(TypeError::class);
> $this->expectException(\TypeError::class);
$plist->set_or_update_value('someKey', 'We really need to pass in CFTypes here');
}
/**
* Get a valid PList header. Must also use footer.
*
* @return string
*/
private function get_plist_xml_header() : string {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
. "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" "
. "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
. "<plist version=\"1.0\">\n"
. " <dict>";
}
/**
* Get a valid PList footer. Must also use header.
*
* @return string
*/
private function get_plist_xml_footer() : string {
return " </dict>\n"
. "</plist>";
}
/**
* Data provider for good data on update.
*
* @return array Array with test data.
*/
public function good_update_data_provider() : array {
return [
'Update string' => ['<key>testKey</key><string>testValue</string>', 'testKey', 'newValue'],
'Update bool' => ['<key>testKey</key><true/>', 'testKey', false],
'Update number' => ['<key>testKey</key><real>888</real>', 'testKey', 123.4],
];
}
/**
* Data provider for bad data on update.
*
* @return array Array with test data.
*/
public function bad_update_data_provider() : array {
return [
'Update string with bool' => ['<key>testKey</key><string>testValue</string>', 'testKey', true, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with number' => ['<key>testKey</key><string>testValue</string>', 'testKey', 999, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with null' => ['<key>testKey</key><string>testValue</string>', 'testKey', null, 'testValue',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFString'],
'Update string with array' => ['<key>testKey</key><string>testValue</string>', 'testKey', ['arrayValue'], 'testValue',
'Use update_element_array to update a collection.'],
'Update bool with string' => ['<key>testKey</key><true/>', 'testKey', 'testValue', true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with number' => ['<key>testKey</key><true/>', 'testKey', 999, true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with null' => ['<key>testKey</key><true/>', 'testKey', null, true,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFBool'],
'Update bool with array' => ['<key>testKey</key><true/>', 'testKey', ['testValue'], true,
'Use update_element_array to update a collection.'],
'Update number with string' => ['<key>testKey</key><real>888</real>', 'testKey', 'string', 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with bool' => ['<key>testKey</key><real>888</real>', 'testKey', true, 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with null' => ['<key>testKey</key><real>888</real>', 'testKey', null, 888,
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFNumber'],
'Update number with array' => ['<key>testKey</key><real>888</real>', 'testKey', ['testValue'], 888,
'Use update_element_array to update a collection.'],
'Update date with string' => ['<key>testKey</key><date>1940-10-09T22:13:56Z</date>', 'testKey', 'string',
'1940-10-10T06:13:56+08:00',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFDate'],
'Update data with number' => ['<key>testKey</key><data>testData</data>', 'testKey', 789, 'testData',
'Invalid parameter value detected (Only string, number and boolean elements can be updated, '
. 'or value type does not match element type: CFPropertyList\CFData'],
];
}
/**
* Data provider for expected JSON from PList.
*
* Examples extracted from requirements listed in SEB Config Key documents.
* https://safeexambrowser.org/developer/seb-config-key.html
*
* 1. Date should be in ISO 8601 format.
* 2. Data should be base 64 encoded.
* 3. String should be UTF-8 encoded.
* 4, 5, 6, 7. No requirements for bools, arrays or dicts.
* 8. Empty dicts should not be included.
* 9. JSON key ordering should be case insensitive, and use string ordering.
* 10. URL forward slashes should not be escaped.
*
* @return array
*/
public function json_data_provider() : array {
$data = "blahblah";
$base64data = base64_encode($data);
return [
'date' => ["<key>date</key><date>1940-10-09T22:13:56Z</date>", "{\"date\":\"1940-10-09T22:13:56+00:00\"}"],
'data' => ["<key>data</key><data>$base64data</data>", "{\"data\":\"$base64data\"}"],
'string' => ["<key>string</key><string>hello wörld</string>", "{\"string\":\"hello wörld\"}"],
'string with 1 backslash' => ["<key>string</key><string>ws:\localhost</string>", "{\"string\":\"ws:\localhost\"}"],
'string with 2 backslashes' => ["<key>string</key><string>ws:\\localhost</string>",
'{"string":"ws:\\localhost"}'],
'string with 3 backslashes' => ["<key>string</key><string>ws:\\\localhost</string>",
'{"string":"ws:\\\localhost"}'],
'string with 4 backslashes' => ["<key>string</key><string>ws:\\\\localhost</string>",
'{"string":"ws:\\\\localhost"}'],
'string with 5 backslashes' => ["<key>string</key><string>ws:\\\\\localhost</string>",
'{"string":"ws:\\\\\localhost"}'],
'bool' => ["<key>bool</key><true/>", "{\"bool\":true}"],
'array' => ["<key>array</key><array><key>arraybool</key><false/><key>arraybool2</key><true/></array>"
, "{\"array\":[false,true]}"],
'empty array' => ["<key>bool</key><true/><key>array</key><array/>"
, "{\"array\":[],\"bool\":true}"],
'dict' => ["<key>dict</key><dict><key>dictbool</key><false/><key>dictbool2</key><true/></dict>"
, "{\"dict\":{\"dictbool\":false,\"dictbool2\":true}}"],
'empty dict' => ["<key>bool</key><true/><key>emptydict</key><dict/>", "{\"bool\":true}"],
'unordered elements' => ["<key>testKey</key>"
. "<string>testValue</string>"
. "<key>allowWLAN</key>"
. "<string>testValue2</string>"
. "<key>allowWlan</key>"
. "<string>testValue3</string>"
, "{\"allowWlan\":\"testValue3\",\"allowWLAN\":\"testValue2\",\"testKey\":\"testValue\"}"],
'url' => ["<key>url</key><string>http://test.com</string>", "{\"url\":\"http://test.com\"}"],
'assoc dict' => ["<key>dict</key><dict><key>banana</key><false/><key>apple</key><true/></dict>",
"{\"dict\":{\"apple\":true,\"banana\":false}}"],
'seq array' => ["<key>array</key><array><key>1</key><false/><key>2</key><true/>
<key>3</key><true/><key>4</key><true/><key>5</key><true/><key>6</key><true/>
<key>7</key><true/><key>8</key><true/><key>9</key><true/><key>10</key><true/></array>",
"{\"array\":[false,true,true,true,true,true,true,true,true,true]}"],
];
}
}