quiz-6.0.0-alpha4/tests/src/Functional/QuizTakingTest.php

tests/src/Functional/QuizTakingTest.php
<?php

namespace Drupal\Tests\quiz\Functional;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\quiz\Util\QuizUtil;

/**
 * Test quiz taking behavior.
 *
 * @group Quiz
 */
class QuizTakingTest extends QuizTestBase {

  use StringTranslationTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'quiz_multichoice',
    'quiz_directions',
    'quiz_truefalse',
  ];

  /**
   * Converts session data from the database into an array of data.
   *
   * This is necessary because PHP's methods write to the $_SESSION
   * superglobal,
   * and we want to work with session data that isn't ours.
   *
   * See
   * https://stackoverflow.com/questions/530761/how-can-i-unserialize-session-data-to-an-arbitrary-variable-in-php
   *
   * @param string $session_data
   *   The serialized session data. (Note this is not the same serialization
   *   format as used by serialize().)
   *
   * @return array
   *   An array of data.
   *
   * @throws \Exception
   */
  protected function unserializePhp(string $session_data): array {
    $return_data = [];
    $offset = 0;

    while ($offset < strlen($session_data)) {
      if (!str_contains(substr($session_data, $offset), "|")) {
        throw new \Exception("Invalid data, remaining: " . substr($session_data, $offset));
      }

      // Extract variable name.
      $pos = strpos($session_data, "|", $offset);
      if ($pos === FALSE) {
        throw new \Exception("Malformed session data.");
      }

      $varname = substr($session_data, $offset, $pos - $offset);
      $offset = $pos + 1;

      // Extract the serialized value.
      $remainingData = substr($session_data, $offset);
      $data = @unserialize($remainingData);

      // Ensure unserialization was successful.
      if ($data === FALSE && $remainingData !== 'b:0;') {
        throw new \Exception("Failed to unserialize data at offset $offset.");
      }

      $return_data[$varname] = $data;

      // Move offset forward by the exact serialized data length.
      $dataLength = strlen(serialize($data));
      $offset += $dataLength;
    }

    return $return_data;
  }

  /**
   * Converts an array of data into a session data string.
   *
   * This is necessary because PHP's methods write to the $_SESSION superglobal,
   * and we want to work with session data that isn't ours.
   *
   * See https://stackoverflow.com/questions/530761/how-can-i-unserialize-session-data-to-an-arbitrary-variable-in-php
   *
   * @param array $data
   *   The session data.
   *
   * @return string
   *   The serialized data. (Note this is not the same serialization format as
   *   used by serialize().)
   */
  protected function serializePhp(array $data): string {
    $return_data = '';
    foreach ($data as $key => $key_data) {
      $return_data .= "$key|" . serialize($key_data);
    }

    return $return_data;
  }

  /**
   * Write data to the given session.
   *
   * This exists because we can't use
   * \Drupal\Core\Session\SessionHandler::write() as that assumes the current
   * session is being written, and will fail within tests as no session exists.
   *
   * @param int $uid
   *   The user ID.
   * @param string $sid
   *   The session ID.
   * @param string $value
   *   The session data. Use serializePhp() to format this.
   */
  protected function writeSession(int $uid, string $sid, string $value): void {
    $fields = [
      'uid' => $uid,
      'hostname' => 'testing',
      'session' => $value,
      'timestamp' => \Drupal::time()->getRequestTime(),
    ];
    $this->container->get('database')->merge('sessions')
      ->keys(['sid' => Crypt::hashBase64($sid)])
      ->fields($fields)
      ->execute();
  }

  /**
   * Test the quiz availability tests.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testQuizAvailability() {
    // Anonymous doesn't have 'access quiz' permissions, so login a user that
    // has that permission.
    $this->drupalLogin($this->user);
    $future = \Drupal::time()->getRequestTime() + 86400;
    $past = \Drupal::time()->getRequestTime() - 86400;

    // Within range.
    $quiz_node_open = $this->createQuiz([
      'quiz_date' => [
        'value' => date('Y-m-d\TH:i:s', $past),
        'end_value' => date('Y-m-d\TH:i:s', $future),
      ],
    ]);
    $this->drupalGet("quiz/{$quiz_node_open->id()}");
    $this->assertSession()->pageTextNotContains($this->t('This @quiz is closed.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->assertSession()->pageTextNotContains($this->t('You are not allowed to take this @quiz.', ['@quiz' => QuizUtil::getQuizName()]));

    // Starts in the future.
    $quiz_node_future = $this->createQuiz([
      'quiz_date' => [
        'value' => date('Y-m-d\TH:i:s', $future),
        'end_value' => date('Y-m-d\TH:i:s', $future + 1),
      ],
    ]);
    $this->drupalGet("quiz/{$quiz_node_future->id()}");
    $this->assertSession()->pageTextContains($this->t('This @quiz is not yet open.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->assertSession()->pageTextNotContains($this->t('You are not allowed to take this @quiz.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->drupalGet("quiz/{$quiz_node_future->id()}/take");
    $this->assertSession()->pageTextContains($this->t('This @quiz is not yet open.', ['@quiz' => QuizUtil::getQuizName()]));

    // Ends in the past.
    $quiz_node_past = $this->createQuiz([
      'quiz_date' => [
        'value' => date('Y-m-d\TH:i:s', $past),
        'end_value' => date('Y-m-d\TH:i:s', $past + 1),
      ],
    ]);
    $this->drupalGet("quiz/{$quiz_node_past->id()}");
    $this->assertSession()->pageTextContains($this->t('This @quiz is closed.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->assertSession()->pageTextNotContains($this->t('You are not allowed to take this @quiz.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->drupalGet("quiz/{$quiz_node_past->id()}/take");
    $this->assertSession()->pageTextContains($this->t('This @quiz is closed.', ['@quiz' => QuizUtil::getQuizName()]));

    // Always available.
    $quiz = $this->createQuiz();
    $this->drupalGet("quiz/{$quiz->id()}");
    $this->assertSession()->pageTextNotContains($this->t('This @quiz is closed.', ['@quiz' => QuizUtil::getQuizName()]));
    $this->assertSession()->pageTextNotContains($this->t('You are not allowed to take this @quiz.', ['@quiz' => QuizUtil::getQuizName()]));
  }

  /**
   * Make sure questions cannot be viewed outside of quizzes.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ExpectationException
   */
  public function testViewQuestionsOutsideQuiz() {
    $this->drupalLogin($this->admin);
    $quiz_node = $this->createQuiz();

    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz-question/{$question1->id()}");
    $this->assertSession()->statusCodeEquals(403);

    $user_with_privs = $this->drupalCreateUser([
      'view any quiz_question',
      'access quiz',
    ]);
    $this->drupalLogin($user_with_privs);
    $this->drupalGet("quiz-question/{$question1->id()}");
    $this->assertSession()->statusCodeEquals(200);
  }

  /**
   * Test allow/restrict changing of answers.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   * @throws \Behat\Mink\Exception\ElementNotFoundException
   * @throws \Behat\Mink\Exception\ExpectationException
   */
  public function testChangeAnswer() {
    $quiz_node = $this->createQuiz([
      'review_options' => ['question' => ['score' => 'score']],
    ]);

    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);
    $question3 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question3, $quiz_node);

    // Answer incorrectly.
    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '0',
    ], (string) $this->t('Next'));
    $this->assertSession()->pageTextContains('Score: 0 of 1');

    // Go back and correct the answer.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
    ], (string) $this->t('Next'));
    $this->assertSession()->pageTextContains('Score: 1 of 1');

    // Go back and incorrect the answer.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '0',
    ], (string) $this->t('Next'));
    $this->assertSession()->pageTextContains('Score: 0 of 1');

    $quiz_node->set('allow_change', 0);
    $quiz_node->save();

    // Check that the answer cannot be changed.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->assertSession()->fieldDisabled('edit-question-1-answer-1');
    $this->submitForm([], (string) $this->t('Next'));
    $this->assertSession()->pageTextContains('Score: 0 of 1');

    // Check allow change/blank behavior.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/2");
    $this->submitForm([], (string) $this->t('Skip'));
    $this->drupalGet("quiz/{$quiz_node->id()}/take/2");
    $this->assertSession()->fieldDisabled('edit-question-2-answer-1');
    $quiz_node->set('allow_change_blank', 1);
    $quiz_node->save();
    $this->drupalGet("quiz/{$quiz_node->id()}/take/2");
    $this->assertSession()->fieldEnabled('edit-question-2-answer-1');
  }

  /**
   * Test the max attempt message configuration.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testQuizMaxAttemptsMessage() {
    $quiz_node = $this->createQuiz([
      'takes' => 2,
    ]);

    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '0',
    ], (string) $this->t('Finish'));

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}");
    $this->assertSession()->pageTextContains('You can only take this Quiz 2 times. You have taken it 1 time.');

    $quiz_node->set('show_attempt_stats', 0)->save();

    $this->drupalGet("quiz/{$quiz_node->id()}");
    $this->assertSession()->pageTextNotContains('You can only take this Quiz 2 times. You have taken it 1 time.');
  }

  /**
   * Test Quiz Max Attempts.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testQuizMaxAttempts() {
    $quiz_node = $this->createQuiz([
      'takes' => 2,
    ]);

    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '0',
    ], (string) $this->t('Next'));
    $this->submitForm([
      "question[{$question2->id()}][answer]" => '0',
    ], (string) $this->t('Finish'));

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}");
    $this->assertSession()->pageTextContains('You can only take this Quiz 2 times. You have taken it 1 time.');
    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '0',
    ], (string) $this->t('Next'));

    // Make sure we can get back.
    $this->drupalGet("quiz/{$quiz_node->id()}");
    $this->assertSession()->pageTextNotContains('You can only take this Quiz 2 times. You have taken it 1 time.');
    $this->drupalGet("quiz/{$quiz_node->id()}/take/2");
    $this->submitForm([
      "question[{$question2->id()}][answer]" => '0',
    ], (string) $this->t('Finish'));

    // No more attempts.
    $this->drupalGet("quiz/{$quiz_node->id()}");
    $this->assertSession()->pageTextContains('You have already taken this Quiz 2 times. You may not take it again.');
  }

  /**
   * Test that a user can answer a skipped question.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testAnswerSkipped() {
    $this->drupalLogin($this->admin);
    $quiz_node = $this->createQuiz([
      'allow_skipping' => 1,
      'allow_jumping' => 1,
    ]);

    // 2 questions.
    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);

    $this->drupalLogin($this->user);

    // Leave a question blank.
    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([], (string) $this->t('Skip'));

    // Fill out the blank question.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
    ], (string) $this->t('Next'));
    $this->submitForm([
      "question[{$question2->id()}][answer]" => '1',
    ], (string) $this->t('Finish'));

    $this->assertSession()->pageTextContains("Your score: 100%");
  }

  /**
   * Make sure a user can answer or skip an old question's revision.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function testAnswerOnOldQuizRevisioning() {
    $this->drupalLogin($this->admin);

    $question1 = $this->createQuestion([
      'title' => 'Q 1',
      'body' => 'Q 1',
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $quiz_node = $this->linkQuestionToQuiz($question1);

    $question2 = $this->createQuestion([
      'title' => 'Q 2',
      'body' => 'Q 2',
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);

    $question1->revision = TRUE;
    $question1->save();

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}/take");

    // Leave a question blank.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([], (string) $this->t('Skip'));

    // Submit the question.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
    ], (string) $this->t('Next'));
  }

  /**
   * Verify non gradable questions are excluded from counts.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ResponseTextException
   */
  public function testQuestionCount() {
    $quiz_node = $this->createQuiz([
      'review_options' => ['question' => ['score' => 'score']],
    ]);

    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'directions',
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);
    $question3 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question3, $quiz_node);
    $question4 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question4, $quiz_node);
    $question5 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question5, $quiz_node);

    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}");

    // @todo check the pager, this isn't reliable
    $this->assertSession()->pageTextContains("4");
  }

  /**
   * Test the mark doubtful functionality.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ElementNotFoundException
   * @throws \Behat\Mink\Exception\ExpectationException
   */
  public function testMarkDoubtful() {
    $this->drupalLogin($this->admin);
    $quiz_node = $this->createQuiz([
      'allow_skipping' => 1,
      'allow_jumping' => 1,
      'mark_doubtful' => 1,
    ]);

    // 2 questions.
    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'directions',
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);

    // Take the quiz.
    $this->drupalLogin($this->user);
    $this->drupalGet("quiz/{$quiz_node->id()}/take");

    // Ensure it is on truefalse.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->assertSession()->fieldExists("edit-question-{$question1->id()}-is-doubtful");

    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
      "question[{$question1->id()}][is_doubtful]" => '1',
    ], (string) $this->t('Next'));
    // Go back and verify it was saved.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->assertSession()->checkboxChecked("edit-question-{$question1->id()}-is-doubtful");

    // Ensure it is not on quiz directions.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/2");
    $this->assertSession()->fieldNotExists("edit-question-{$question2->id()}-is-doubtful");
  }

  /**
   * Test if necessary session data disappears during a quiz.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Behat\Mink\Exception\ExpectationException
   * @throws \Exception
   */
  public function testQuestionSessionLossRedirect(): void {
    $this->drupalLogin($this->admin);
    $quiz_node = $this->createQuiz();

    // 2 questions.
    $question1 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question1, $quiz_node);
    $question2 = $this->createQuestion([
      'type' => 'truefalse',
      'truefalse_correct' => 1,
    ]);
    $this->linkQuestionToQuiz($question2, $quiz_node);

    $this->drupalLogin($this->user);

    $this->drupalGet("quiz/{$quiz_node->id()}/take");
    $this->assertSession()->addressEquals("quiz/{$quiz_node->id()}/take/1");
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
    ], (string) $this->t('Next'));
    // Go back to the previous question.
    $this->drupalGet("quiz/{$quiz_node->id()}/take/1");
    $this->assertSession()->addressEquals("quiz/{$quiz_node->id()}/take/1");
    // Remove the session data.
    $sid = $this->getSession()->getCookie($this->getSessionName());
    $session_data = $this->container->get('session_handler.storage')->read($sid);
    $session_data = $this->unserializePhp($session_data);
    // Unsetting $session_data['_sf2_attributes']['quiz'][1]['current_question']
    // has no effect. Whereas this should trigger 'access denied' or a redirect.
    unset($session_data['_sf2_attributes']['quiz'][1]['result_id']);
    // Write the removed session data back to the database.
    $this->writeSession($this->user->id(), $sid, $this->serializePhp($session_data));
    // Try to go to the next question.
    $this->submitForm([
      "question[{$question1->id()}][answer]" => '1',
    ], (string) $this->t('Next'));
    // Find if the user is redirected to quiz/quiz id.
    $this->assertSession()->addressEquals("quiz/{$quiz_node->id()}");
    // Confirm that this is not a 403.
    $this->assertSession()->pageTextNotContains("Access denied");

  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc