mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-11 12:28:23 +02:00
test: expand coverage, dedupe data-driven tests, extract shared WebTestCase base (#201)
* test: expand coverage, dedupe data-driven tests, extract shared WebTestCase base - Add #[CoversClass] to Base64Test and FilenameSanitizerTest - Merge near-duplicate test methods into #[DataProvider] cases across ResetPasswordControllerTest, SettingsControllerTest, ClaimSeasonCommandTest, FilenameSanitizerTest, Base64Test, and SeasonRepositoryTest - Add integration tests for previously untested controllers: public QuizController (quiz-taking flow), LoginController, RegistrationController, EliminationController, and PrepareEliminationController - Add unit tests for Elimination and BankQuestion entity logic - Extract shared WebTestCase setup/helpers (client, entityManager, login, entity lookups, CSRF token scraping) into AbstractControllerWebTestCase, removing duplicated boilerplate from all 14 WebTestCase files * test: address PR review feedback - Scope AbstractControllerWebTestCase::getCandidate/getQuizByName by season code (both Candidate and Quiz are only unique per season, not system-wide) and add a CandidateRepository regression test guarding against same-named candidates in different seasons - Add missing entityManager->clear() before verifying DB state after a POST in PrepareEliminationControllerTest and QuestionBankControllerTest - Add non-owner denial tests for BackofficeController::exportQuiz and QuizQuestionController::edit/reorder, which had IsGranted checks with no test coverage * ci: publish PHPUnit coverage to GitHub code coverage - Generate a Cobertura report alongside the existing JUnit report and upload it with actions/upload-code-coverage so coverage shows up on PRs and the default branch via GitHub's code coverage feature - Add a step to copy both reports out of the php container before publishing them, since var/ is a Docker volume (see the Dockerfile's VOLUME /app/var/) and isn't bind-mounted to the runner — this also fixes the existing JUnit report publishing step, which was silently looking at a path that never had contets on the runner * ci: fix coverage report paths and tolerate Code Quality not yet enabled - Write PHPUnit's JUnit and Cobertura reports to reports/ instead of var/, since var/ is a Docker volume (Dockerfile's VOLUME /app/var/) that isn't bind-mounted to the runner - reports written there never reached the host, which is also why the prior junit.xml publish step had nothing to read. reports/ is a plain path under the project's bind mount, so no extra copy-out step is needed - Set fail-on-error: false on the coverage upload step: it 404s until "Code Quality" is turned on for the repo under Settings > Code security > Code quality, a one-time manual step
This commit is contained in:
@@ -4,11 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Tests\Controller\Backoffice;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Safe\DateTimeImmutable;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Tvdt\Controller\Backoffice\SettingsController;
|
||||
@@ -17,43 +15,21 @@ use Tvdt\Entity\Quiz;
|
||||
use Tvdt\Entity\ResetPasswordRequest;
|
||||
use Tvdt\Entity\Season;
|
||||
use Tvdt\Entity\User;
|
||||
use Tvdt\Tests\Controller\AbstractControllerWebTestCase;
|
||||
|
||||
#[CoversClass(SettingsController::class)]
|
||||
final class SettingsControllerTest extends WebTestCase
|
||||
final class SettingsControllerTest extends AbstractControllerWebTestCase
|
||||
{
|
||||
private KernelBrowser $client;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->client = self::createClient();
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
parent::setUp();
|
||||
|
||||
$this->loginAs('test@example.org');
|
||||
}
|
||||
|
||||
private function loginAs(string $email): void
|
||||
{
|
||||
$user = $this->getUserByEmail($email);
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->client->loginUser($user);
|
||||
}
|
||||
|
||||
private function getUserByEmail(string $email): ?User
|
||||
{
|
||||
return $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]);
|
||||
}
|
||||
|
||||
private function getCsrfTokenFromSettings(string $formActionContains): string
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
self::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');
|
||||
return $this->getCsrfTokenFromPage('/backoffice/settings', $formActionContains);
|
||||
}
|
||||
|
||||
public function testSettingsPageLoadsAndNavContainsSettingsLink(): void
|
||||
@@ -98,7 +74,6 @@ final class SettingsControllerTest extends WebTestCase
|
||||
$this->entityManager->clear();
|
||||
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
$this->assertTrue($hasher->isPasswordValid($user, 'NewPass123!'));
|
||||
|
||||
@@ -107,32 +82,21 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testChangePasswordWithWrongCurrentPasswordIsRejected(): void
|
||||
/** @return iterable<string, array{string, string, string}> */
|
||||
public static function invalidPasswordChangeProvider(): iterable
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
$form = $this->client->getCrawler()->filter('form[action*="/backoffice/settings/password"]')->form([
|
||||
'change_user_password_form[currentPassword]' => 'wrong-password',
|
||||
'change_user_password_form[plainPassword][first]' => 'NewPass123!',
|
||||
'change_user_password_form[plainPassword][second]' => 'NewPass123!',
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
$this->entityManager->clear();
|
||||
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
$this->assertTrue($hasher->isPasswordValid($user, TestFixtures::PASSWORD));
|
||||
yield 'wrong current password' => ['wrong-password', 'NewPass123!', 'NewPass123!'];
|
||||
yield 'mismatched repeat' => [TestFixtures::PASSWORD, 'NewPass123!', 'SomethingElse!'];
|
||||
}
|
||||
|
||||
public function testChangePasswordWithMismatchedRepeatIsRejected(): void
|
||||
#[DataProvider('invalidPasswordChangeProvider')]
|
||||
public function testChangePasswordIsRejected(string $currentPassword, string $first, string $second): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
$form = $this->client->getCrawler()->filter('form[action*="/backoffice/settings/password"]')->form([
|
||||
'change_user_password_form[currentPassword]' => TestFixtures::PASSWORD,
|
||||
'change_user_password_form[plainPassword][first]' => 'NewPass123!',
|
||||
'change_user_password_form[plainPassword][second]' => 'SomethingElse!',
|
||||
'change_user_password_form[currentPassword]' => $currentPassword,
|
||||
'change_user_password_form[plainPassword][first]' => $first,
|
||||
'change_user_password_form[plainPassword][second]' => $second,
|
||||
]);
|
||||
$this->client->submit($form);
|
||||
|
||||
@@ -140,7 +104,6 @@ final class SettingsControllerTest extends WebTestCase
|
||||
$this->entityManager->clear();
|
||||
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
$this->assertTrue($hasher->isPasswordValid($user, TestFixtures::PASSWORD));
|
||||
}
|
||||
@@ -157,9 +120,8 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertEmailCount(1);
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->assertNotInstanceOf(User::class, $this->getUserByEmail('test@example.org'));
|
||||
$this->assertNotInstanceOf(User::class, $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'test@example.org']));
|
||||
$user = $this->getUserByEmail('new-address@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->assertFalse($user->isVerified);
|
||||
|
||||
// User stays logged in
|
||||
@@ -179,7 +141,7 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertEmailCount(0);
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->assertInstanceOf(User::class, $this->getUserByEmail('test@example.org'));
|
||||
$this->getUserByEmail('test@example.org');
|
||||
}
|
||||
|
||||
public function testResendConfirmationEmailSendsEmail(): void
|
||||
@@ -200,8 +162,8 @@ final class SettingsControllerTest extends WebTestCase
|
||||
$token = $this->getCsrfTokenFromSettings('/backoffice/settings/resend-confirmation');
|
||||
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$user->isVerified = true;
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
@@ -242,7 +204,6 @@ final class SettingsControllerTest extends WebTestCase
|
||||
public function testChangePasswordInvalidatesResetPasswordRequests(): void
|
||||
{
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->createResetPasswordRequest($user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
@@ -257,14 +218,12 @@ final class SettingsControllerTest extends WebTestCase
|
||||
$this->entityManager->clear();
|
||||
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->assertSame(0, $this->entityManager->getRepository(ResetPasswordRequest::class)->count(['user' => $user]));
|
||||
}
|
||||
|
||||
public function testChangeEmailInvalidatesResetPasswordRequests(): void
|
||||
{
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->createResetPasswordRequest($user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/backoffice/settings');
|
||||
@@ -277,7 +236,6 @@ final class SettingsControllerTest extends WebTestCase
|
||||
$this->entityManager->clear();
|
||||
|
||||
$user = $this->getUserByEmail('new-address@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->assertSame(0, $this->entityManager->getRepository(ResetPasswordRequest::class)->count(['user' => $user]));
|
||||
}
|
||||
|
||||
@@ -293,7 +251,7 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertResponseRedirects('/backoffice/settings');
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->assertInstanceOf(User::class, $this->getUserByEmail('test@example.org'));
|
||||
$this->getUserByEmail('test@example.org');
|
||||
}
|
||||
|
||||
public function testDeleteAccountRemovesSoleOwnerSeasonsAndKeepsSharedSeasons(): void
|
||||
@@ -309,7 +267,7 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertResponseRedirects();
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->assertNotInstanceOf(User::class, $this->getUserByEmail('sole-owner@example.org'));
|
||||
$this->assertNotInstanceOf(User::class, $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'sole-owner@example.org']));
|
||||
|
||||
// Sole-owner season is removed, including its quiz
|
||||
$this->assertNotInstanceOf(Season::class, $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => 'doomd']));
|
||||
@@ -340,7 +298,7 @@ final class SettingsControllerTest extends WebTestCase
|
||||
self::assertResponseRedirects();
|
||||
$this->entityManager->clear();
|
||||
|
||||
$this->assertNotInstanceOf(User::class, $this->getUserByEmail('user2@example.org'));
|
||||
$this->assertNotInstanceOf(User::class, $this->entityManager->getRepository(User::class)->findOneBy(['email' => 'user2@example.org']));
|
||||
|
||||
foreach (['krtek', 'bbbbb'] as $seasonCode) {
|
||||
$season = $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => $seasonCode]);
|
||||
@@ -378,7 +336,6 @@ final class SettingsControllerTest extends WebTestCase
|
||||
public function testDownloadDataRequiresVerifiedEmail(): void
|
||||
{
|
||||
$user = $this->getUserByEmail('test@example.org');
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$this->assertFalse($user->isVerified);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/backoffice/settings/download-data');
|
||||
@@ -389,8 +346,8 @@ final class SettingsControllerTest extends WebTestCase
|
||||
private function markUserVerified(string $email): void
|
||||
{
|
||||
$user = $this->getUserByEmail($email);
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
$user->isVerified = true;
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user