feat: add question bank management, quiz finalization, and related backend/frontend functionality

This commit is contained in:
2026-07-04 20:10:03 +02:00
parent d1d1eb3a24
commit c34c25dff7
37 changed files with 2493 additions and 206 deletions
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Tvdt\Tests\Controller\Backoffice;
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\User;
/**
* Guards against N+1 regressions: every backoffice page must run a
* constant number of queries, independent of the amount of data.
*/
#[CoversNothing]
final class QueryCountTest extends WebTestCase
{
private KernelBrowser $client;
protected function setUp(): void
{
$this->client = self::createClient();
$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$user = $entityManager->getRepository(User::class)->findOneBy(['email' => 'krtek-admin@example.org']);
$this->assertInstanceOf(User::class, $user);
$this->client->loginUser($user);
}
/** @return \Generator<string, array{string, int}> */
public static function pageProvider(): \Generator
{
yield 'season tests tab' => ['/backoffice/season/krtek', 4];
yield 'question bank tab' => ['/backoffice/season/krtek/question-bank', 6];
yield 'candidates tab' => ['/backoffice/season/krtek/candidates', 4];
yield 'settings tab' => ['/backoffice/season/krtek/settings', 4];
yield 'question bank new' => ['/backoffice/season/krtek/question-bank/new', 4];
yield 'quiz overview' => ['/backoffice/season/krtek/quiz/%quiz%/overview', 5];
yield 'quiz result' => ['/backoffice/season/krtek/quiz/%quiz%/result', 6];
yield 'quiz candidates list' => ['/backoffice/season/krtek/quiz/%quiz%/candidates-list', 7];
}
#[DataProvider('pageProvider')]
public function testPageStaysWithinQueryBudget(string $url, int $maxQueries): void
{
if (str_contains($url, '%quiz%')) {
$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$quiz = $entityManager->getRepository(Quiz::class)->findOneBy(['name' => 'Quiz 1']);
$this->assertInstanceOf(Quiz::class, $quiz);
$url = str_replace('%quiz%', (string) $quiz->id, $url);
}
// Warm up so boot/setup queries do not pollute the profiled request
$this->client->request(Request::METHOD_GET, '/backoffice');
$this->client->enableProfiler();
$this->client->request(Request::METHOD_GET, $url);
$this->assertResponseIsSuccessful();
$profile = $this->client->getProfile();
$this->assertInstanceOf(Profile::class, $profile);
$collector = $profile->getCollector('db');
$this->assertInstanceOf(DoctrineDataCollector::class, $collector);
$queries = $collector->getQueries()['default'] ?? [];
$sql = implode("\n", array_map(static fn (array $query): string => $query['sql'], $queries));
$this->assertLessThanOrEqual($maxQueries, \count($queries), \sprintf("Query budget exceeded for %s:\n%s", $url, $sql));
}
}
@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Tvdt\Tests\Controller\Backoffice;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Tvdt\Controller\Backoffice\QuestionBankController;
use Tvdt\Entity\BankQuestion;
use Tvdt\Entity\Question;
use Tvdt\Entity\QuestionLabel;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\User;
#[CoversClass(QuestionBankController::class)]
final class QuestionBankControllerTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->client = self::createClient();
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
}
private function loginAsOwner(): void
{
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'krtek-admin@example.org']);
$this->assertInstanceOf(User::class, $user);
$this->client->loginUser($user);
}
private function getBankQuestion(string $question): BankQuestion
{
$bankQuestion = $this->entityManager->getRepository(BankQuestion::class)->findOneBy(['question' => $question]);
$this->assertInstanceOf(BankQuestion::class, $bankQuestion);
return $bankQuestion;
}
private function getQuizByName(string $name): Quiz
{
$quiz = $this->entityManager->getRepository(Quiz::class)->findOneBy(['name' => $name]);
$this->assertInstanceOf(Quiz::class, $quiz);
return $quiz;
}
private function getCsrfToken(string $formActionContains): string
{
$crawler = $this->client->getCrawler();
$input = $crawler->filter(\sprintf('form[action*="%s"] input[name="_token"]', $formActionContains));
$this->assertGreaterThan(0, $input->count(), \sprintf('No form found with action containing "%s"', $formActionContains));
return (string) $input->first()->attr('value');
}
public function testIndexListsBankQuestions(): void
{
$this->loginAsOwner();
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('body', 'Wie is de Krtek?');
$this->assertSelectorTextContains('body', 'Waar sliep de Krtek?');
$this->assertSelectorTextContains('body', 'Wat at de Krtek als ontbijt?');
}
public function testIndexFiltersByLabel(): void
{
$this->loginAsOwner();
$label = $this->entityManager->getRepository(QuestionLabel::class)->findOneBy(['name' => 'Locatie']);
$this->assertInstanceOf(QuestionLabel::class, $label);
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank?label='.$label->id);
$this->assertResponseIsSuccessful();
$body = $crawler->filter('tbody')->text();
$this->assertStringContainsString('Waar sliep de Krtek?', $body);
$this->assertStringNotContainsString('Wie is de Krtek?', $body);
}
public function testNonOwnerIsDenied(): void
{
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'test@example.org']);
$this->assertInstanceOf(User::class, $user);
$this->client->loginUser($user);
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$this->assertResponseStatusCodeSame(403);
}
public function testCreateBankQuestion(): void
{
$this->loginAsOwner();
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank/new');
$this->assertResponseIsSuccessful();
$token = (string) $crawler->filter('input[name="bank_question_form[_token]"]')->attr('value');
$this->client->request(Request::METHOD_POST, '/backoffice/season/krtek/question-bank/new', [
'bank_question_form' => [
'question' => 'Wat is de lievelingskleur van de Krtek?',
'reusable' => '1',
'answers' => [
['text' => 'Rood', 'isRightAnswer' => '1'],
['text' => 'Blauw'],
],
'_token' => $token,
],
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$bankQuestion = $this->getBankQuestion('Wat is de lievelingskleur van de Krtek?');
$this->assertTrue($bankQuestion->reusable);
$this->assertCount(2, $bankQuestion->answers);
$this->assertSame('Rood', (string) $bankQuestion->answers->first());
}
public function testCreateRefusedWithoutCorrectAnswer(): void
{
$this->loginAsOwner();
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank/new');
$token = (string) $crawler->filter('input[name="bank_question_form[_token]"]')->attr('value');
$this->client->request(Request::METHOD_POST, '/backoffice/season/krtek/question-bank/new', [
'bank_question_form' => [
'question' => 'Vraag zonder goed antwoord',
'answers' => [
['text' => 'Een'],
['text' => 'Twee'],
],
'_token' => $token,
],
]);
$this->assertResponseIsUnprocessable();
$this->assertNotInstanceOf(BankQuestion::class, $this->entityManager->getRepository(BankQuestion::class)->findOneBy(['question' => 'Vraag zonder goed antwoord']));
}
public function testEditBankQuestion(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Wat at de Krtek als ontbijt?');
$url = \sprintf('/backoffice/season/krtek/question-bank/%s/edit', $bankQuestion->id);
$crawler = $this->client->request(Request::METHOD_GET, $url);
$this->assertResponseIsSuccessful();
$token = (string) $crawler->filter('input[name="bank_question_form[_token]"]')->attr('value');
$this->client->request(Request::METHOD_POST, $url, [
'bank_question_form' => [
'question' => 'Wat dronk de Krtek als ontbijt?',
'answers' => [
['text' => 'Koffie', 'isRightAnswer' => '1'],
['text' => 'Thee'],
],
'_token' => $token,
],
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$bankQuestion = $this->getBankQuestion('Wat dronk de Krtek als ontbijt?');
$this->assertFalse($bankQuestion->reusable);
$this->assertCount(2, $bankQuestion->answers);
}
public function testDeleteUsedBankQuestionLeavesQuizIntact(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Waar sliep de Krtek?');
$quiz2QuestionCount = $this->getQuizByName('Quiz 2')->questions->count();
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$token = $this->getCsrfToken(\sprintf('%s/delete', $bankQuestion->id));
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/question-bank/%s/delete', $bankQuestion->id), [
'_token' => $token,
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$this->assertNotInstanceOf(BankQuestion::class, $this->entityManager->getRepository(BankQuestion::class)->findOneBy(['question' => 'Waar sliep de Krtek?']));
$this->assertCount($quiz2QuestionCount, $this->getQuizByName('Quiz 2')->questions);
}
public function testAssignCopiesQuestionIntoQuiz(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Wat at de Krtek als ontbijt?');
$quiz = $this->getQuizByName('Quiz 2');
$questionCount = $quiz->questions->count();
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$token = $this->getCsrfToken(\sprintf('%s/assign', $bankQuestion->id));
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/question-bank/%s/assign', $bankQuestion->id), [
'_token' => $token,
'quiz' => (string) $quiz->id,
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$quiz = $this->getQuizByName('Quiz 2');
$this->assertCount($questionCount + 1, $quiz->questions);
$copiedQuestion = null;
$maxOrdering = 0;
foreach ($quiz->questions as $question) {
$maxOrdering = max($maxOrdering, $question->ordering);
if ('Wat at de Krtek als ontbijt?' === $question->question) {
$copiedQuestion = $question;
}
}
$this->assertInstanceOf(Question::class, $copiedQuestion);
$this->assertSame($maxOrdering, $copiedQuestion->ordering);
$this->assertCount(3, $copiedQuestion->answers);
$bankQuestion = $this->getBankQuestion('Wat at de Krtek als ontbijt?');
$this->assertTrue($bankQuestion->isUsed());
$this->assertFalse($bankQuestion->canBeAssigned());
}
public function testAssignUsedNonReusableQuestionIsRefused(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Waar sliep de Krtek?');
$quiz = $this->getQuizByName('Quiz 2');
$questionCount = $quiz->questions->count();
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
// The assign form is not rendered for used questions, so post with another form's token
$token = $this->getCsrfToken('/assign');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/question-bank/%s/assign', $bankQuestion->id), [
'_token' => $token,
'quiz' => (string) $quiz->id,
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$this->assertCount($questionCount, $this->getQuizByName('Quiz 2')->questions);
}
public function testAssignSameReusableQuestionTwiceToSameQuizIsRefused(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Wie is de Krtek?');
$quiz = $this->getQuizByName('Quiz 2');
$questionCount = $quiz->questions->count();
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$token = $this->getCsrfToken(\sprintf('%s/assign', $bankQuestion->id));
$url = \sprintf('/backoffice/season/krtek/question-bank/%s/assign', $bankQuestion->id);
$this->client->request(Request::METHOD_POST, $url, ['_token' => $token, 'quiz' => (string) $quiz->id]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->client->request(Request::METHOD_POST, $url, ['_token' => $token, 'quiz' => (string) $quiz->id]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$this->assertCount($questionCount + 1, $this->getQuizByName('Quiz 2')->questions);
}
public function testAssignIntoFinalizedQuizIsDenied(): void
{
$this->loginAsOwner();
$bankQuestion = $this->getBankQuestion('Wie is de Krtek?');
$finalizedQuiz = $this->getQuizByName('Quiz 1');
$this->assertTrue($finalizedQuiz->isFinalized());
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$token = $this->getCsrfToken(\sprintf('%s/assign', $bankQuestion->id));
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/question-bank/%s/assign', $bankQuestion->id), [
'_token' => $token,
'quiz' => (string) $finalizedQuiz->id,
]);
$this->assertResponseStatusCodeSame(403);
}
public function testAddAndDeleteLabel(): void
{
$this->loginAsOwner();
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$token = (string) $crawler->filter('form[action$="/question-bank/labels"] input[name="_token"]')->attr('value');
$this->client->request(Request::METHOD_POST, '/backoffice/season/krtek/question-bank/labels', [
'_token' => $token,
'name' => 'Opdracht',
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$label = $this->entityManager->getRepository(QuestionLabel::class)->findOneBy(['name' => 'Opdracht']);
$this->assertInstanceOf(QuestionLabel::class, $label);
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/question-bank');
$deleteToken = $this->getCsrfToken(\sprintf('labels/%s/delete', $label->id));
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/question-bank/labels/%s/delete', $label->id), [
'_token' => $deleteToken,
]);
$this->assertResponseRedirects('/backoffice/season/krtek/question-bank');
$this->entityManager->clear();
$this->assertNotInstanceOf(QuestionLabel::class, $this->entityManager->getRepository(QuestionLabel::class)->findOneBy(['name' => 'Opdracht']));
}
}
@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace Tvdt\Tests\Controller\Backoffice;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use Safe\DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Tvdt\Controller\Backoffice\QuizController;
use Tvdt\Entity\Answer;
use Tvdt\Entity\Candidate;
use Tvdt\Entity\Question;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\QuizCandidate;
use Tvdt\Entity\Season;
use Tvdt\Entity\User;
#[CoversClass(QuizController::class)]
final class QuizFinalizeTest extends WebTestCase
{
private KernelBrowser $client;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
$this->client = self::createClient();
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'krtek-admin@example.org']);
$this->assertInstanceOf(User::class, $user);
$this->client->loginUser($user);
}
private function getQuizByName(string $name): Quiz
{
$quiz = $this->entityManager->getRepository(Quiz::class)->findOneBy(['name' => $name]);
$this->assertInstanceOf(Quiz::class, $quiz);
return $quiz;
}
private function getKrtekSeason(): Season
{
$season = $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => 'krtek']);
$this->assertInstanceOf(Season::class, $season);
return $season;
}
private function getCsrfTokenFromOverview(Quiz $quiz, string $formActionContains): string
{
$crawler = $this->client->request(Request::METHOD_GET, \sprintf('/backoffice/season/krtek/quiz/%s/overview', $quiz->id));
$this->assertResponseIsSuccessful();
$input = $crawler->filter(\sprintf('form[action*="%s"] input[name="_token"]', $formActionContains));
$this->assertGreaterThan(0, $input->count(), \sprintf('No form found with action containing "%s"', $formActionContains));
return (string) $input->first()->attr('value');
}
public function testFinalizeSetsFinalizedAt(): void
{
$quiz = $this->getQuizByName('Quiz 2');
$this->assertFalse($quiz->isFinalized());
$token = $this->getCsrfTokenFromOverview($quiz, '/finalize');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/finalize', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertTrue($this->getQuizByName('Quiz 2')->isFinalized());
}
public function testFinalizeRefusedWhenQuizHasErrors(): void
{
$season = $this->getKrtekSeason();
$invalidQuiz = new Quiz();
$invalidQuiz->name = 'Invalid Quiz';
$question = new Question();
$question->question = 'Vraag zonder goed antwoord';
$question->ordering = 1;
$question->addAnswer(new Answer('Fout'));
$question->addAnswer(new Answer('Ook fout'));
$invalidQuiz->addQuestion($question);
$season->addQuiz($invalidQuiz);
$this->entityManager->persist($invalidQuiz);
$this->entityManager->flush();
// Token intention is shared, so any quiz overview provides it
$token = $this->getCsrfTokenFromOverview($invalidQuiz, '/finalize');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/finalize', $invalidQuiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertFalse($this->getQuizByName('Invalid Quiz')->isFinalized());
}
public function testEnableRefusedWhenNotFinalized(): void
{
$quiz = $this->getQuizByName('Quiz 2');
$this->assertFalse($quiz->isFinalized());
$token = $this->getCsrfTokenFromOverview($quiz, '/enable');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/quiz/%s/enable', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$season = $this->getKrtekSeason();
$this->assertInstanceOf(Quiz::class, $season->activeQuiz);
$this->assertSame('Quiz 1', $season->activeQuiz->name);
}
public function testEnableAllowedWhenFinalized(): void
{
$quiz = $this->getQuizByName('Quiz 2');
$quiz->finalizedAt = new DateTimeImmutable();
$this->entityManager->flush();
$token = $this->getCsrfTokenFromOverview($quiz, '/enable');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/quiz/%s/enable', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$season = $this->getKrtekSeason();
$this->assertInstanceOf(Quiz::class, $season->activeQuiz);
$this->assertSame('Quiz 2', $season->activeQuiz->name);
}
public function testUnfinalize(): void
{
$quiz = $this->getQuizByName('Quiz 2');
$quiz->finalizedAt = new DateTimeImmutable();
$this->entityManager->flush();
$token = $this->getCsrfTokenFromOverview($quiz, '/unfinalize');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/unfinalize', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertFalse($this->getQuizByName('Quiz 2')->isFinalized());
}
public function testUnfinalizeRefusedWhenQuizIsActive(): void
{
// Quiz 1 is finalized and active in the fixtures; scrape a token from Quiz 2 (same intention)
$quiz2 = $this->getQuizByName('Quiz 2');
$quiz2->finalizedAt = new DateTimeImmutable();
$this->entityManager->flush();
$token = $this->getCsrfTokenFromOverview($quiz2, '/unfinalize');
$quiz1 = $this->getQuizByName('Quiz 1');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/unfinalize', $quiz1->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertTrue($this->getQuizByName('Quiz 1')->isFinalized());
}
public function testUnfinalizeRefusedWhenCandidatesStarted(): void
{
$quiz = $this->getQuizByName('Quiz 2');
$quiz->finalizedAt = new DateTimeImmutable();
$this->entityManager->flush();
// Scrape the token before a candidate starts, since the button disappears afterwards
$token = $this->getCsrfTokenFromOverview($quiz, '/unfinalize');
$candidate = $this->entityManager->getRepository(Candidate::class)->findOneBy(['name' => 'Tom']);
$this->assertInstanceOf(Candidate::class, $candidate);
$quizCandidate = new QuizCandidate($quiz, $candidate);
$quizCandidate->started = new DateTimeImmutable();
$this->entityManager->persist($quizCandidate);
$this->entityManager->flush();
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/unfinalize', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertTrue($this->getQuizByName('Quiz 2')->isFinalized());
}
public function testClearQuizResetsFinalization(): void
{
$quiz = $this->getQuizByName('Quiz 1');
$this->assertTrue($quiz->isFinalized());
$token = $this->getCsrfTokenFromOverview($quiz, '/clear');
$this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/clear', $quiz->id), ['_token' => $token]);
$this->assertResponseRedirects();
$this->entityManager->clear();
$this->assertFalse($this->getQuizByName('Quiz 1')->isFinalized());
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Tvdt\Tests\Repository;
use PHPUnit\Framework\Attributes\CoversClass;
use Tvdt\Entity\QuestionLabel;
use Tvdt\Repository\BankQuestionRepository;
#[CoversClass(BankQuestionRepository::class)]
final class BankQuestionRepositoryTest extends DatabaseTestCase
{
private BankQuestionRepository $bankQuestionRepository;
protected function setUp(): void
{
parent::setUp();
$this->bankQuestionRepository = self::getContainer()->get(BankQuestionRepository::class);
}
public function testFindBySeasonReturnsAllQuestions(): void
{
$season = $this->getSeasonByCode('krtek');
$bankQuestions = $this->bankQuestionRepository->findBySeason($season);
$this->assertCount(3, $bankQuestions);
}
public function testFindBySeasonFiltersByLabel(): void
{
$season = $this->getSeasonByCode('krtek');
$label = $this->entityManager->getRepository(QuestionLabel::class)
->findOneBy(['season' => $season, 'name' => 'Locatie']);
$this->assertInstanceOf(QuestionLabel::class, $label);
$bankQuestions = $this->bankQuestionRepository->findBySeason($season, $label);
$this->assertCount(2, $bankQuestions);
foreach ($bankQuestions as $bankQuestion) {
$this->assertTrue($bankQuestion->labels->contains($label));
}
}
public function testFindBySeasonIgnoresOtherSeasons(): void
{
$season = $this->getSeasonByCode('bbbbb');
$this->assertCount(0, $this->bankQuestionRepository->findBySeason($season));
}
}
+57
View File
@@ -12,9 +12,11 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Tvdt\Entity\Answer;
use Tvdt\Entity\BankQuestion;
use Tvdt\Entity\Candidate;
use Tvdt\Entity\Elimination;
use Tvdt\Entity\Question;
use Tvdt\Entity\QuestionLabel;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\Season;
use Tvdt\Entity\User;
@@ -75,6 +77,61 @@ final class SeasonVoterTest extends TestCase
$answer = self::createStub(Answer::class);
$answer->question = $question;
yield 'Answer' => [$answer];
$bankQuestion = self::createStub(BankQuestion::class);
$bankQuestion->season = $season;
yield 'BankQuestion' => [$bankQuestion];
$questionLabel = self::createStub(QuestionLabel::class);
$questionLabel->season = $season;
yield 'QuestionLabel' => [$questionLabel];
}
public function testModifyQuizContentGrantedOnUnlockedQuiz(): void
{
$season = self::createStub(Season::class);
$season->method('isOwner')->willReturn(true);
$quiz = self::createStub(Quiz::class);
$quiz->season = $season;
$quiz->method('isLocked')->willReturn(false);
$this->assertSame(VoterInterface::ACCESS_GRANTED, $this->seasonVoter->vote($this->token, $quiz, [SeasonVoter::MODIFY_QUIZ_CONTENT]));
}
public function testModifyQuizContentDeniedOnLockedQuiz(): void
{
$season = self::createStub(Season::class);
$season->method('isOwner')->willReturn(true);
$quiz = self::createStub(Quiz::class);
$quiz->season = $season;
$quiz->method('isLocked')->willReturn(true);
$this->assertSame(VoterInterface::ACCESS_DENIED, $this->seasonVoter->vote($this->token, $quiz, [SeasonVoter::MODIFY_QUIZ_CONTENT]));
}
public function testModifyQuizContentDeniedOnLockedQuizForAdmin(): void
{
$user = new User();
$user->roles = ['ROLE_ADMIN'];
$token = $this->createStub(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$quiz = self::createStub(Quiz::class);
$quiz->season = self::createStub(Season::class);
$quiz->method('isLocked')->willReturn(true);
$this->assertSame(VoterInterface::ACCESS_DENIED, $this->seasonVoter->vote($token, $quiz, [SeasonVoter::MODIFY_QUIZ_CONTENT]));
}
public function testModifyQuizContentDeniedOnNonQuizSubject(): void
{
$season = self::createStub(Season::class);
$season->method('isOwner')->willReturn(true);
$this->assertSame(VoterInterface::ACCESS_DENIED, $this->seasonVoter->vote($this->token, $season, [SeasonVoter::MODIFY_QUIZ_CONTENT]));
}
public function testWrongUserTypeReturnFalse(): void