. /** * Unit Tests for the Moodle Content Writer. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\writer; use \core_privacy\local\request\moodle_content_writer; /** * Tests for the \core_privacy API's moodle_content_writer functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\moodle_content_writer */ class moodle_content_writer_test extends advanced_testcase { /** * Test that exported data is saved correctly within the system context. * * @dataProvider export_data_provider * @param \stdClass $data Data * @covers ::export_data * @covers :: */ public function test_export_data($data) { $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported data is saved correctly for context/subcontext. * * @dataProvider export_data_provider * @param \stdClass $data Data * @covers ::export_data * @covers :: */ public function test_export_data_different_context($data) { $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); $subcontext = ['sub', 'context']; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported is saved within the correct directory locations. * * @covers ::export_data * @covers :: */ public function test_export_data_writes_to_multiple_context() { $subcontext = ['sub', 'context']; $systemcontext = \context_system::instance(); $systemdata = (object) [ 'belongsto' => 'system', ]; $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); $userdata = (object) [ 'belongsto' => 'user', ]; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_data($subcontext, $systemdata); $writer ->set_context($usercontext) ->export_data($subcontext, $userdata); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($systemdata, $expanded); $contextpath = $this->get_context_path($usercontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($userdata, $expanded); } /** * Test that multiple writes to the same location cause the latest version to be written. * * @covers ::export_data * @covers :: */ public function test_export_data_multiple_writes_same_context() { $subcontext = ['sub', 'context']; $systemcontext = \context_system::instance(); $originaldata = (object) [ 'belongsto' => 'system', ]; $newdata = (object) [ 'abc' => 'def', ]; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_data($subcontext, $originaldata); $writer ->set_context($systemcontext) ->export_data($subcontext, $newdata); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($newdata, $expanded); } /** * Data provider for exporting user data. */ public function export_data_provider() { return [ 'basic' => [ (object) [ 'example' => (object) [ 'key' => 'value', ], ], ], ]; } /** * Test that metadata can be set. * * @dataProvider export_metadata_provider * @param string $key Key * @param string $value Value * @param string $description Description * @covers ::export_metadata * @covers :: */ public function test_export_metadata($key, $value, $description) { $context = \context_system::instance(); $subcontext = ['a', 'b', 'c']; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $key, $value, $description); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $this->assertEquals($value, $expanded->$key->value); $this->assertEquals($description, $expanded->$key->description); } /** * Test that metadata can be set additively. * * @covers ::export_metadata * @covers :: */ public function test_export_metadata_additive() { $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance(); $writer ->set_context($context) ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription'); $writer ->set_context($context) ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('firstvalue', $expanded->firstkey->value); $this->assertEquals('firstdescription', $expanded->firstkey->description); $this->assertTrue(isset($expanded->secondkey)); $this->assertEquals('secondvalue', $expanded->secondkey->value); $this->assertEquals('seconddescription', $expanded->secondkey->description); } /** * Test that metadata can be set additively. * * @covers ::export_metadata * @covers :: */ public function test_export_metadata_to_multiple_contexts() { $systemcontext = \context_system::instance(); $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); $subcontext = []; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription') ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); $writer ->set_context($usercontext) ->export_metadata($subcontext, 'firstkey', 'alternativevalue', 'alternativedescription') ->export_metadata($subcontext, 'thirdkey', 'thirdvalue', 'thirddescription'); $fileroot = $this->fetch_exported_content($writer); $systemcontextpath = $this->get_context_path($systemcontext, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($systemcontextpath)); $json = $fileroot->getChild($systemcontextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('firstvalue', $expanded->firstkey->value); $this->assertEquals('firstdescription', $expanded->firstkey->description); $this->assertTrue(isset($expanded->secondkey)); $this->assertEquals('secondvalue', $expanded->secondkey->value); $this->assertEquals('seconddescription', $expanded->secondkey->description); $this->assertFalse(isset($expanded->thirdkey)); $usercontextpath = $this->get_context_path($usercontext, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($usercontextpath)); $json = $fileroot->getChild($usercontextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('alternativevalue', $expanded->firstkey->value); $this->assertEquals('alternativedescription', $expanded->firstkey->description); $this->assertFalse(isset($expanded->secondkey)); $this->assertTrue(isset($expanded->thirdkey)); $this->assertEquals('thirdvalue', $expanded->thirdkey->value); $this->assertEquals('thirddescription', $expanded->thirdkey->description); } /** * Data provider for exporting user metadata. * * return array */ public function export_metadata_provider() { return [ 'basic' => [ 'key', 'value', 'This is a description', ], 'valuewithspaces' => [ 'key', 'value has mixed', 'This is a description', ], 'encodedvalue' => [ 'key', base64_encode('value has mixed'), 'This is a description', ], ]; } /** * Exporting a single stored_file should cause that file to be output in the files directory. * * @covers ::export_area_files * @covers :: */ public function test_export_area_files() { $this->resetAfterTest(); $context = \context_system::instance(); $fs = get_file_storage(); // Add two files to core_privacy::tests::0. $files = []; $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'a.txt', 'content' => 'Test file 0', ]; $files[] = $file; $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/sub/', 'name' => 'b.txt', 'content' => 'Test file 1', ]; $files[] = $file; // One with a different itemid. $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 1, 'path' => '/', 'name' => 'c.txt', 'content' => 'Other', ]; $files[] = $file; // One with a different filearea. $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'alternative', 'itemid' => 0, 'path' => '/', 'name' => 'd.txt', 'content' => 'Alternative', ]; $files[] = $file; // One with a different component. $file = (object) [ 'component' => 'core', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'e.txt', 'content' => 'Other tests', ]; $files[] = $file; foreach ($files as $file) { $record = [ 'contextid' => $context->id, 'component' => $file->component, 'filearea' => $file->filearea, 'itemid' => $file->itemid, 'filepath' => $file->path, 'filename' => $file->name, ]; $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name; $file->storedfile = $fs->create_file_from_string($record, $file->content); } $writer = $this->get_writer_instance() ->set_context($context) ->export_area_files([], 'core_privacy', 'tests', 0); $fileroot = $this->fetch_exported_content($writer); $firstfiles = array_slice($files, 0, 2); foreach ($firstfiles as $file) { $contextpath = $this->get_context_path($context, ['_files'], $file->namepath); $this->assertTrue($fileroot->hasChild($contextpath)); $this->assertEquals($file->content, $fileroot->getChild($contextpath)->getContent()); } $otherfiles = array_slice($files, 2); foreach ($otherfiles as $file) { $contextpath = $this->get_context_path($context, ['_files'], $file->namepath); $this->assertFalse($fileroot->hasChild($contextpath)); } } /** * Exporting a single stored_file should cause that file to be output in the files directory. * * @dataProvider export_file_provider * @param string $filearea File area * @param int $itemid Item ID * @param string $filepath File path * @param string $filename File name * @param string $content Content * * @covers ::export_file * @covers :: */ public function test_export_file($filearea, $itemid, $filepath, $filename, $content) { $this->resetAfterTest(); $context = \context_system::instance(); $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename; $filerecord = array( 'contextid' => $context->id, 'component' => 'core_privacy', 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => $filepath, 'filename' => $filename, ); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, $content); $writer = $this->get_writer_instance() ->set_context($context) ->export_file([], $file); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, ['_files'], $filenamepath); $this->assertTrue($fileroot->hasChild($contextpath)); $this->assertEquals($content, $fileroot->getChild($contextpath)->getContent()); } /** * Data provider for the test_export_file function. * * @return array */ public function export_file_provider() { return [ 'basic' => [ 'intro', 0, '/', 'testfile.txt', 'An example file content', ], 'longpath' => [ 'attachments', '12', '/path/within/a/path/within/a/path/', 'testfile.txt', 'An example file content', ], 'pathwithspaces' => [ 'intro', 0, '/path with/some spaces/', 'testfile.txt', 'An example file content', ], 'filewithspaces' => [ 'submission_attachments', 1, '/path with/some spaces/', 'test file.txt', 'An example file content', ], 'image' => [ 'intro', 0, '/', 'logo.png', file_get_contents(__DIR__ . '/fixtures/logo.png'), ], 'UTF8' => [ 'submission_content', 2, '/Žluťoučký/', 'koníček.txt', 'koníček', ], 'EUC-JP' => [ 'intro', 0, '/言語設定/', '言語設定.txt', '言語設定', ], ]; } /** * User preferences can be exported against a user. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_user($component, $key, $value, $desc) { $admin = \core_user::get_user_by_username('admin'); $writer = $this->get_writer_instance(); $context = \context_user::instance($admin->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a course category. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_coursecat($component, $key, $value, $desc) { global $DB; $categories = $DB->get_records('course_categories'); $firstcategory = reset($categories); $context = \context_coursecat::instance($firstcategory->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a course. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_course($component, $key, $value, $desc) { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a module context. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_module($component, $key, $value, $desc) { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can not be exported against a block context. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_block($component, $key, $value, $desc) { global $DB; $blocks = $DB->get_records('block_instances'); $block = reset($blocks); $context = \context_block::instance($block->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * Writing user preferences for two different blocks with the same name and * same parent context should generate two different context paths and export * files. * * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_block_multiple_instances() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $coursecontext = context_course::instance($course->id); $block1 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]); $block2 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]); $block1context = context_block::instance($block1->id); $block2context = context_block::instance($block2->id); $component = 'block'; $desc = 'test preference'; $block1key = 'block1key'; $block1value = 'block1value'; $block2key = 'block2key'; $block2value = 'block2value'; $writer = $this->get_writer_instance(); // Confirm that we have two different block contexts with the same name // and the same parent context id. $this->assertNotEquals($block1context->id, $block2context->id); $this->assertEquals($block1context->get_context_name(), $block2context->get_context_name()); $this->assertEquals($block1context->get_parent_context()->id, $block2context->get_parent_context()->id); $retrieveexport = function($context) use ($writer, $component) { $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); return json_decode($json); }; $writer->set_context($block1context) ->export_user_preference($component, $block1key, $block1value, $desc); $writer->set_context($block2context) ->export_user_preference($component, $block2key, $block2value, $desc); $block1export = $retrieveexport($block1context); $block2export = $retrieveexport($block2context); // Confirm that the exports didn't write to the same file. $this->assertTrue(isset($block1export->$block1key)); $this->assertTrue(isset($block2export->$block2key)); $this->assertFalse(isset($block1export->$block2key)); $this->assertFalse(isset($block2export->$block1key)); $this->assertEquals($block1value, $block1export->$block1key->value); $this->assertEquals($block2value, $block2export->$block2key->value); } /** * User preferences can be exported against the system. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_context_system($component, $key, $value, $desc) { $context = \context_system::instance(); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against the system. * * @covers ::export_user_preference * @covers :: */ public function test_export_multiple_user_preference_context_system() { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $component = 'core_privacy'; $writer ->set_context($context) ->export_user_preference($component, 'key1', 'val1', 'desc1') ->export_user_preference($component, 'key2', 'val2', 'desc2'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->key1)); $data = $expanded->key1; $this->assertEquals('val1', $data->value); $this->assertEquals('desc1', $data->description); $this->assertTrue(isset($expanded->key2)); $data = $expanded->key2; $this->assertEquals('val2', $data->value); $this->assertEquals('desc2', $data->description); } /** * User preferences can be exported against the system. * * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_replace() { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $component = 'core_privacy'; $key = 'key'; $writer ->set_context($context) ->export_user_preference($component, $key, 'val1', 'desc1'); $writer ->set_context($context) ->export_user_preference($component, $key, 'val2', 'desc2'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals('val2', $data->value); $this->assertEquals('desc2', $data->description); } /** * Provider for various user preferences. * * @return array */ public function export_user_preference_provider() { return [ 'basic' => [ 'core_privacy', 'onekey', 'value', 'description', ], 'encodedvalue' => [ 'core_privacy', 'donkey', base64_encode('value'), 'description', ], 'long description' => [ 'core_privacy', 'twokey', 'value', 'This is a much longer description which actually states what this is used for. Blah blah blah.', ], ]; } /** * Test that exported data is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_data * @covers :: */ public function test_export_data_unescaped_unicode($text) { $context = \context_system::instance(); $subcontext = []; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported metadata is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_metadata * @covers :: */ public function test_export_metadata_unescaped_unicode($text) { $context = \context_system::instance(); $subcontext = ['a', 'b', 'c']; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Test that exported related data is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_related_data * @covers :: */ public function test_export_related_data_unescaped_unicode($text) { $context = \context_system::instance(); $subcontext = []; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data($subcontext, 'name', $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'name.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported user preference is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_user_preference * @covers :: */ public function test_export_user_preference_unescaped_unicode($text) { $context = \context_system::instance(); $component = 'core_privacy'; $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Provider for various user preferences. * * @return array */ public function unescaped_unicode_export_provider() { return [ 'Unicode' => ['ةكءيٓ‌پچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'], ]; } /** * Test that exported data is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text * * @covers ::export_data * @covers :: */ public function test_export_data_long_filename($longtext, $expected, $text) { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $expectedpath = "System _.{$context->id}/{$expected}/data.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported related data is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text * * @covers ::export_related_data * @covers :: */ public function test_export_related_data_long_filename($longtext, $expected, $text) { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data($subcontext, 'name', $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'name.json'); $expectedpath = "System _.{$context->id}/{$expected}/name.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported metadata is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text */ public function test_export_metadata_long_filename($longtext, $expected, $text) { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $expectedpath = "System _.{$context->id}/{$expected}/metadata.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Test that exported user preference is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text */ public function test_export_user_preference_long_filename($longtext, $expected, $text) { $this->resetAfterTest(); if (!array_key_exists('json', core_filetypes::get_types())) { // Add json as mime type to avoid lose the extension when shortening filenames. core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive'); } $context = \context_system::instance(); $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json"; $component = $longtext; $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertRegExp("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Provider for long filenames. * * @return array */ public function long_filename_provider() { return [ 'More than 100 characters' => [ 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean efficitur suscipit nibh nec suscipit', 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean effici - 22f7a5030d', 'value', ], ]; } /** * Get a fresh content writer. * * @return moodle_content_writer */ public function get_writer_instance() { $factory = $this->createMock(writer::class); return new moodle_content_writer($factory); } /** * Fetch the exported content for inspection. * * @param moodle_content_writer $writer * @return \org\bovigo\vfs\vfsStreamDirectory */ protected function fetch_exported_content(moodle_content_writer $writer) { $export = $writer ->set_context(\context_system::instance()) ->finalise_content(); $fileroot = \org\bovigo\vfs\vfsStream::setup('root'); $target = \org\bovigo\vfs\vfsStream::url('root'); $fp = get_file_packer(); $fp->extract_to_pathname($export, $target); return $fileroot; } /** * Determine the path for the current context. * * Note: This is a wrapper around the real function. * * @param \context $context The context being written * @param array $subcontext The subcontext path * @param string $name THe name of the file target * @return array The context path. */ protected function get_context_path($context, $subcontext = null, $name = '') { $rc = new ReflectionClass(moodle_content_writer::class); $writer = $this->get_writer_instance(); $writer->set_context($context); if (null === $subcontext) { $rcm = $rc->getMethod('get_context_path'); $rcm->setAccessible(true); $path = $rcm->invoke($writer); } else { $rcm = $rc->getMethod('get_path'); $rcm->setAccessible(true); $path = $rcm->invoke($writer, $subcontext, $name); } // PHPUnit uses mikey179/vfsStream which is a stream wrapper for a virtual file system that uses '/' // as the directory separator. $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); return $path; } /** * Test correct rewriting of @@PLUGINFILE@@ in the exported contents. * * @dataProvider rewrite_pluginfile_urls_provider * @param string $filearea The filearea within that component. * @param int $itemid Which item those files belong to. * @param string $input Raw text as stored in the database. * @param string $expectedoutput Expected output of URL rewriting. * @covers ::rewrite_pluginfile_urls * @covers :: */ public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput) { $writer = $this->get_writer_instance(); $writer->set_context(\context_system::instance()); $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input); $this->assertEquals($expectedoutput, $realoutput); } /** * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}. * * @return array */ public function rewrite_pluginfile_urls_provider() { return [ 'zeroitemid' => [ 'intro', 0, '

', '

', ], 'nonzeroitemid' => [ 'submission_content', 34, '

First

', '

First

', ], 'withfilepath' => [ 'post_content', 9889, 'Click here!', 'Click here!', ], ]; } public function test_export_html_functions() { $this->resetAfterTest(); $data = (object) ['key' => 'value']; $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, (object) $data); $writer->set_context($context)->export_data(['paper'], $data); $coursecategory = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($coursecategory->id); $course = $this->getDataGenerator()->create_course(); $misccoursecxt = \context_coursecat::instance($course->category); $coursecontext = \context_course::instance($course->id); $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]); $modulecontext = \context_module::instance($cm->cmid); $writer->set_context($modulecontext)->export_data([], $data); $writer->set_context($coursecontext)->export_data(['grades'], $data); $writer->set_context($categorycontext)->export_data([], $data); $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data); // Add a file. $fs = get_file_storage(); $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'a.txt', 'content' => 'Test file 0', ]; $record = [ 'contextid' => $context->id, 'component' => $file->component, 'filearea' => $file->filearea, 'itemid' => $file->itemid, 'filepath' => $file->path, 'filename' => $file->name, ]; $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name; $file->storedfile = $fs->create_file_from_string($record, $file->content); $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0); list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [], '\core_privacy\local\request\moodle_content_writer'); $expectedtreeoutput = [ 'System _.1' => [ 'data.json', 'paper' => 'data.json', 'Category Miscellaneous _.' . $misccoursecxt->id => [ 'Course Test course 1 _.' . $coursecontext->id => [ 'Chat Chat 1 _.' . $modulecontext->id => 'data.json', 'grades' => 'data.json' ] ], 'Category Course category 1 _.' . $categorycontext->id => 'data.json', '_files' => [ 'tests' => 'a.txt' ], 'Logs' => [ 'Standard log' => 'data.json' ] ] ]; $this->assertEquals($expectedtreeoutput, $tree); $expectedlistoutput = [ 'System _.1/data.json' => 'data_file_1', 'System _.1/paper/data.json' => 'data_file_2', 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json' => 'data_file_3', 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/grades/data.json' => 'data_file_4', 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5', 'System _.1/_files/tests/a.txt' => 'No var', 'System _.1/Logs/Standard log/data.json' => 'data_file_6' ]; $this->assertEquals($expectedlistoutput, $treelist); $expectedindex = [ 'data_file_1' => 'System _.1/data.js', 'data_file_2' => 'System _.1/paper/data.js', 'data_file_3' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js', 'data_file_4' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/grades/data.js', 'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js', 'data_file_6' => 'System _.1/Logs/Standard log/data.js' ]; $this->assertEquals($expectedindex, $indexdata); $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist], '\core_privacy\local\request\moodle_content_writer'); // This is a big one. $expectedrichtree = [ 'System _.1' => (object) [ 'itemtype' => 'treeitem', 'name' => 'System ', 'context' => \context_system::instance(), 'children' => [ (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_1' ], 'paper' => (object) [ 'itemtype' => 'treeitem', 'name' => 'paper', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_2' ] ] ], 'Category Miscellaneous _.' . $misccoursecxt->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Miscellaneous ', 'context' => $misccoursecxt, 'children' => [ 'Course Test course 1 _.' . $coursecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Course Test course 1 ', 'context' => $coursecontext, 'children' => [ 'Chat Chat 1 _.' . $modulecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Chat Chat 1 ', 'context' => $modulecontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_3' ] ] ], 'grades' => (object) [ 'itemtype' => 'treeitem', 'name' => 'grades', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_4' ] ] ] ] ] ] ], 'Category Course category 1 _.' . $categorycontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Course category 1 ', 'context' => $categorycontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_5' ] ] ], '_files' => (object) [ 'itemtype' => 'treeitem', 'name' => '_files', 'children' => [ 'tests' => (object) [ 'itemtype' => 'treeitem', 'name' => 'tests', 'children' => [ 'a.txt' => (object) [ 'name' => 'a.txt', 'itemtype' => 'item', 'url' => new \moodle_url('System _.1/_files/tests/a.txt') ] ] ] ] ], 'Logs' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Logs', 'children' => [ 'Standard log' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Standard log', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_6' ] ] ] ] ] ] ] ]; $this->assertEquals($expectedrichtree, $richtree); // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code // instead to do the same thing, but with references working obviously. $funfunction = function($object, $data) { return $object->sort_my_list($data); }; $funfunction = Closure::bind($funfunction, null, $writer); $funfunction($writer, $richtree); // This is a big one. $expectedsortedtree = [ 'System _.1' => (object) [ 'itemtype' => 'treeitem', 'name' => 'System ', 'context' => \context_system::instance(), 'children' => [ 'Category Miscellaneous _.' . $misccoursecxt->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Miscellaneous ', 'context' => $misccoursecxt, 'children' => [ 'Course Test course 1 _.' . $coursecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Course Test course 1 ', 'context' => $coursecontext, 'children' => [ 'Chat Chat 1 _.' . $modulecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Chat Chat 1 ', 'context' => $modulecontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_3' ] ] ], 'grades' => (object) [ 'itemtype' => 'treeitem', 'name' => 'grades', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_4' ] ] ] ] ] ] ], 'Category Course category 1 _.' . $categorycontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Course category 1 ', 'context' => $categorycontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_5' ] ] ], '_files' => (object) [ 'itemtype' => 'treeitem', 'name' => '_files', 'children' => [ 'tests' => (object) [ 'itemtype' => 'treeitem', 'name' => 'tests', 'children' => [ 'a.txt' => (object) [ 'name' => 'a.txt', 'itemtype' => 'item', 'url' => new \moodle_url('System _.1/_files/tests/a.txt') ] ] ] ] ], 'Logs' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Logs', 'children' => [ 'Standard log' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Standard log', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_6' ] ] ] ] ], 'paper' => (object) [ 'itemtype' => 'treeitem', 'name' => 'paper', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_2' ] ] ], (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_1' ] ] ] ]; $this->assertEquals($expectedsortedtree, $richtree); } }