From a24d8a0d2ad7f452a4f1fc2ea32090f4e552b3cb Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 5 Jul 2023 19:37:36 +0800 Subject: [PATCH] MDL-63399 behat: new steps to verify downloads Co-authored-by: Tim Hunt --- lib/tests/behat/behat_download.php | 211 +++++++++++++++++++++++++++++ lib/tests/behat/behat_general.php | 24 +++- 2 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 lib/tests/behat/behat_download.php diff --git a/lib/tests/behat/behat_download.php b/lib/tests/behat/behat_download.php new file mode 100644 index 00000000000..1ae4f5aa9fd --- /dev/null +++ b/lib/tests/behat/behat_download.php @@ -0,0 +1,211 @@ +. + +/** + * Steps definitions to verify a downloaded file. + * + * @package core + * @category test + * @copyright 2024 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use Behat\Gherkin\Node\TableNode; +use Behat\Mink\Exception\ExpectationException; + +require_once(__DIR__ . '/../../behat/behat_base.php'); + +/** + * Steps definitions to verify a downloaded file. + * + * @package core + * @category test + * @copyright 2024 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_download extends behat_base { + + /** + * Downloads the file from a link on the page and verify the type and content. + * + * @Then following :link_text should download a file that: + * + * @param string $linktext the text of the link. + * @param TableNode $table the table of assertions to use the check the file contents. + * @throws ExpectationException if the file cannot be downloaded, or if the download does not pass all the checks. + */ + public function following_should_download_a_file_that(string $linktext, TableNode $table): void { + $this->following_in_element_should_download_a_file_that($linktext, '', '', $table); + } + + /** + * Downloads the file from a link on the page and verify the type and content. + * + * @Then following :link_text in the :element_container_string :text_selector_string should download a file that: + * + * @param string $linktext the text of the link. + * @param string $containerlocator the container element. + * @param string $containertype the container selector type. + * @param TableNode $table the table of assertions to use the check the file contents. + * @throws ExpectationException if the file cannot be downloaded, or if the download does not pass all the checks. + */ + public function following_in_element_should_download_a_file_that(string $linktext, string $containerlocator, + string $containertype, TableNode $table): void { + + $filecontent = $this->download_file($linktext, $containerlocator, $containertype); + $this->verify_file_content($filecontent, $table); + } + + /** + * Download a file from the given link. + * + * @param string $linktext the text of the link. + * @param string $containerlocator the container element. + * @param string $containertype the container selector type. + * @return string the file contents. + * @throws ExpectationException if the download fails. + */ + protected function download_file(string $linktext, string $containerlocator, string $containertype): string { + return behat_context_helper::get('behat_general')->download_file_from_link($linktext, $containerlocator, $containertype); + } + + /** + * Checks the content of the downloaded file. + * + * @param string $filecontent the content of the file. + * @param TableNode $table the table of assertions to check. + * @throws ExpectationException if the file content does not pass all the checks. + */ + private function verify_file_content(string $filecontent, TableNode $table): void { + foreach ($table->getRows() as $row) { + switch (strtolower(trim($row[0]))) { + case 'contains text': + $this->verify_file_contains_text($filecontent, $row[1]); + break; + case 'contains text in xml element': + $this->verify_xml_element_contains($filecontent, $row[1]); + break; + case 'has mimetype': + $this->verify_file_mimetype($filecontent, $row[1]); + break; + case 'contains file in zip': + $this->verify_zip_file_content($filecontent, $row[1]); + break; + default: + throw new ExpectationException( + 'Invalid type of file assertion: ' . $row[0], $this->getSession()); + } + } + } + + /** + * Validates the downloaded file appears to be of the mimetype. + * + * @param string $filecontent the content of the file. + * @param string $expectedmimetype the expected file mimetype e.g. 'application/xml'. + * @throws ExpectationException if the file does not appear to be of the expected type. + */ + protected function verify_file_mimetype(string $filecontent, string $expectedmimetype): void { + + $finfo = new finfo(FILEINFO_MIME_TYPE); + $actualmimetype = $finfo->buffer($filecontent); + + if ($actualmimetype !== $expectedmimetype) { + throw new ExpectationException( + "The file downloaded should have been a $expectedmimetype file, " . + "but got $actualmimetype instead.", + $this->getSession(), + ); + } + } + + /** + * Asserts that the given string is present in the file content. + * + * @param string $filecontent the content of the file. + * @param string $expectedcontent the string to search for. + * @throws ExpectationException if verification fails. + */ + protected function verify_file_contains_text(string $filecontent, string $expectedcontent): void { + if (!str_contains($filecontent, $expectedcontent)) { + throw new ExpectationException( + "The string '$expectedcontent' was not found in the file content.", + $this->getSession(), + ); + } + } + + /** + * Asserts that the given XML file is valid and contains the expected string. + * + * @param string $filecontent the content of the file. + * @param string $expectedcontent the string to search for. + * @throws ExpectationException + */ + protected function verify_xml_element_contains(string $filecontent, string $expectedcontent): void { + $xml = new SimpleXMLElement($filecontent); + $result = $xml->xpath("//*[contains(text(), '$expectedcontent')]"); + + if (empty($result)) { + throw new ExpectationException( + "The string '$expectedcontent' was not found in the content of any element in this XML file.", + $this->getSession(), + ); + } + } + + /** + * Save the downloaded file to tempdir and return the path. + * + * @param string $filecontent the content of the file. + * @param string $fileextension the expected file type, given as a file extension, e.g. 'txt', 'xml'. + * @return string path where the file was saved temporarily. + */ + protected function save_to_temp_file(string $filecontent, string $fileextension): string { + // Then perform additional image-specific validations. + $tempdir = make_request_directory(); + $filepath = $tempdir . '/downloaded.' . $fileextension; + file_put_contents($filepath, $filecontent); + return $filepath; + } + + /** + * Asserts that the given zip archive contains the expected file(s). + * + * @param string $filecontent the content of the file. + * @param string $expectedfile the name of the file to search for. + * @throws ExpectationException if the zip file does not contain the expected files. + */ + protected function verify_zip_file_content(string $filecontent, string $expectedfile): void { + + $zip = new ZipArchive(); + $res = $zip->open($this->save_to_temp_file($filecontent, 'zip')); + + if ($res !== true) { + throw new ExpectationException( + "Failed to open zip file.", + $this->getSession(), + ); + } + + if ($zip->locateName($expectedfile) === false) { + throw new ExpectationException( + "The file '$expectedfile' was not found in the downloaded zip archive.", + $this->getSession(), + ); + } + } +} diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index e3900e009b1..5bb7a3daa7e 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1652,15 +1652,24 @@ EOF; /** * Given the text of a link, download the linked file and return the contents. * - * This is a helper method used by {@link following_should_download_bytes()} - * and {@link following_should_download_between_and_bytes()} + * A helper method used by the steps in {@see behat_download}, and the legacy + * {@see following_should_download_bytes()} and {@see following_should_download_between_and_bytes()}. * * @param string $link the text of the link. + * @param string $containerlocator optional container element locator. + * @param string $containertype optional container element selector type. + * * @return string the content of the downloaded file. */ - public function download_file_from_link($link) { + public function download_file_from_link(string $link, string $containerlocator = '', string $containertype = ''): string { + // Find the link. - $linknode = $this->find_link($link); + if ($containerlocator !== '' && $containertype !== '') { + $linknode = $this->get_node_in_container('link', $link, $containertype, $containerlocator); + } else { + $linknode = $this->find_link($link); + } + $this->ensure_node_is_visible($linknode); // Get the href and check it. @@ -1682,6 +1691,8 @@ EOF; /** * Downloads the file from a link on the page and checks the size. * + * Not recommended any more. The steps in {@see behat_download} are much better! + * * Only works if the link has an href attribute. Javascript downloads are * not supported. Currently, the href must be an absolute URL. * @@ -1715,6 +1726,8 @@ EOF; /** * Downloads the file from a link on the page and checks the size is in a given range. * + * Not recommended any more. The steps in {@see behat_download} are much better! + * * Only works if the link has an href attribute. Javascript downloads are * not supported. Currently, the href must be an absolute URL. * @@ -1722,10 +1735,11 @@ EOF; * be between "5" and "10" bytes, and between "10" and "20" bytes. * * @Then /^following "(?P[^"]*)" should download between "(?P\d+)" and "(?P\d+)" bytes$/ - * @throws ExpectationException + * * @param string $link the text of the link. * @param number $minexpectedsize the minimum expected file size in bytes. * @param number $maxexpectedsize the maximum expected file size in bytes. + * @throws ExpectationException */ public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) { // If the minimum is greater than the maximum then swap the values.