Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace quizaccess_seb;
  18  
  19  use quizaccess_seb;
  20  
  21  defined('MOODLE_INTERNAL') || die();
  22  
  23  require_once (__DIR__ . '/test_helper_trait.php');
  24  
  25  /**
  26   * PHPUnit tests for plugin rule class.
  27   *
  28   * @package    quizaccess_seb
  29   * @author     Andrew Madden <andrewmadden@catalyst-au.net>
  30   * @copyright  2020 Catalyst IT
  31   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   * @covers \quizaccess_seb
  33   */
  34  class rule_test extends \advanced_testcase {
  35      use \quizaccess_seb_test_helper_trait;
  36  
  37      /**
  38       * Called before every test.
  39       */
  40      public function setUp(): void {
  41          parent::setUp();
  42  
  43          $this->resetAfterTest();
  44          $this->course = $this->getDataGenerator()->create_course();
  45      }
  46  
  47      /**
  48       * Called after every test.
  49       */
  50      public function tearDown(): void {
  51          global $SESSION;
  52  
  53          if (!empty($this->quiz)) {
  54              unset($SESSION->quizaccess_seb_access);
  55          }
  56      }
  57  
  58      /**
  59       * Helper method to get SEB download link for testing.
  60       *
  61       * @return string
  62       */
  63      private function get_seb_download_link() {
  64          return 'https://safeexambrowser.org/download_en.html';
  65      }
  66  
  67      /**
  68       * Helper method to get SEB launch link for testing.
  69       *
  70       * @return string
  71       */
  72      private function get_seb_launch_link() {
  73          return 'sebs://www.example.com/moodle/mod/quiz/accessrule/seb/config.php';
  74      }
  75  
  76      /**
  77       * Helper method to get SEB config download link for testing.
  78       *
  79       * @return string
  80       */
  81      private function get_seb_config_download_link() {
  82          return 'https://www.example.com/moodle/mod/quiz/accessrule/seb/config.php';
  83      }
  84  
  85      /**
  86       * Provider to return valid form field data when saving settings.
  87       *
  88       * @return array
  89       */
  90      public function valid_form_data_provider() : array {
  91          return [
  92              'valid seb_requiresafeexambrowser' => ['seb_requiresafeexambrowser', '0'],
  93              'valid seb_linkquitseb0' => ['seb_linkquitseb', 'http://safeexambrowser.org/macosx'],
  94              'valid seb_linkquitseb1' => ['seb_linkquitseb', 'safeexambrowser.org/macosx'],
  95              'valid seb_linkquitseb2' => ['seb_linkquitseb', 'www.safeexambrowser.org/macosx'],
  96              'valid seb_linkquitseb3' => ['seb_linkquitseb', 'any.type.of.url.looking.thing'],
  97              'valid seb_linkquitseb4' => ['seb_linkquitseb', 'http://any.type.of.url.looking.thing'],
  98          ];
  99      }
 100  
 101      /**
 102       * Provider to return invalid form field data when saving settings.
 103       *
 104       * @return array
 105       */
 106      public function invalid_form_data_provider() : array {
 107          return [
 108              'invalid seb_requiresafeexambrowser' => ['seb_requiresafeexambrowser', 'Uh oh!'],
 109              'invalid seb_linkquitseb0' => ['seb_linkquitseb', '\0'],
 110              'invalid seb_linkquitseb1' => ['seb_linkquitseb', 'invalid url'],
 111              'invalid seb_linkquitseb2' => ['seb_linkquitseb', 'http]://safeexambrowser.org/macosx'],
 112              'invalid seb_linkquitseb3' => ['seb_linkquitseb', '0'],
 113              'invalid seb_linkquitseb4' => ['seb_linkquitseb', 'seb://any.type.of.url.looking.thing'],
 114          ];
 115      }
 116  
 117      /**
 118       * Test no errors are found with valid data.
 119       *
 120       * @param string $setting
 121       * @param string $data
 122       *
 123       * @dataProvider valid_form_data_provider
 124       */
 125      public function test_validate_settings_with_valid_data(string $setting, string $data) {
 126          $this->setAdminUser();
 127          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 128  
 129          $form = $this->createMock('mod_quiz_mod_form');
 130          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 131  
 132          // Validate settings with a dummy form.
 133          $errors = quizaccess_seb::validate_settings_form_fields([], [
 134              'instance' => $this->quiz->id,
 135              'coursemodule' => $this->quiz->cmid,
 136              $setting => $data
 137          ], [], $form);
 138          $this->assertEmpty($errors);
 139      }
 140  
 141      /**
 142       * Test errors are found with invalid data.
 143       *
 144       * @param string $setting
 145       * @param string $data
 146       *
 147       * @dataProvider invalid_form_data_provider
 148       */
 149      public function test_validate_settings_with_invalid_data(string $setting, string $data) {
 150          $this->setAdminUser();
 151  
 152          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 153          $form = $this->createMock('mod_quiz_mod_form');
 154          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 155  
 156          // Validate settings with a dummy form and quiz instance.
 157          $errors = quizaccess_seb::validate_settings_form_fields([], [
 158              'instance' => $this->quiz->id,
 159              'coursemodule' => $this->quiz->cmid,
 160              $setting => $data
 161          ], [], $form);
 162          $this->assertEquals([$setting => 'Data submitted is invalid'], $errors);
 163      }
 164  
 165      /**
 166       * Test settings validation is not run if settings are locked.
 167       */
 168      public function test_settings_validation_is_not_run_if_settings_are_locked() {
 169          $user = $this->getDataGenerator()->create_user();
 170          $this->quiz = $this->create_test_quiz($this->course);
 171          $this->attempt_quiz($this->quiz, $user);
 172  
 173          $this->setAdminUser();
 174  
 175          $form = $this->createMock('mod_quiz_mod_form');
 176          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 177  
 178          // Validate settings with a dummy form and quiz instance.
 179          $errors = quizaccess_seb::validate_settings_form_fields([], [
 180              'instance' => $this->quiz->id,
 181              'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => 'Uh oh!'
 182          ], [], $form);
 183          $this->assertEmpty($errors);
 184      }
 185  
 186      /**
 187       * Test settings validation is not run if settings are conflicting.
 188       */
 189      public function test_settings_validation_is_not_run_if_conflicting_permissions() {
 190          $this->setAdminUser();
 191          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 192  
 193          $form = $this->createMock('mod_quiz_mod_form');
 194          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 195  
 196          $user = $this->getDataGenerator()->create_user();
 197          $roleid = $this->getDataGenerator()->create_role();
 198          $context = \context_module::instance($this->quiz->cmid);
 199          assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $roleid, $context->id);
 200          $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id);
 201  
 202          // By default The user won't have permissions to configure manually.
 203          $this->setUser($user);
 204  
 205          // Validate settings with a dummy form and quiz instance.
 206          $errors = quizaccess_seb::validate_settings_form_fields([], [
 207              'instance' => $this->quiz->id,
 208              'coursemodule' => $this->quiz->cmid,
 209              'seb_requiresafeexambrowser' => 'Uh oh!'
 210          ], [], $form);
 211          $this->assertEmpty($errors);
 212      }
 213  
 214      /**
 215       * Test bypassing validation if user don't have permissions to manage seb settings.
 216       */
 217      public function test_validate_settings_is_not_run_if_a_user_do_not_have_permissions_to_manage_seb_settings() {
 218          // Set the user who can't change seb settings. So validation should be bypassed.
 219          $user = $this->getDataGenerator()->create_user();
 220          $this->setUser($user);
 221  
 222          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 223          $form = $this->createMock('mod_quiz_mod_form');
 224          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 225  
 226          // Validate settings with a dummy form and quiz instance.
 227          $errors = quizaccess_seb::validate_settings_form_fields([], [
 228              'instance' => $this->quiz->id,
 229              'coursemodule' => $this->quiz->cmid, 'seb_requiresafeexambrowser' => 'Uh oh!'
 230          ], [], $form);
 231          $this->assertEmpty($errors);
 232      }
 233  
 234      /**
 235       * Test settings are saved to DB.
 236       */
 237      public function test_create_settings_with_existing_quiz() {
 238          global $DB;
 239  
 240          $this->setAdminUser();
 241  
 242          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
 243          $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id]));
 244  
 245          $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_CONFIG_MANUALLY;
 246          quizaccess_seb::save_settings($this->quiz);
 247          $this->assertNotFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id]));
 248      }
 249  
 250      /**
 251       * Test settings are not saved to DB if settings are locked.
 252       */
 253      public function test_settings_are_not_saved_if_settings_are_locked() {
 254          global $DB;
 255  
 256          $this->setAdminUser();
 257          $this->quiz = $this->create_test_quiz($this->course);
 258  
 259          $user = $this->getDataGenerator()->create_user();
 260          $this->setUser($user);
 261          $this->attempt_quiz($this->quiz, $user);
 262  
 263          $this->setAdminUser();
 264          $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_CONFIG_MANUALLY;
 265          quizaccess_seb::save_settings($this->quiz);
 266          $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id]));
 267      }
 268  
 269      /**
 270       * Test settings are not saved to DB if conflicting permissions.
 271       */
 272      public function test_settings_are_not_saved_if_conflicting_permissions() {
 273          $this->setAdminUser();
 274          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 275  
 276          $user = $this->getDataGenerator()->create_user();
 277          $roleid = $this->getDataGenerator()->create_role();
 278          $context = \context_module::instance($this->quiz->cmid);
 279          assign_capability('quizaccess/seb:manage_seb_requiresafeexambrowser', CAP_ALLOW, $roleid, $context->id);
 280          $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id);
 281  
 282          // By default The user won't have permissions to configure manually.
 283          $this->setUser($user);
 284  
 285          $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_NO;
 286          quizaccess_seb::save_settings($this->quiz);
 287  
 288          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 289          $this->assertEquals(settings_provider::USE_SEB_CONFIG_MANUALLY, $quizsettings->get('requiresafeexambrowser'));
 290      }
 291  
 292      /**
 293       * Test exception thrown if cm could not be found while saving settings.
 294       */
 295      public function test_save_settings_throw_an_exception_if_cm_not_found() {
 296          global $DB;
 297  
 298          $this->expectException(\dml_missing_record_exception::class);
 299          $this->expectExceptionMessage('Can\'t find data record in database.');
 300  
 301          $this->setAdminUser();
 302  
 303          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 304          $DB->delete_records('quiz', ['id' => $this->quiz->id]);
 305          $this->quiz->seb_requiresafeexambrowser = settings_provider::USE_SEB_NO;
 306          quizaccess_seb::save_settings($this->quiz);
 307      }
 308  
 309      /**
 310       * Test nothing happens when deleted is called without settings saved.
 311       */
 312      public function test_delete_settings_without_existing_settings() {
 313          global $DB;
 314          $this->setAdminUser();
 315  
 316          $quiz = new \stdClass();
 317          $quiz->id = 1;
 318          quizaccess_seb::delete_settings($quiz);
 319          $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $quiz->id]));
 320      }
 321  
 322      /**
 323       * Test settings are deleted from DB.
 324       */
 325      public function test_delete_settings_with_existing_settings() {
 326          global $DB;
 327          $this->setAdminUser();
 328  
 329          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 330  
 331          // Using a generator will create the quiz_settings record.
 332          $this->assertNotFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id]));
 333          quizaccess_seb::delete_settings($this->quiz);
 334          $this->assertFalse($DB->record_exists('quizaccess_seb_quizsettings', ['quizid' => $this->quiz->id]));
 335      }
 336  
 337      /**
 338       * A helper method to check invalid config key.
 339       */
 340      protected function check_invalid_config_key() {
 341          // Create an event sink, trigger event and retrieve event.
 342          $sink = $this->redirectEvents();
 343  
 344          // Check that correct error message is returned.
 345          $errormsg = $this->make_rule()->prevent_access();
 346          $this->assertNotEmpty($errormsg);
 347          $this->assertStringContainsString("The Safe Exam Browser keys could not be validated. "
 348              . "Check that you're using Safe Exam Browser with the correct configuration file.", $errormsg);
 349          $this->assertStringContainsString($this->get_seb_download_link(), $errormsg);
 350          $this->assertStringContainsString($this->get_seb_launch_link(), $errormsg);
 351          $this->assertStringContainsString($this->get_seb_config_download_link(), $errormsg);
 352  
 353          $events = $sink->get_events();
 354          $this->assertEquals(1, count($events));
 355          $event = reset($events);
 356  
 357          // Test that the event data is as expected.
 358          $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
 359          $this->assertEquals('Invalid SEB config key', $event->other['reason']);
 360      }
 361  
 362      /**
 363       * Test access prevented if config key is invalid.
 364       */
 365      public function test_access_prevented_if_config_key_invalid() {
 366          global $FULLME;
 367  
 368          $this->setAdminUser();
 369          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 370  
 371          // Set up dummy request.
 372          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 373          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key';
 374  
 375          $user = $this->getDataGenerator()->create_user();
 376          $this->setUser($user);
 377  
 378          $this->check_invalid_config_key();
 379      }
 380  
 381      /**
 382       * Test access prevented if config keys is invalid and using uploaded config.
 383       */
 384      public function test_access_prevented_if_config_key_invalid_uploaded_config() {
 385          global $FULLME;
 386  
 387          $this->setAdminUser();
 388          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 389  
 390          // Set quiz setting to require seb and save BEK.
 391          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 392          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
 393          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 394          $this->create_module_test_file($xml, $this->quiz->cmid);
 395          $quizsettings->save();
 396  
 397          // Set up dummy request.
 398          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 399          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key';
 400  
 401          $user = $this->getDataGenerator()->create_user();
 402          $this->setUser($user);
 403  
 404          $this->check_invalid_config_key();
 405      }
 406  
 407      /**
 408       * Test access prevented if config keys is invalid and using template.
 409       */
 410      public function test_access_prevented_if_config_key_invalid_uploaded_template() {
 411          global $FULLME;
 412  
 413          $this->setAdminUser();
 414          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 415  
 416          // Set quiz setting to require seb and save BEK.
 417          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 418          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
 419          $quizsettings->set('templateid', $this->create_template()->get('id'));
 420          $quizsettings->save();
 421  
 422          // Set up dummy request.
 423          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 424          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = 'Broken config key';
 425  
 426          $user = $this->getDataGenerator()->create_user();
 427          $this->setUser($user);
 428  
 429          $this->check_invalid_config_key();
 430      }
 431  
 432      /**
 433       * Test access not prevented if config key matches header.
 434       */
 435      public function test_access_allowed_if_config_key_valid() {
 436          global $FULLME;
 437  
 438          $this->setAdminUser();
 439          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 440  
 441          $user = $this->getDataGenerator()->create_user();
 442          $this->setUser($user);
 443  
 444          // Set quiz setting to require seb.
 445          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 446  
 447          // Set up dummy request.
 448          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 449          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 450          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 451  
 452          // Check that correct error message is returned.
 453          $this->assertFalse($this->make_rule()->prevent_access());
 454      }
 455  
 456      /**
 457       * Test access not prevented if config key matches header.
 458       */
 459      public function test_access_allowed_if_config_key_valid_uploaded_config() {
 460          global $FULLME;
 461  
 462          $this->setAdminUser();
 463          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 464  
 465          // Set quiz setting to require seb and save BEK.
 466          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 467          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
 468          $quizsettings->set('templateid', $this->create_template()->get('id'));
 469          $quizsettings->save();
 470  
 471          $user = $this->getDataGenerator()->create_user();
 472          $this->setUser($user);
 473  
 474          // Set up dummy request.
 475          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 476          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 477          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 478  
 479          // Check that correct error message is returned.
 480          $this->assertFalse($this->make_rule()->prevent_access());
 481      }
 482  
 483      /**
 484       * Test access not prevented if config key matches header.
 485       */
 486      public function test_access_allowed_if_config_key_valid_template() {
 487          global $FULLME;
 488  
 489          $this->setAdminUser();
 490          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 491  
 492          // Set quiz setting to require seb and save BEK.
 493          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 494          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
 495          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 496          $this->create_module_test_file($xml, $this->quiz->cmid);
 497          $quizsettings->save();
 498  
 499          $user = $this->getDataGenerator()->create_user();
 500          $this->setUser($user);
 501  
 502          // Set up dummy request.
 503          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 504          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 505          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 506  
 507          // Check that correct error message is returned.
 508          $this->assertFalse($this->make_rule()->prevent_access());
 509      }
 510  
 511      /**
 512       * Test access not prevented if browser exam keys match headers.
 513       */
 514      public function test_access_allowed_if_browser_exam_keys_valid() {
 515          global $FULLME;
 516  
 517          $this->setAdminUser();
 518          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 519  
 520          $user = $this->getDataGenerator()->create_user();
 521          $this->setUser($user);
 522  
 523          // Set quiz setting to require seb and save BEK.
 524          $browserexamkey = hash('sha256', 'testkey');
 525          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 526          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key.
 527          $quizsettings->set('allowedbrowserexamkeys', $browserexamkey);
 528          $quizsettings->save();
 529  
 530          // Set up dummy request.
 531          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 532          $expectedhash = hash('sha256', $FULLME . $browserexamkey);
 533          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedhash;
 534          $_SERVER['HTTP_USER_AGENT'] = 'SEB';
 535  
 536          // Check that correct error message is returned.
 537          $this->assertFalse($this->make_rule()->prevent_access());
 538      }
 539  
 540      /**
 541       * Test access not prevented if browser exam keys match headers.
 542       */
 543      public function test_access_allowed_if_browser_exam_keys_valid_use_uploaded_file() {
 544          global $FULLME;
 545  
 546          $this->setAdminUser();
 547          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 548  
 549          // Set quiz setting to require seb and save BEK.
 550          $browserexamkey = hash('sha256', 'testkey');
 551          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 552          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
 553          $quizsettings->set('allowedbrowserexamkeys', $browserexamkey);
 554          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 555          $this->create_module_test_file($xml, $this->quiz->cmid);
 556          $quizsettings->save();
 557  
 558          // Set up dummy request.
 559          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 560          $expectedbrowserkey = hash('sha256', $FULLME . $browserexamkey);
 561          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = $expectedbrowserkey;
 562          $expectedconfigkey = hash('sha256', $FULLME . $quizsettings->get_config_key());
 563          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedconfigkey;
 564  
 565          $user = $this->getDataGenerator()->create_user();
 566          $this->setUser($user);
 567  
 568          // Check that correct error message is returned.
 569          $this->assertFalse($this->make_rule()->prevent_access());
 570      }
 571  
 572      public function test_access_allowed_if_access_state_stored_in_session() {
 573          global $SESSION;
 574  
 575          $this->setAdminUser();
 576          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
 577  
 578          $user = $this->getDataGenerator()->create_user();
 579          $this->setUser($user);
 580  
 581          // Check that access is prevented.
 582          $this->check_invalid_basic_header();
 583  
 584          $SESSION->quizaccess_seb_access = [$this->quiz->cmid => true];
 585  
 586          // Check access is now not prevented.
 587          $this->assertFalse($this->make_rule()->prevent_access());
 588      }
 589  
 590      /**
 591       * A helper method to check invalid browser key.
 592       *
 593       * @param bool $downloadseblink Make sure download SEB link is present.
 594       * @param bool $launchlink Make sure launch SEB link is present.
 595       * @param bool $downloadconfiglink Make download config link is present.
 596       */
 597      protected function check_invalid_browser_exam_key($downloadseblink = true, $launchlink = true, $downloadconfiglink = true) {
 598          // Create an event sink, trigger event and retrieve event.
 599          $sink = $this->redirectEvents();
 600  
 601          // Check that correct error message is returned.
 602          $errormsg = $this->make_rule()->prevent_access();
 603          $this->assertNotEmpty($errormsg);
 604          $this->assertStringContainsString("The Safe Exam Browser keys could not be validated. "
 605              . "Check that you're using Safe Exam Browser with the correct configuration file.", $errormsg);
 606  
 607          if ($downloadseblink) {
 608              $this->assertStringContainsString($this->get_seb_download_link(), $errormsg);
 609          } else {
 610              $this->assertStringNotContainsString($this->get_seb_download_link(), $errormsg);
 611          }
 612  
 613          if ($launchlink) {
 614              $this->assertStringContainsString($this->get_seb_launch_link(), $errormsg);
 615          } else {
 616              $this->assertStringNotContainsString($this->get_seb_launch_link(), $errormsg);
 617          }
 618  
 619          if ($downloadconfiglink) {
 620              $this->assertStringContainsString($this->get_seb_config_download_link(), $errormsg);
 621          } else {
 622              $this->assertStringNotContainsString($this->get_seb_config_download_link(), $errormsg);
 623          }
 624  
 625          $events = $sink->get_events();
 626          $this->assertEquals(1, count($events));
 627          $event = reset($events);
 628  
 629          // Test that the event data is as expected.
 630          $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
 631          $this->assertEquals('Invalid SEB browser key', $event->other['reason']);
 632      }
 633  
 634      /**
 635       * Test access prevented if browser exam keys do not match headers.
 636       */
 637      public function test_access_prevented_if_browser_exam_keys_are_invalid() {
 638          $this->setAdminUser();
 639          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 640  
 641          $user = $this->getDataGenerator()->create_user();
 642          $this->setUser($user);
 643  
 644          // Set quiz setting to require seb and save BEK.
 645          $browserexamkey = hash('sha256', 'testkey');
 646          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 647          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key.
 648          $quizsettings->set('allowedbrowserexamkeys', $browserexamkey);
 649          $quizsettings->save();
 650  
 651          // Set up dummy request.
 652          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key';
 653          $_SERVER['HTTP_USER_AGENT'] = 'SEB';
 654  
 655          $this->check_invalid_browser_exam_key(true, false, false);
 656      }
 657  
 658      /**
 659       * Test access prevented if browser exam keys do not match headers and using uploaded config.
 660       */
 661      public function test_access_prevented_if_browser_exam_keys_are_invalid_use_uploaded_file() {
 662          global $FULLME;
 663  
 664          $this->setAdminUser();
 665          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 666  
 667          // Set quiz setting to require seb and save BEK.
 668          $browserexamkey = hash('sha256', 'testkey');
 669          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 670          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
 671          $quizsettings->set('allowedbrowserexamkeys', $browserexamkey);
 672          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 673          $this->create_module_test_file($xml, $this->quiz->cmid);
 674          $quizsettings->save();
 675  
 676          // Set up dummy request.
 677          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 678          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 679          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 680  
 681          // Set  up broken browser key.
 682          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key';
 683  
 684          $user = $this->getDataGenerator()->create_user();
 685          $this->setUser($user);
 686  
 687          $this->check_invalid_browser_exam_key();
 688      }
 689  
 690      /**
 691       * Test access not prevented if browser exam keys do not match headers and using template.
 692       */
 693      public function test_access_prevented_if_browser_exam_keys_are_invalid_use_template() {
 694          global $FULLME;
 695  
 696          $this->setAdminUser();
 697          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 698  
 699          // Set quiz setting to require seb and save BEK.
 700          $browserexamkey = hash('sha256', 'testkey');
 701          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 702          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
 703          $quizsettings->set('allowedbrowserexamkeys', $browserexamkey);
 704          $quizsettings->set('templateid', $this->create_template()->get('id'));
 705          $quizsettings->save();
 706  
 707          // Set up dummy request.
 708          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 709          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 710          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 711  
 712          // Set  up broken browser key.
 713          $_SERVER['HTTP_X_SAFEEXAMBROWSER_REQUESTHASH'] = 'Broken browser key';
 714  
 715          $user = $this->getDataGenerator()->create_user();
 716          $this->setUser($user);
 717  
 718          // Check that correct error message is returned.
 719          $this->assertFalse($this->make_rule()->prevent_access());
 720      }
 721  
 722      /**
 723       * Test access allowed if using client configuration and SEB user agent header is valid.
 724       */
 725      public function test_access_allowed_if_using_client_config_basic_header_is_valid() {
 726          $this->setAdminUser();
 727          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 728  
 729          $user = $this->getDataGenerator()->create_user();
 730          $this->setUser($user);
 731  
 732          // Set quiz setting to require seb.
 733          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 734          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key.
 735          $quizsettings->save();
 736  
 737          // Set up basic dummy request.
 738          $_SERVER['HTTP_USER_AGENT'] = 'SEB_TEST_SITE';
 739  
 740          // Check that correct error message is returned.
 741          $this->assertFalse($this->make_rule()->prevent_access());
 742      }
 743  
 744      /**
 745       * Test access prevented if using client configuration and SEB user agent header is invalid.
 746       */
 747      public function test_access_prevented_if_using_client_configuration_and_basic_head_is_invalid() {
 748          $this->setAdminUser();
 749          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 750  
 751          $user = $this->getDataGenerator()->create_user();
 752          $this->setUser($user);
 753  
 754          // Set quiz setting to require seb.
 755          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 756          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG); // Doesn't check config key.
 757          $quizsettings->save();
 758  
 759          // Set up basic dummy request.
 760          $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE';
 761  
 762          // Create an event sink, trigger event and retrieve event.
 763          $this->check_invalid_basic_header();
 764      }
 765  
 766      /**
 767       * A helper method to check invalid basic header.
 768       */
 769      protected function check_invalid_basic_header() {
 770          // Create an event sink, trigger event and retrieve event.
 771          $sink = $this->redirectEvents();
 772  
 773          // Check that correct error message is returned.
 774          $this->assertStringContainsString(
 775              'This quiz has been configured to use the Safe Exam Browser with client configuration.',
 776              $this->make_rule()->prevent_access()
 777          );
 778  
 779          $events = $sink->get_events();
 780          $this->assertEquals(1, count($events));
 781          $event = reset($events);
 782  
 783          // Test that the event data is as expected.
 784          $this->assertInstanceOf('\quizaccess_seb\event\access_prevented', $event);
 785          $this->assertEquals('No Safe Exam Browser is being used.', $event->other['reason']);
 786      }
 787  
 788      /**
 789       * Test access allowed if using client configuration and SEB user agent header is invalid and use uploaded file.
 790       */
 791      public function test_access_allowed_if_using_client_configuration_and_basic_head_is_invalid_use_uploaded_config() {
 792          global $FULLME;
 793  
 794          $this->setAdminUser();
 795          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 796  
 797          // Set quiz setting to require seb.
 798          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 799          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG); // Doesn't check basic header.
 800          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
 801          $this->create_module_test_file($xml, $this->quiz->cmid);
 802          $quizsettings->save();
 803  
 804          // Set up dummy request.
 805          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 806          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 807          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 808          $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE';
 809  
 810          $user = $this->getDataGenerator()->create_user();
 811          $this->setUser($user);
 812  
 813          // Check that correct error message is returned.
 814          $this->assertFalse($this->make_rule()->prevent_access());
 815      }
 816  
 817      /**
 818       * Test access allowed if using client configuration and SEB user agent header is invalid and use template.
 819       */
 820      public function test_access_allowed_if_using_client_configuration_and_basic_head_is_invalid_use_template() {
 821          global $FULLME;
 822  
 823          $this->setAdminUser();
 824          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 825  
 826          // Set quiz setting to require seb.
 827          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 828          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
 829          $quizsettings->set('templateid', $this->create_template()->get('id'));
 830          $quizsettings->save();
 831  
 832          // Set up dummy request.
 833          $FULLME = 'https://example.com/moodle/mod/quiz/attempt.php?attemptid=123&page=4';
 834          $expectedhash = hash('sha256', $FULLME . $quizsettings->get_config_key());
 835          $_SERVER['HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH'] = $expectedhash;
 836          $_SERVER['HTTP_USER_AGENT'] = 'WRONG_TEST_SITE';
 837  
 838          $user = $this->getDataGenerator()->create_user();
 839          $this->setUser($user);
 840  
 841          // Check that correct error message is returned.
 842          $this->assertFalse($this->make_rule()->prevent_access());
 843      }
 844  
 845      /**
 846       * Test access not prevented if SEB not required.
 847       */
 848      public function test_access_allowed_if_seb_not_required() {
 849          $this->setAdminUser();
 850          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 851  
 852          $user = $this->getDataGenerator()->create_user();
 853          $this->setUser($user);
 854  
 855          // Set quiz setting to not require seb.
 856          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
 857          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_NO);
 858          $quizsettings->save();
 859  
 860          // The rule will not exist as the settings are not configured for SEB usage.
 861          $this->assertNull($this->make_rule());
 862      }
 863  
 864      /**
 865       * Test access not prevented if USER has bypass capability.
 866       */
 867      public function test_access_allowed_if_user_has_bypass_capability() {
 868          $this->setAdminUser();
 869          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 870  
 871          $user = $this->getDataGenerator()->create_user();
 872          $this->setUser($user);
 873  
 874          // Set the bypass SEB check capability to $USER.
 875          $this->assign_user_capability('quizaccess/seb:bypassseb', \context_module::instance($this->quiz->cmid)->id);
 876  
 877          // Check that correct error message is returned.
 878          $this->assertFalse($this->make_rule()->prevent_access());
 879      }
 880  
 881      /**
 882       * Test that quiz form cannot be saved if using template, but not actually pick one.
 883       */
 884      public function test_mod_quiz_form_cannot_be_saved_using_template_and_template_is_not_set() {
 885          $this->setAdminUser();
 886          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 887  
 888          $form = $this->createMock('mod_quiz_mod_form');
 889          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 890  
 891          // Validate settings with a dummy form.
 892          $errors = quizaccess_seb::validate_settings_form_fields([], [
 893              'instance' => $this->quiz->id,
 894              'coursemodule' => $this->quiz->cmid,
 895              'seb_requiresafeexambrowser' => settings_provider::USE_SEB_TEMPLATE
 896          ], [], $form);
 897  
 898          $this->assertContains(get_string('invalidtemplate', 'quizaccess_seb'), $errors);
 899      }
 900  
 901      /**
 902       * Test that quiz form cannot be saved if uploaded invalid file.
 903       */
 904      public function test_mod_quiz_form_cannot_be_saved_using_uploaded_file_and_file_is_not_valid() {
 905          $this->setAdminUser();
 906          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 907  
 908          $form = $this->createMock('mod_quiz_mod_form');
 909          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 910  
 911          // Validate settings with a dummy form.
 912          $errors = quizaccess_seb::validate_settings_form_fields([], [
 913              'instance' => $this->quiz->id,
 914              'coursemodule' => $this->quiz->cmid,
 915              'seb_requiresafeexambrowser' => settings_provider::USE_SEB_UPLOAD_CONFIG,
 916              'filemanager_sebconfigfile' => 0,
 917          ], [], $form);
 918  
 919          $this->assertContainsEquals(get_string('filenotpresent', 'quizaccess_seb'), $errors);
 920      }
 921  
 922      /**
 923       * Test that quiz form cannot be saved if the global settings are set to require a password and no password is set.
 924       */
 925      public function test_mod_quiz_form_cannot_be_saved_if_global_settings_force_quiz_password_and_none_is_set() {
 926          $this->setAdminUser();
 927          // Set global settings to require quiz password but set password to be empty.
 928          set_config('quizpasswordrequired', '1', 'quizaccess_seb');
 929          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 930  
 931          $form = $this->createMock('mod_quiz_mod_form');
 932          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 933  
 934          // Validate settings with a dummy form.
 935          $errors = quizaccess_seb::validate_settings_form_fields([], [
 936              'instance' => $this->quiz->id,
 937              'coursemodule' => $this->quiz->cmid,
 938              'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
 939          ], [], $form);
 940  
 941          $this->assertContains(get_string('passwordnotset', 'quizaccess_seb'), $errors);
 942      }
 943  
 944      /**
 945       * Test that access to quiz is allowed if global setting is set to restrict quiz if no quiz password is set, and global quiz
 946       * password is set.
 947       */
 948      public function test_mod_quiz_form_can_be_saved_if_global_settings_force_quiz_password_and_is_set() {
 949          $this->setAdminUser();
 950          // Set global settings to require quiz password but set password to be empty.
 951          set_config('quizpasswordrequired', '1', 'quizaccess_seb');
 952  
 953          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 954  
 955          $form = $this->createMock('mod_quiz_mod_form');
 956          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 957  
 958          // Validate settings with a dummy form.
 959          $errors = quizaccess_seb::validate_settings_form_fields([], [
 960              'instance' => $this->quiz->id,
 961              'coursemodule' => $this->quiz->cmid,
 962              'quizpassword' => 'set',
 963              'seb_requiresafeexambrowser' => settings_provider::USE_SEB_CONFIG_MANUALLY,
 964          ], [], $form);
 965          $this->assertNotContains(get_string('passwordnotset', 'quizaccess_seb'), $errors);
 966      }
 967  
 968      /**
 969       * Test that quiz form can be saved if the global settings are set to require a password and no seb usage selected.
 970       */
 971      public function test_mod_quiz_form_can_be_saved_if_global_settings_force_quiz_password_and_none_no_seb() {
 972          $this->setAdminUser();
 973          // Set global settings to require quiz password but set password to be empty.
 974          set_config('quizpasswordrequired', '1', 'quizaccess_seb');
 975          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_NO);
 976  
 977          $form = $this->createMock('mod_quiz_mod_form');
 978          $form->method('get_context')->willReturn(\context_module::instance($this->quiz->cmid));
 979  
 980          // Validate settings with a dummy form.
 981          $errors = quizaccess_seb::validate_settings_form_fields([], [
 982              'instance' => $this->quiz->id,
 983              'coursemodule' => $this->quiz->cmid,
 984              'seb_requiresafeexambrowser' => settings_provider::USE_SEB_NO,
 985          ], [], $form);
 986  
 987          $this->assertNotContains(get_string('passwordnotset', 'quizaccess_seb'), $errors);
 988      }
 989  
 990      /**
 991       * Test get_download_seb_button, checks for empty config setting quizaccess_seb/downloadlink.
 992       */
 993      public function test_get_download_seb_button() {
 994          $this->setAdminUser();
 995          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
 996  
 997          $user = $this->getDataGenerator()->create_user();
 998          $this->setUser($user);
 999  
1000          $reflection = new \ReflectionClass('quizaccess_seb');
1001          $method = $reflection->getMethod('get_download_seb_button');
1002          $method->setAccessible(true);
1003  
1004          // The current default contents.
1005          $this->assertStringContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule()));
1006  
1007          set_config('downloadlink', '', 'quizaccess_seb');
1008  
1009          // Will not return any button if the URL is empty.
1010          $this->assertSame('', $method->invoke($this->make_rule()));
1011      }
1012  
1013      /**
1014       * Test get_download_seb_button shows download SEB link when required,
1015       */
1016      public function test_get_get_action_buttons_shows_download_seb_link() {
1017          $this->setAdminUser();
1018          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
1019  
1020          $user = $this->getDataGenerator()->create_user();
1021          $this->setUser($user);
1022  
1023          $reflection = new \ReflectionClass('quizaccess_seb');
1024          $method = $reflection->getMethod('get_action_buttons');
1025          $method->setAccessible(true);
1026  
1027          $this->assertStringContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule()));
1028  
1029          $this->quiz->seb_showsebdownloadlink = 0;
1030          $this->assertStringNotContainsString($this->get_seb_download_link(), $method->invoke($this->make_rule()));
1031      }
1032  
1033      /**
1034       * Test get_download_seb_button shows SEB config related links when required.
1035       */
1036      public function test_get_get_action_buttons_shows_launch_and_download_config_links() {
1037          $this->setAdminUser();
1038          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
1039  
1040          $user = $this->getDataGenerator()->create_user();
1041          $this->setUser($user);
1042  
1043          $reflection = new \ReflectionClass('quizaccess_seb');
1044          $method = $reflection->getMethod('get_action_buttons');
1045          $method->setAccessible(true);
1046  
1047          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
1048  
1049          // Should see link when using manually.
1050          $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1051          $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1052  
1053          // Should see links when using template.
1054          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_TEMPLATE);
1055          $quizsettings->set('templateid', $this->create_template()->get('id'));
1056          $quizsettings->save();
1057          $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1058          $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1059  
1060          // Should see links when using uploaded config.
1061          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_UPLOAD_CONFIG);
1062          $xml = file_get_contents(__DIR__ . '/fixtures/unencrypted.seb');
1063          $this->create_module_test_file($xml, $this->quiz->cmid);
1064          $quizsettings->save();
1065          $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1066          $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1067  
1068          // Shouldn't see links if using client config.
1069          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG);
1070          $quizsettings->save();
1071          $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1072          $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1073      }
1074  
1075      /**
1076       * Test get_download_seb_button shows SEB config related links as configured in "showseblinks".
1077       */
1078      public function test_get_get_action_buttons_shows_launch_and_download_config_links_as_configured() {
1079          $this->setAdminUser();
1080          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
1081  
1082          $user = $this->getDataGenerator()->create_user();
1083          $this->setUser($user);
1084  
1085          $reflection = new \ReflectionClass('quizaccess_seb');
1086          $method = $reflection->getMethod('get_action_buttons');
1087          $method->setAccessible(true);
1088  
1089          set_config('showseblinks', 'seb,http', 'quizaccess_seb');
1090          $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1091          $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1092  
1093          set_config('showseblinks', 'http', 'quizaccess_seb');
1094          $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1095          $this->assertStringContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1096  
1097          set_config('showseblinks', 'seb', 'quizaccess_seb');
1098          $this->assertStringContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1099          $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1100  
1101          set_config('showseblinks', '', 'quizaccess_seb');
1102          $this->assertStringNotContainsString($this->get_seb_launch_link(), $method->invoke($this->make_rule()));
1103          $this->assertStringNotContainsString($this->get_seb_config_download_link(), $method->invoke($this->make_rule()));
1104      }
1105  
1106      /**
1107       * Test get_quit_button. If attempt count is greater than 0
1108       */
1109      public function test_get_quit_button() {
1110          $this->setAdminUser();
1111          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
1112          $this->quiz->seb_linkquitseb = "http://test.quit.link";
1113  
1114          $user = $this->getDataGenerator()->create_user();
1115          $this->attempt_quiz($this->quiz, $user);
1116          $this->setUser($user);
1117  
1118          // Set-up the button to be called.
1119          $reflection = new \ReflectionClass('quizaccess_seb');
1120          $method = $reflection->getMethod('get_quit_button');
1121          $method->setAccessible(true);
1122  
1123          $button = $method->invoke($this->make_rule());
1124          $this->assertStringContainsString("http://test.quit.link", $button);
1125      }
1126  
1127      /**
1128       * Test description, checks for a valid SEB session and attempt count .
1129       */
1130      public function test_description() {
1131          $this->setAdminUser();
1132          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
1133  
1134          $this->quiz->seb_linkquitseb = "http://test.quit.link";
1135  
1136          // Set up basic dummy request.
1137          $_SERVER['HTTP_USER_AGENT'] = 'SEB_TEST_SITE';
1138  
1139          $user = $this->getDataGenerator()->create_user();
1140          $this->attempt_quiz($this->quiz, $user);
1141  
1142          $description = $this->make_rule()->description();
1143          $this->assertCount(2, $description);
1144          $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb'));
1145          $this->assertEquals($description[1], '');
1146  
1147          // Set the user as display_quit_button() uses the global $USER.
1148          $this->setUser($user);
1149          $description = $this->make_rule()->description();
1150          $this->assertCount(2, $description);
1151          $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb'));
1152  
1153          // The button is contained in the description when a quiz attempt is finished.
1154          $this->assertStringContainsString("http://test.quit.link", $description[1]);
1155      }
1156  
1157      /**
1158       * Test description displays download SEB config button when required.
1159       */
1160      public function test_description_shows_download_config_link_when_required() {
1161          $this->setAdminUser();
1162          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
1163  
1164          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
1165  
1166          $user = $this->getDataGenerator()->create_user();
1167          $roleid = $this->getDataGenerator()->create_role();
1168          $context = \context_module::instance($this->quiz->cmid);
1169          assign_capability('quizaccess/seb:bypassseb', CAP_ALLOW, $roleid, $context->id);
1170  
1171          $this->setUser($user);
1172  
1173          // Can see just basic description with standard perms.
1174          $description = $this->make_rule()->description();
1175          $this->assertCount(1, $description);
1176          $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb'));
1177  
1178          // Can see download config link as have bypass SEB permissions.
1179          $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id);
1180          $description = $this->make_rule()->description();
1181          $this->assertCount(3, $description);
1182          $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb'));
1183          $this->assertStringContainsString($this->get_seb_config_download_link(), $description[1]);
1184  
1185          // Can't see download config link as usage method doesn't have SEB config to download.
1186          $quizsettings->set('requiresafeexambrowser', settings_provider::USE_SEB_CLIENT_CONFIG);
1187          $quizsettings->save();
1188          $description = $this->make_rule()->description();
1189          $this->assertCount(2, $description);
1190          $this->assertEquals($description[0], get_string('sebrequired', 'quizaccess_seb'));
1191      }
1192  
1193      /**
1194       * Test block display before a quiz started.
1195       */
1196      public function test_blocks_display_before_attempt_started() {
1197          global $PAGE;
1198  
1199          $this->setAdminUser();
1200          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
1201  
1202          $user = $this->getDataGenerator()->create_user();
1203          $this->setUser($user);
1204  
1205          // We will check if we show only fake blocks. Which means no other blocks on a page.
1206          $reflection = new \ReflectionClass('block_manager');
1207          $property = $reflection->getProperty('fakeblocksonly');
1208          $property->setAccessible(true);
1209  
1210          $this->assertFalse($property->getValue($PAGE->blocks));
1211  
1212          // Don't display blocks before start.
1213          set_config('displayblocksbeforestart', 0, 'quizaccess_seb');
1214          $this->set_up_quiz_view_page();
1215          $this->make_rule()->prevent_access();
1216          $this->assertEquals('secure', $PAGE->pagelayout);
1217          $this->assertTrue($property->getValue($PAGE->blocks));
1218  
1219          // Display blocks before start.
1220          set_config('displayblocksbeforestart', 1, 'quizaccess_seb');
1221          $this->set_up_quiz_view_page();
1222          $this->make_rule()->prevent_access();
1223          $this->assertEquals('secure', $PAGE->pagelayout);
1224          $this->assertFalse($property->getValue($PAGE->blocks));
1225      }
1226  
1227      /**
1228       * Test block display after a quiz completed.
1229       */
1230      public function test_blocks_display_after_attempt_finished() {
1231          global $PAGE;
1232  
1233          $this->setAdminUser();
1234          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CLIENT_CONFIG);
1235  
1236          // Finish the quiz.
1237          $user = $this->getDataGenerator()->create_user();
1238          $this->attempt_quiz($this->quiz, $user);
1239          $this->setUser($user);
1240  
1241          // We will check if we show only fake blocks. Which means no other blocks on a page.
1242          $reflection = new \ReflectionClass('block_manager');
1243          $property = $reflection->getProperty('fakeblocksonly');
1244          $property->setAccessible(true);
1245  
1246          $this->assertFalse($property->getValue($PAGE->blocks));
1247  
1248          // Don't display blocks after finish.
1249          set_config('displayblockswhenfinished', 0, 'quizaccess_seb');
1250          $this->set_up_quiz_view_page();
1251          $this->make_rule()->prevent_access();
1252          $this->assertEquals('secure', $PAGE->pagelayout);
1253          $this->assertTrue($property->getValue($PAGE->blocks));
1254  
1255          // Display blocks after finish.
1256          set_config('displayblockswhenfinished', 1, 'quizaccess_seb');
1257          $this->set_up_quiz_view_page();
1258          $this->make_rule()->prevent_access();
1259          $this->assertEquals('secure', $PAGE->pagelayout);
1260          $this->assertFalse($property->getValue($PAGE->blocks));
1261      }
1262  
1263      /**
1264       * Test cleanup when quiz is completed.
1265       */
1266      public function test_current_attempt_finished() {
1267          global $SESSION;
1268          $this->setAdminUser();
1269  
1270          $this->quiz = $this->create_test_quiz($this->course, settings_provider::USE_SEB_CONFIG_MANUALLY);
1271          $quizsettings = quiz_settings::get_record(['quizid' => $this->quiz->id]);
1272          $quizsettings->save();
1273          // Set access for Moodle session.
1274          $SESSION->quizaccess_seb_access = [$this->quiz->cmid => true];
1275          $this->make_rule()->current_attempt_finished();
1276  
1277          $this->assertTrue(empty($SESSION->quizaccess_seb_access[$this->quiz->cmid]));
1278      }
1279  }