. /** * PHPUnit tests for fileconverter API. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use core_files\conversion; use core_files\converter; /** * PHPUnit tests for fileconverter API. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_files_converter_testcase extends advanced_testcase { /** * Get a testable mock of the abstract files_converter class. * * @param array $mockedmethods A list of methods you intend to override * If no methods are specified, only abstract functions are mocked. * @return \core_files\converter */ protected function get_testable_mock($mockedmethods = []) { $converter = $this->getMockBuilder(\core_files\converter::class) ->setMethods($mockedmethods) ->getMockForAbstractClass(); return $converter; } /** * Get a testable mock of the conversion. * * @param array $mockedmethods A list of methods you intend to override * @return \core_files\conversion */ protected function get_testable_conversion($mockedmethods = []) { $conversion = $this->getMockBuilder(\core_files\conversion::class) ->setMethods($mockedmethods) ->setConstructorArgs([0, (object) []]) ->getMock(); return $conversion; } /** * Get a testable mock of the abstract files_converter class. * * @param array $mockedmethods A list of methods you intend to override * If no methods are specified, only abstract functions are mocked. * @return \core_files\converter_interface */ protected function get_mocked_converter($mockedmethods = []) { $converter = $this->getMockBuilder(\core_files\converter_interface::class) ->setMethods($mockedmethods) ->getMockForAbstractClass(); return $converter; } /** * Helper to create a stored file objectw with the given supplied content. * * @param string $filecontent The content of the mocked file * @param string $filename The file name to use in the stored_file * @param array $mockedmethods A list of methods you intend to override * If no methods are specified, only abstract functions are mocked. * @return stored_file */ protected function get_stored_file($filecontent = 'content', $filename = null, $filerecord = [], $mockedmethods = null) { global $CFG; $contenthash = sha1($filecontent); if (empty($filename)) { $filename = $contenthash; } $filerecord['contenthash'] = $contenthash; $filerecord['filesize'] = strlen($filecontent); $filerecord['filename'] = $filename; $filerecord['id'] = 42; $file = $this->getMockBuilder(stored_file::class) ->setMethods($mockedmethods) ->setConstructorArgs([get_file_storage(), (object) $filerecord]) ->getMock(); return $file; } /** * Helper to create a stored file object with the given supplied content. * * @param string $filecontent The content of the mocked file * @param string $filename The file name to use in the stored_file * @param string $filerecord Any overrides to the filerecord * @return stored_file */ protected function create_stored_file($filecontent = 'content', $filename = 'testfile.txt', $filerecord = []) { $filerecord = array_merge([ 'contextid' => context_system::instance()->id, 'component' => 'core', 'filearea' => 'unittest', 'itemid' => 0, 'filepath' => '/', 'filename' => $filename, ], $filerecord); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, $filecontent); return $file; } /** * Get a mock of the file_storage API. * * @param array $mockedmethods A list of methods you intend to override * @return file_storage */ protected function get_file_storage_mock($mockedmethods = []) { $fs = $this->getMockBuilder(\file_storage::class) ->setMethods($mockedmethods) ->disableOriginalConstructor() ->getMock(); return $fs; } /** * Test the start_conversion function. */ public function test_start_conversion_existing_single() { $this->resetAfterTest(); $sourcefile = $this->create_stored_file(); $first = new conversion(0, (object) [ 'sourcefileid' => $sourcefile->get_id(), 'targetformat' => 'pdf', ]); $first->create(); $converter = $this->get_testable_mock(['poll_conversion']); $conversion = $converter->start_conversion($sourcefile, 'pdf', false); // The old conversions should still be present and match the one returned. $this->assertEquals($first->get('id'), $conversion->get('id')); } /** * Test the start_conversion function. */ public function test_start_conversion_existing_multiple() { $this->resetAfterTest(); $sourcefile = $this->create_stored_file(); $first = new conversion(0, (object) [ 'sourcefileid' => $sourcefile->get_id(), 'targetformat' => 'pdf', ]); $first->create(); $second = new conversion(0, (object) [ 'sourcefileid' => $sourcefile->get_id(), 'targetformat' => 'pdf', ]); $second->create(); $converter = $this->get_testable_mock(['poll_conversion']); $conversion = $converter->start_conversion($sourcefile, 'pdf', false); // The old conversions should have been removed. $this->assertFalse(conversion::get_record(['id' => $first->get('id')])); $this->assertFalse(conversion::get_record(['id' => $second->get('id')])); } /** * Test the start_conversion function. */ public function test_start_conversion_no_existing() { $this->resetAfterTest(); $sourcefile = $this->create_stored_file(); $converter = $this->get_testable_mock(['poll_conversion']); $conversion = $converter->start_conversion($sourcefile, 'pdf', false); $this->assertInstanceOf(\core_files\conversion::class, $conversion); } /** * Test the get_document_converter_classes function with no enabled plugins. */ public function test_get_document_converter_classes_no_plugins() { $converter = $this->get_testable_mock(['get_enabled_plugins']); $converter->method('get_enabled_plugins')->willReturn([]); $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes'); $method->setAccessible(true); $result = $method->invokeArgs($converter, ['docx', 'pdf']); $this->assertEmpty($result); } /** * Test the get_document_converter_classes function when no class was found. */ public function test_get_document_converter_classes_plugin_class_not_found() { $converter = $this->get_testable_mock(['get_enabled_plugins']); $converter->method('get_enabled_plugins')->willReturn([ 'noplugin' => '\not\a\real\plugin', ]); $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes'); $method->setAccessible(true); $result = $method->invokeArgs($converter, ['docx', 'pdf']); $this->assertEmpty($result); } /** * Test the get_document_converter_classes function when the returned classes do not meet requirements. */ public function test_get_document_converter_classes_plugin_class_requirements_not_met() { $plugin = $this->getMockBuilder(\core_file_converter_requirements_not_met_test::class) ->setMethods() ->getMock(); $converter = $this->get_testable_mock(['get_enabled_plugins']); $converter->method('get_enabled_plugins')->willReturn([ 'test_plugin' => get_class($plugin), ]); $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes'); $method->setAccessible(true); $result = $method->invokeArgs($converter, ['docx', 'pdf']); $this->assertEmpty($result); } /** * Test the get_document_converter_classes function when the returned classes do not meet requirements. */ public function test_get_document_converter_classes_plugin_class_met_not_supported() { $plugin = $this->getMockBuilder(\core_file_converter_type_not_supported_test::class) ->setMethods() ->getMock(); $converter = $this->get_testable_mock(['get_enabled_plugins']); $converter->method('get_enabled_plugins')->willReturn([ 'test_plugin' => get_class($plugin), ]); $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes'); $method->setAccessible(true); $result = $method->invokeArgs($converter, ['docx', 'pdf']); $this->assertEmpty($result); } /** * Test the get_document_converter_classes function when the returned classes do not meet requirements. */ public function test_get_document_converter_classes_plugin_class_met_and_supported() { $plugin = $this->getMockBuilder(\core_file_converter_type_supported_test::class) ->setMethods() ->getMock(); $classname = get_class($plugin); $converter = $this->get_testable_mock(['get_enabled_plugins']); $converter->method('get_enabled_plugins')->willReturn([ 'test_plugin' => $classname, ]); $method = new ReflectionMethod(\core_files\converter::class, 'get_document_converter_classes'); $method->setAccessible(true); $result = $method->invokeArgs($converter, ['docx', 'pdf']); $this->assertCount(1, $result); $this->assertNotFalse(array_search($classname, $result)); } /** * Test the can_convert_storedfile_to function with a directory. */ public function test_can_convert_storedfile_to_directory() { $converter = $this->get_testable_mock(); // A file with filename '.' is a directory. $file = $this->get_stored_file('', '.'); $this->assertFalse($converter->can_convert_storedfile_to($file, 'target')); } /** * Test the can_convert_storedfile_to function with an empty file. */ public function test_can_convert_storedfile_to_emptyfile() { $converter = $this->get_testable_mock(); // A file with filename '.' is a directory. $file = $this->get_stored_file(''); $this->assertFalse($converter->can_convert_storedfile_to($file, 'target')); } /** * Test the can_convert_storedfile_to function with a file with indistinguished mimetype. */ public function test_can_convert_storedfile_to_no_mimetype() { $converter = $this->get_testable_mock(); // A file with filename '.' is a directory. $file = $this->get_stored_file('example content', 'example', [ 'mimetype' => null, ]); $this->assertFalse($converter->can_convert_storedfile_to($file, 'target')); } /** * Test the can_convert_storedfile_to function with a file with a known mimetype and extension. */ public function test_can_convert_storedfile_to_docx() { $returnvalue = (object) []; $converter = $this->get_testable_mock([ 'can_convert_format_to' ]); $types = \core_filetypes::get_types(); $file = $this->get_stored_file('example content', 'example.docx', [ 'mimetype' => $types['docx']['type'], ]); $converter->expects($this->once()) ->method('can_convert_format_to') ->willReturn($returnvalue); $result = $converter->can_convert_storedfile_to($file, 'target'); $this->assertEquals($returnvalue, $result); } /** * Test the can_convert_format_to function. */ public function test_can_convert_format_to_found() { $converter = $this->get_testable_mock(['get_document_converter_classes']); $mock = $this->get_mocked_converter(); $converter->method('get_document_converter_classes') ->willReturn([$mock]); $result = $converter->can_convert_format_to('from', 'to'); $this->assertTrue($result); } /** * Test the can_convert_format_to function. */ public function test_can_convert_format_to_not_found() { $converter = $this->get_testable_mock(['get_document_converter_classes']); $converter->method('get_document_converter_classes') ->willReturn([]); $result = $converter->can_convert_format_to('from', 'to'); $this->assertFalse($result); } /** * Test the can_convert_storedfile_to function with an empty file. */ public function test_poll_conversion_in_progress() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', 'get_next_converter', ]); $converter->method('get_document_converter_classes')->willReturn([]); $converter->method('get_next_converter')->willReturn(false); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $this->get_testable_conversion([ 'get_converter_instance', ]); $conversion->set_sourcefile($file); $conversion->set('targetformat', 'target'); $conversion->set('status', conversion::STATUS_IN_PROGRESS); $conversion->create(); $converterinstance = $this->get_mocked_converter([ 'poll_conversion_status', ]); $converterinstance->expects($this->once()) ->method('poll_conversion_status'); $conversion->method('get_converter_instance')->willReturn($converterinstance); $converter->poll_conversion($conversion); $this->assertEquals(conversion::STATUS_IN_PROGRESS, $conversion->get('status')); } /** * Test poll_conversion with an in-progress conversion where we are * unable to instantiate the converter instance. */ public function test_poll_conversion_in_progress_fail() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', 'get_next_converter', ]); $converter->method('get_document_converter_classes')->willReturn([]); $converter->method('get_next_converter')->willReturn(false); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $this->get_testable_conversion([ 'get_converter_instance', ]); $conversion->set_sourcefile($file); $conversion->set('targetformat', 'target'); $conversion->set('status', conversion::STATUS_IN_PROGRESS); $conversion->create(); $conversion->method('get_converter_instance')->willReturn(false); $converter->poll_conversion($conversion); $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status')); } /** * Test the can_convert_storedfile_to function with an empty file. */ public function test_poll_conversion_none_supported() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', 'get_next_converter', ]); $converter->method('get_document_converter_classes')->willReturn([]); $converter->method('get_next_converter')->willReturn(false); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = new conversion(0, (object) [ 'sourcefileid' => $file->get_id(), 'targetformat' => 'target', ]); $conversion->create(); $converter->poll_conversion($conversion); $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status')); } /** * Test the can_convert_storedfile_to function with an empty file. */ public function test_poll_conversion_pick_first() { $this->resetAfterTest(); $converterinstance = $this->get_mocked_converter([ 'start_document_conversion', 'poll_conversion_status', ]); $converter = $this->get_testable_mock([ 'get_document_converter_classes', 'get_next_converter', ]); $converter->method('get_document_converter_classes')->willReturn([]); $converter->method('get_next_converter')->willReturn(get_class($converterinstance)); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $this->get_testable_conversion([ 'get_converter_instance', ]); $conversion->set_sourcefile($file); $conversion->set('targetformat', 'target'); $conversion->set('status', conversion::STATUS_PENDING); $conversion->create(); $conversion->method('get_converter_instance')->willReturn($converterinstance); $converterinstance->expects($this->once()) ->method('start_document_conversion'); $converterinstance->expects($this->never()) ->method('poll_conversion_status'); $converter->poll_conversion($conversion); $this->assertEquals(conversion::STATUS_IN_PROGRESS, $conversion->get('status')); } /** * Test the can_convert_storedfile_to function with an empty file. */ public function test_poll_conversion_pick_subsequent() { $this->resetAfterTest(); $converterinstance = $this->get_mocked_converter([ 'start_document_conversion', 'poll_conversion_status', ]); $converterinstance2 = $this->get_mocked_converter([ 'start_document_conversion', 'poll_conversion_status', ]); $converter = $this->get_testable_mock([ 'get_document_converter_classes', 'get_next_converter', ]); $converter->method('get_document_converter_classes')->willReturn([]); $converter->method('get_next_converter') ->will($this->onConsecutiveCalls( get_class($converterinstance), get_class($converterinstance2) )); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $this->get_testable_conversion([ 'get_converter_instance', 'get_status', ]); $conversion->set_sourcefile($file); $conversion->set('targetformat', 'target'); $conversion->set('status', conversion::STATUS_PENDING); $conversion->create(); $conversion->method('get_status') ->will($this->onConsecutiveCalls( // Initial status check. conversion::STATUS_PENDING, // Second check to make sure it's still pending after polling. conversion::STATUS_PENDING, // First one fails. conversion::STATUS_FAILED, // Second one succeeds. conversion::STATUS_COMPLETE, // And the final result checked in this unit test. conversion::STATUS_COMPLETE )); $conversion->method('get_converter_instance') ->will($this->onConsecutiveCalls( $converterinstance, $converterinstance2 )); $converterinstance->expects($this->once()) ->method('start_document_conversion'); $converterinstance->expects($this->never()) ->method('poll_conversion_status'); $converterinstance2->expects($this->once()) ->method('start_document_conversion'); $converterinstance2->expects($this->never()) ->method('poll_conversion_status'); $converter->poll_conversion($conversion); $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status')); } /** * Test the start_conversion with a single converter which succeeds. */ public function test_start_conversion_one_supported_success() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', ]); $converter->method('get_document_converter_classes') ->willReturn([\core_file_converter_type_successful::class]); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $converter->start_conversion($file, 'target'); $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status')); } /** * Test the start_conversion with a single converter which failes. */ public function test_start_conversion_one_supported_failure() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', ]); $mock = $this->get_mocked_converter(['start_document_conversion']); $converter->method('get_document_converter_classes') ->willReturn([\core_file_converter_type_failed::class]); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $converter->start_conversion($file, 'target'); $this->assertEquals(conversion::STATUS_FAILED, $conversion->get('status')); } /** * Test the start_conversion with two converters - fail, then succeed. */ public function test_start_conversion_two_supported() { $this->resetAfterTest(); $converter = $this->get_testable_mock([ 'get_document_converter_classes', ]); $mock = $this->get_mocked_converter(['start_document_conversion']); $converter->method('get_document_converter_classes') ->willReturn([ \core_file_converter_type_failed::class, \core_file_converter_type_successful::class, ]); $file = $this->create_stored_file('example content', 'example', [ 'mimetype' => null, ]); $conversion = $converter->start_conversion($file, 'target'); $this->assertEquals(conversion::STATUS_COMPLETE, $conversion->get('status')); } /** * Ensure that get_next_converter returns false when no converters are available. */ public function test_get_next_converter_no_converters() { $rcm = new \ReflectionMethod(converter::class, 'get_next_converter'); $rcm->setAccessible(true); $converter = new \core_files\converter(); $result = $rcm->invoke($converter, [], null); $this->assertFalse($result); } /** * Ensure that get_next_converter returns false when already on the * only converter. */ public function test_get_next_converter_only_converters() { $rcm = new \ReflectionMethod(converter::class, 'get_next_converter'); $rcm->setAccessible(true); $converter = new converter(); $result = $rcm->invoke($converter, ['example'], 'example'); $this->assertFalse($result); } /** * Ensure that get_next_converter returns false when already on the * last converter. */ public function test_get_next_converter_last_converters() { $rcm = new \ReflectionMethod(converter::class, 'get_next_converter'); $rcm->setAccessible(true); $converter = new converter(); $result = $rcm->invoke($converter, ['foo', 'example'], 'example'); $this->assertFalse($result); } /** * Ensure that get_next_converter returns the next vlaue when in a * current converter. */ public function test_get_next_converter_middle_converters() { $rcm = new \ReflectionMethod(converter::class, 'get_next_converter'); $rcm->setAccessible(true); $converter = new converter(); $result = $rcm->invoke($converter, ['foo', 'bar', 'baz', 'example'], 'bar'); $this->assertEquals('baz', $result); } /** * * Ensure that get_next_converter returns the next vlaue when in a * current converter. */ public function test_get_next_converter_first() { $rcm = new \ReflectionMethod(converter::class, 'get_next_converter'); $rcm->setAccessible(true); $converter = new converter(); $result = $rcm->invoke($converter, ['foo', 'bar', 'baz', 'example']); $this->assertEquals('foo', $result); } } class core_file_converter_requirements_test_base implements \core_files\converter_interface { /** * Whether the plugin is configured and requirements are met. * * @return bool */ public static function are_requirements_met() { return false; } /** * Convert a document to a new format and return a conversion object relating to the conversion in progress. * * @param conversion $conversion The file to be converted * @return conversion */ public function start_document_conversion(conversion $conversion) { } /** * Poll an existing conversion for status update. * * @param conversion $conversion The file to be converted * @return conversion */ public function poll_conversion_status(conversion $conversion) { } /** * Whether a file conversion can be completed using this converter. * * @param string $from The source type * @param string $to The destination type * @return bool */ public static function supports($from, $to) { return false; } /** * A list of the supported conversions. * * @return string */ public function get_supported_conversions() { return []; } } /** * Test class for converter support with requirements are not met. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_file_converter_requirements_not_met_test extends core_file_converter_requirements_test_base { } /** * Test class for converter support with requirements met and conversion not supported. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_file_converter_type_not_supported_test extends core_file_converter_requirements_test_base { /** * Whether the plugin is configured and requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } } /** * Test class for converter support with requirements met and conversion supported. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_file_converter_type_supported_test extends core_file_converter_requirements_test_base { /** * Whether the plugin is configured and requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } /** * Whether a file conversion can be completed using this converter. * * @param string $from The source type * @param string $to The destination type * @return bool */ public static function supports($from, $to) { return true; } } /** * Test class for converter support with requirements met and successful conversion. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_file_converter_type_successful extends core_file_converter_requirements_test_base { /** * Convert a document to a new format and return a conversion object relating to the conversion in progress. * * @param conversion $conversion The file to be converted * @return conversion */ public function start_document_conversion(conversion $conversion) { $conversion->set('status', conversion::STATUS_COMPLETE); return $conversion; } /** * Whether a file conversion can be completed using this converter. * * @param string $from The source type * @param string $to The destination type * @return bool */ public static function supports($from, $to) { return true; } } /** * Test class for converter support with requirements met and failed conversion. * * @package core_files * @copyright 2017 Andrew nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_file_converter_type_failed extends core_file_converter_requirements_test_base { /** * Whether the plugin is configured and requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } /** * Convert a document to a new format and return a conversion object relating to the conversion in progress. * * @param conversion $conversion The file to be converted * @return conversion */ public function start_document_conversion(conversion $conversion) { $conversion->set('status', conversion::STATUS_FAILED); return $conversion; } /** * Whether a file conversion can be completed using this converter. * * @param string $from The source type * @param string $to The destination type * @return bool */ public static function supports($from, $to) { return true; } }