Skip to content

Commit

Permalink
MDL-63399 behat: implement a new download step
Browse files Browse the repository at this point in the history
  • Loading branch information
lameze committed May 29, 2024
1 parent d32844c commit 96d1741
Show file tree
Hide file tree
Showing 24 changed files with 303 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class behat_admin_presets extends behat_base {
*/
final public function following_in_the_should_download_between_and_bytes(string $link, string $selectortype,
string $nodeelement, string $nodeselectortype, int $minexpectedsize, int $maxexpectedsize): void {
// TO BE DEPRECATED.
// If the minimum is greater than the maximum then swap the values.
if ((int) $minexpectedsize > (int) $maxexpectedsize) {
list($minexpectedsize, $maxexpectedsize) = [$maxexpectedsize, $minexpectedsize];
Expand Down
8 changes: 4 additions & 4 deletions admin/tool/admin_presets/tests/behat/download.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Feature: I can download a preset
| name |
| Custom preset |

@javascript
Scenario: Custom preset settings can be downloaded
Given I log in as "admin"
And I navigate to "Site admin presets" in site administration
When I open the action menu in "Custom preset" "table_row"
Then following "Download" "link" in the "Custom preset" "table_row" should download between "0" and "5000" bytes
Then following "Download" in the "Custom preset" "table_row" should download a "xml" file that:
| Contains | Custom preset |

@javascript
Scenario: Core preset settings can be downloaded
Given I log in as "admin"
And I navigate to "Site admin presets" in site administration
When I open the action menu in "Starter" "table_row"
Then following "Download" "link" in the "Starter" "table_row" should download between "0" and "5000" bytes
Then following "Download" in the "Starter" "table_row" should download a "xml" file that:
| Contains | Starter |
18 changes: 12 additions & 6 deletions admin/tool/dataprivacy/tests/behat/dataexport.feature
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "200000" bytes
And following "Download" should download a "zip" file that:
| Contains | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
Expand Down Expand Up @@ -89,7 +90,8 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "200000" bytes
And following "Download" should download a "zip" file that:
| Contains | index.html |

And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
Expand Down Expand Up @@ -126,7 +128,8 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "200000" bytes
And following "Download" should download a "zip" file that:
| Contains | index.html |

And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
Expand Down Expand Up @@ -185,7 +188,8 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "170000" bytes
Then following "Download" should download a "zip" file that:
| Contains | index.html |
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
Expand Down Expand Up @@ -229,7 +233,8 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "180000" bytes
And following "Download" should download a "zip" file that:
| Contains | index.html |

@javascript
Scenario: Filter before export data for a user and download it in the view request action
Expand Down Expand Up @@ -262,4 +267,5 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "180000" bytes
And following "Download" should download a "zip" file that:
| Contains | index.html |
3 changes: 2 additions & 1 deletion analytics/tests/behat/manage_models.feature
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ Feature: Manage analytics models
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Export" in the open action menu
And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
And following "Export" should download between "100" and "500" bytes
And following "Download" should download a "zip" file that:
| Contains | model-config.json |

Scenario: Check invalid site elements
When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
Expand Down
224 changes: 224 additions & 0 deletions lib/tests/behat/behat_download.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?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/>.

/**
* Steps definitions to verify a downloaded file.
*
* @package core
* @category test
* @copyright 2024 Simey Lameze <[email protected]>
* @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 <[email protected]>
* @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 checks the size is in a given range.
*
* @Then /^following "(?P<link_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should download a "(?P<format_string>[^"]*)" file that:$/
*
* @param string $link the text of the link.
* @param string $elementcontainer the container element.
* @param string $textselector the text selector.
* @param string $format the expected file format.
* @param TableNode|null $table the table of assertions to check.
* @throws ExpectationException
*/
public function following_in_element_should_download_a_file_that(string $link, string $elementcontainer,
string $textselector, string $format, ?TableNode $table = null): void {

$this->following_should_download_a_file_that($link, $format, $elementcontainer, $textselector, $table);
}

/**
* Downloads the file from a link on the page and checks the size is in a given range.
*
* @Then /^following "(?P<link_string>[^"]*)" should download a "(?P<format_string>[^"]*)" file$/
* @Then /^following "(?P<link_string>[^"]*)" should download a "(?P<format_string>[^"]*)" file that:$/
*
* @param string $link the text of the link.
* @param string $format the expected file format.
* @param string|null $nodeelement the element to look in.
* @param string|null $nodeselectortype the type of selector to look in.
* @param TableNode|null $table the table of assertions to check.
* @throws ExpectationException
*/
public function following_should_download_a_file_that(string $link, string $format, ?string $nodeelement = null,
?string $nodeselectortype = null, ?TableNode $table = null): void {

$behatgeneralcontext = behat_context_helper::get('behat_general');
$exception = new ExpectationException(
"Error while downloading data from {$link}",
$this->getSession(),
);

$format = strtolower($format);
$linkparams = empty($nodeelement) && empty($nodeselectortype)
? [$link]
: [$link, $nodeselectortype, $nodeelement];

$filecontent = $this->spin(
fn($context, $link) => $behatgeneralcontext->download_file_from_link(...$linkparams),
$link,
behat_base::get_extended_timeout(),
$exception
);

// Images don't need to be checked for content.
if (in_array($format, ['png', 'jpg', 'gif'])) {
$this->assert_file_type_image($filecontent, $format);
return;
}

$this->validate_mime_type($filecontent, $format);
$this->assert_file_content($filecontent, $format, $table);
}
private function assert_file_content(string $filecontent, string $format, ?TableNode $table): void {
if (!$rows = $table->getRows()) {
return;
}

// Determine the assertion method to use based on the file format.
// If the format is 'gift' or 'aiken', use the text assertion method.
$assertmethod = ($format === 'gift' || $format === 'aiken') ? "assert_file_type_text" : "assert_file_type_{$format}";

foreach ($rows as $row) {
$assertion = strtolower(trim($row[0]));
match ($assertion) {
'contains' => $this->$assertmethod($filecontent, $row[1]),
default => throw new ExpectationException(
"Invalid assertion: {$assertion}",
$this->getSession(),
)
};
}
}

protected function validate_mime_type(string $filecontent, string $format): void {

$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimetype = $finfo->buffer($filecontent);

$validmimetypes = match ($format) {
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'xml' => ['application/xml', 'text/xml'],
'text' => ['text/plain'],
'gift' => ['text/plain'],
'aiken' => ['text/plain'],
'png' => ['image/png'],
default => [$format], // If the format is not one of the above, assume it is the only valid MIME type
};

if (!in_array($mimetype, $validmimetypes)) {
throw new ExpectationException(
"The file downloaded should have been a {$format} file, but got {$mimetype} instead.",
$this->getSession(),
);
}
}

protected function assert_file_type_xml(string $filecontent, string $contains): void {

// Load the XML content into a SimpleXMLElement object
$xml = new SimpleXMLElement($filecontent);

// Use xpath to search for the string in the XML content
$result = $xml->xpath("//*[contains(text(), '$contains')]");

// If the result is empty, the string was not found in the XML content
if (empty($result)) {
throw new ExpectationException(
"The string '{$contains}' was not found in the XML content.",
$this->getSession(),
);
}
}

protected function assert_file_type_image(string $filecontent, string $format): void {
// First validate the MIME type
$this->validate_mime_type($filecontent, $format);

// Then perform additional image-specific validations
$tempdir = make_request_directory();
$filepath = $tempdir . '/downloaded.' . $format;
file_put_contents($filepath, $filecontent);

if (!getimagesize($filepath)) {
throw new ExpectationException(
"The file downloaded does not appear to be a valid {$format} image.",
$this->getSession(),
);
}
}

protected function assert_file_type_zip(string $filecontent, string $expectedfile): void {

// Save the file to disk.
$tempdir = make_request_directory();
$filepath = $tempdir . '/downloaded.zip';
file_put_contents($filepath, $filecontent);

$zip = new ZipArchive();
$res = $zip->open($filepath);

if ($res !== true) {
throw new ExpectationException(
"Failed to open zip file.",
$this->getSession(),
);
}

// Check if the expected file exists in the zip archive.
if ($zip->locateName($expectedfile) === false) {
throw new ExpectationException(
"The file '{$expectedfile}' was not found in the zip archive.",
$this->getSession(),
);
}
}

protected function assert_file_type_text(string $filecontent, string $contains): void {

// Check if the string is present in the file content.
if (!str_contains($filecontent, $contains)) {
throw new ExpectationException(
"The string '{$contains}' was not found in the file content.",
$this->getSession(),
);
}
}

protected function assert_file_type_gift(string $filecontent, string $contains): void {
$this->assert_file_type_text($filecontent, $contains);
}

protected function assert_file_type_aiken(string $filecontent, string $contains): void {
$this->assert_file_type_text($filecontent, $contains);
}
}
16 changes: 14 additions & 2 deletions lib/tests/behat/behat_general.php
Original file line number Diff line number Diff line change
Expand Up @@ -1569,15 +1569,25 @@ public function following_should_not_exist_in_the_table($table, TableNode $data)
* and {@link following_should_download_between_and_bytes()}
*
* @param string $link the text of the link.
* @param string $nodeselectortype the type of the node where the link is.
* @param string $nodeelement the element of the node where the link is.
*
* @return string the content of the downloaded file.
*/
public function download_file_from_link($link) {
public function download_file_from_link(string $link, string $nodeselectortype = '', string $nodeelement = '') {

// Find the link.
$linknode = $this->find_link($link);
if (!empty($nodeselectortype) && !empty($nodeelement)) {
$linknode = $this->get_node_in_container('link', $link, $nodeselectortype, $nodeelement);
} else {
$linknode = $this->find_link($link);
}

$this->ensure_node_is_visible($linknode);

// Get the href and check it.
$url = $linknode->getAttribute('href');

if (!$url) {
throw new ExpectationException('Download link does not have href attribute',
$this->getSession());
Expand Down Expand Up @@ -1641,6 +1651,8 @@ function($context, $args) {
* @param number $maxexpectedsize the maximum expected file size in bytes.
*/
public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
// TO BE DEPRECATED

// If the minimum is greater than the maximum then swap the values.
if ((int)$minexpectedsize > (int)$maxexpectedsize) {
list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
Expand Down
8 changes: 6 additions & 2 deletions mod/data/tests/behat/data_presets.feature
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,14 @@ Feature: Users can view and manage data presets
# The teacher should be able to export any saved preset.
And I open the action menu in "Saved preset by teacher1" "table_row"
Then I should see "Export"
And following "Export" "link" in the "Saved preset by teacher1" "table_row" should download between "1" and "5000" bytes
# Failing.
Then following "Export" should download a "xml" file that:
| Contains | Saved preset by teacher1 |
And I open the action menu in "Saved preset 1" "table_row"
And I should see "Export"
And following "Export" "link" in the "Saved preset 1" "table_row" should download between "1" and "5000" bytes
# Failing.
And following "Export" should download a "xml" file that:
| Contains | Saved preset 1 |

@javascript @_file_upload
Scenario Outline: Admins and Teachers can load a preset from a file
Expand Down
5 changes: 3 additions & 2 deletions mod/folder/tests/behat/recent_activity.feature
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ Feature: Files added in folder activity are visible in the recent activity block
And "//img[@alt='empty.txt']" "xpath_element" should exist
And "//img[contains(@src, 'preview=tinyicon')]" "xpath_element" should exist
# Confirm files are downloadable
And following "empty.txt" should download between "1" and "3000" bytes
And following "gd-logo.png" should download between "1" and "3000" bytes
Then following "empty.txt" should download a "text" file that:
| Contains | empty file for testing purposes |
And following "gd-logo.png" should download a "png" file
3 changes: 2 additions & 1 deletion mod/glossary/tests/behat/create_entry.feature
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ Feature: Create a glossary entry.
When I am on the "Test glossary" "glossary activity" page logged in as teacher1
Then I should see "Entry 1"
And I should see "musicians.xml"
And following "musicians.xml" should download between "1" and "3000" bytes
And following "musicians.xml" should download a "xml" file that:
| Contains | Paul McCartney |
Loading

0 comments on commit 96d1741

Please sign in to comment.