- {% if verification_mode %}
- {% include '_partials/_alert.html.twig' with { 'message': 'Bitte überprüfe deine Daten und bestätige sie am Ende des Formulars.' } %}
- {% endif %}
{% if not form.vars.valid %}
{% include '_partials/_form_errors.html.twig' with { 'form': form } %}
@@ -192,14 +189,8 @@
} %}
- {% if verification_mode %}
-
- {{ form_row(form.confirmDataVerification) }}
-
- {% endif %}
-
diff --git a/tests/Form/FinancialDataTypeTest.php b/tests/Form/FinancialDataTypeTest.php
new file mode 100644
index 0000000..5c414dc
--- /dev/null
+++ b/tests/Form/FinancialDataTypeTest.php
@@ -0,0 +1,166 @@
+createForm($formData);
+ $form->submit($this->validSubmission());
+
+ $this->assertTrue($form->isValid());
+
+ $formData->applyTo($teamer);
+
+ $this->assertNull($teamer->getPhoto());
+ $this->assertNull($teamer->getLanguage());
+ $this->assertNull($teamer->getHealthInsuranceCompany());
+ }
+
+ /**
+ * Guards against the form and the check drifting apart: what the form accepts
+ * as complete must be exactly what unlocks the portal.
+ */
+ public function testAValidSubmissionSatisfiesTheCheck(): void
+ {
+ $teamer = new Teamer();
+ $user = (new User())
+ ->setRoles(['ROLE_TEAMER'])
+ ->setTeamer($teamer)
+ ;
+
+ $check = new FinancialDataRequiredCheck();
+ $this->assertFalse($check->isSatisfied($user));
+
+ $formData = FinancialDataDto::fromTeamer($teamer);
+ $form = $this->createForm($formData);
+ $form->submit($this->validSubmission());
+ $formData->applyTo($teamer);
+
+ $this->assertTrue($check->isSatisfied($user));
+ }
+
+ public function testEmptySubmitIsRejectedOnEveryField(): void
+ {
+ $form = $this->createForm(new FinancialDataDto());
+ $form->submit([
+ 'taxId' => '',
+ 'iban' => '',
+ 'bic' => '',
+ 'bank' => '',
+ 'holder' => '',
+ ]);
+
+ $this->assertFalse($form->isValid());
+
+ foreach (['taxId', 'iban', 'bic', 'bank', 'holder'] as $field) {
+ $this->assertCount(1, $form->get($field)->getErrors(), sprintf('Expected one error on "%s"', $field));
+ }
+ }
+
+ public function testMalformedIbanIsRejected(): void
+ {
+ $form = $this->createForm(new FinancialDataDto());
+ $form->submit(array_merge($this->validSubmission(), ['iban' => 'DE00 not an iban']));
+
+ $this->assertFalse($form->isValid());
+ $this->assertCount(1, $form->get('iban')->getErrors());
+ $this->assertCount(0, $form->get('taxId')->getErrors());
+ }
+
+ public function testFromTeamerToleratesAMissingBankAccount(): void
+ {
+ $teamer = (new Teamer())->setTaxId('12345678901');
+
+ $formData = FinancialDataDto::fromTeamer($teamer);
+
+ $this->assertSame('12345678901', $formData->getTaxId());
+ $this->assertNull($formData->getIban());
+ }
+
+ public function testApplyToCreatesABankAccountWhenTheTeamerHasNone(): void
+ {
+ $teamer = new Teamer();
+ $this->assertNull($teamer->getBankAccount());
+
+ (new FinancialDataDto())
+ ->setTaxId('12345678901')
+ ->setIban('DE02120300000000202051')
+ ->setBic('BYLADEM1001')
+ ->setBank('Deutsche Kreditbank')
+ ->setHolder('Erika Mustermann')
+ ->applyTo($teamer)
+ ;
+
+ $this->assertInstanceOf(BankAccount::class, $teamer->getBankAccount());
+ $this->assertSame('DE02120300000000202051', $teamer->getBankAccount()->getIban());
+ $this->assertSame('12345678901', $teamer->getTaxId());
+ }
+
+ public function testFromTeamerRoundTripsAnExistingBankAccount(): void
+ {
+ $teamer = (new Teamer())
+ ->setTaxId('12345678901')
+ ->setBankAccount(
+ (new BankAccount())
+ ->setIban('DE02120300000000202051')
+ ->setBic('BYLADEM1001')
+ ->setBank('Deutsche Kreditbank')
+ ->setHolder('Erika Mustermann')
+ )
+ ;
+
+ $formData = FinancialDataDto::fromTeamer($teamer);
+
+ $this->assertSame('DE02120300000000202051', $formData->getIban());
+ $this->assertSame('BYLADEM1001', $formData->getBic());
+ $this->assertSame('Deutsche Kreditbank', $formData->getBank());
+ $this->assertSame('Erika Mustermann', $formData->getHolder());
+ }
+
+ private function validSubmission(): array
+ {
+ return [
+ 'taxId' => '12345678901',
+ 'iban' => 'DE02120300000000202051',
+ 'bic' => 'BYLADEM1001',
+ 'bank' => 'Deutsche Kreditbank',
+ 'holder' => 'Erika Mustermann',
+ ];
+ }
+
+ private function createForm(FinancialDataDto $formData): FormInterface
+ {
+ self::bootKernel();
+
+ /** @var FormFactoryInterface $formFactory */
+ $formFactory = self::getContainer()->get(FormFactoryInterface::class);
+
+ return $formFactory->create(FinancialDataType::class, $formData, [
+ 'csrf_protection' => false,
+ ]);
+ }
+}
diff --git a/tests/Form/PersonalDataConfirmationTypeTest.php b/tests/Form/PersonalDataConfirmationTypeTest.php
new file mode 100644
index 0000000..887a47d
--- /dev/null
+++ b/tests/Form/PersonalDataConfirmationTypeTest.php
@@ -0,0 +1,84 @@
+setCreatedAt(new \DateTimeImmutable('2023-10-11 16:29:04'));
+ $user = (new User())
+ ->setRoles(['ROLE_TEAMER'])
+ ->setTeamer($teamer)
+ ;
+
+ $check = new PersonalDataVerificationRequiredCheck(new RecurringDeadlines(['04-01', '10-01']));
+ $this->assertFalse($check->isSatisfied($user));
+
+ $form = $this->createForm();
+ $form->submit(['confirmed' => '1']);
+
+ $this->assertTrue($form->isValid());
+
+ // what PersonalDataController does on a valid submit
+ $teamer->setDataVerifiedAt(new \DateTimeImmutable('2026-08-10 12:00:00'));
+
+ $this->assertTrue($check->isSatisfied($user));
+ $this->assertNull($teamer->getPhoto());
+ $this->assertNull($teamer->getLanguage());
+ $this->assertNull($teamer->getHealthInsuranceCompany());
+ }
+
+ public function testAnUntickedBoxIsRejected(): void
+ {
+ $form = $this->createForm();
+ $form->submit(['confirmed' => null]);
+
+ $this->assertFalse($form->isValid());
+ $this->assertCount(1, $form->get('confirmed')->getErrors());
+ }
+
+ private function createForm(): FormInterface
+ {
+ self::bootKernel();
+
+ /** @var FormFactoryInterface $formFactory */
+ $formFactory = self::getContainer()->get(FormFactoryInterface::class);
+
+ return $formFactory->create(PersonalDataConfirmationType::class, new PersonalDataConfirmationDto(), [
+ 'csrf_protection' => false,
+ ]);
+ }
+}
diff --git a/tests/Security/RequiredCheck/FinancialDataRequiredCheckTest.php b/tests/Security/RequiredCheck/FinancialDataRequiredCheckTest.php
new file mode 100644
index 0000000..2606159
--- /dev/null
+++ b/tests/Security/RequiredCheck/FinancialDataRequiredCheckTest.php
@@ -0,0 +1,136 @@
+check = new FinancialDataRequiredCheck();
+ }
+
+ /**
+ * @dataProvider mixedRoleProvider
+ */
+ public function testDoesNotApplyToExcludedMixedRoleUsersWithTeamerRole(string $excludedRole): void
+ {
+ $user = $this->createTeamerUser();
+ $user->setRoles([$excludedRole, 'ROLE_TEAMER']);
+
+ $this->assertFalse($this->check->appliesTo($user));
+ }
+
+ public function mixedRoleProvider(): array
+ {
+ return [
+ ['ROLE_ADMIN'],
+ ['ROLE_MANAGER'],
+ ['ROLE_HOUSE_MANAGER'],
+ ];
+ }
+
+ public function testRedirectsToItsOwnCheckPage(): void
+ {
+ // must not be the full profile form, which would also demand photo,
+ // language and health insurance and could lock the teamer out
+ $this->assertSame('app_teamer_check_financial_data', $this->check->getRouteName());
+ }
+
+ public function testIsNotSatisfiedWithoutTeamer(): void
+ {
+ $user = (new User())->setRoles(['ROLE_TEAMER']);
+
+ $this->assertFalse($this->check->isSatisfied($user));
+ }
+
+ public function testIsNotSatisfiedWhenNothingIsSet(): void
+ {
+ $user = $this->createTeamerUser();
+
+ $this->assertFalse($this->check->isSatisfied($user));
+ }
+
+ public function testIsNotSatisfiedWhenTaxIdIsMissing(): void
+ {
+ $user = $this->createTeamerUser();
+ $user->getTeamer()?->setBankAccount($this->createBankAccount());
+
+ $this->assertFalse($this->check->isSatisfied($user));
+ }
+
+ public function testIsNotSatisfiedWhenBankAccountIsMissing(): void
+ {
+ $user = $this->createTeamerUser();
+ $user->getTeamer()?->setTaxId('12345678901');
+
+ $this->assertFalse($this->check->isSatisfied($user));
+ }
+
+ /**
+ * @dataProvider missingBankAccountFieldProvider
+ */
+ public function testIsNotSatisfiedWhenBankAccountFieldIsMissing(string $setter): void
+ {
+ $bankAccount = $this->createBankAccount();
+ $bankAccount->{$setter}(null);
+
+ $user = $this->createTeamerUser();
+ $user->getTeamer()
+ ?->setTaxId('12345678901')
+ ->setBankAccount($bankAccount)
+ ;
+
+ $this->assertFalse($this->check->isSatisfied($user));
+ }
+
+ public function missingBankAccountFieldProvider(): array
+ {
+ return [
+ ['setIban'],
+ ['setBic'],
+ ['setBank'],
+ ['setHolder'],
+ ];
+ }
+
+ public function testIsSatisfiedWhenTaxIdAndBankAccountAreComplete(): void
+ {
+ $user = $this->createTeamerUser();
+ $user->getTeamer()
+ ?->setTaxId('12345678901')
+ ->setBankAccount($this->createBankAccount())
+ ;
+
+ $this->assertTrue($this->check->isSatisfied($user));
+ }
+
+ private function createBankAccount(): BankAccount
+ {
+ return (new BankAccount())
+ ->setIban('DE02120300000000202051')
+ ->setBic('BYLADEM1001')
+ ->setBank('Deutsche Kreditbank')
+ ->setHolder('Erika Mustermann')
+ ;
+ }
+
+ private function createTeamerUser(): User
+ {
+ $teamer = new Teamer();
+
+ return (new User())
+ ->setRoles(['ROLE_TEAMER'])
+ ->setTeamer($teamer)
+ ;
+ }
+}
diff --git a/tests/Security/RequiredCheck/PersonalDataVerificationRequiredCheckTest.php b/tests/Security/RequiredCheck/PersonalDataVerificationRequiredCheckTest.php
index d35d646..9915dba 100644
--- a/tests/Security/RequiredCheck/PersonalDataVerificationRequiredCheckTest.php
+++ b/tests/Security/RequiredCheck/PersonalDataVerificationRequiredCheckTest.php
@@ -7,6 +7,7 @@ namespace App\Tests\Security\RequiredCheck;
use App\Entity\Teamer;
use App\Entity\User;
use App\RequiredTeamerCheck\PersonalDataVerificationRequiredCheck;
+use App\RequiredTeamerCheck\RecurringDeadlines;
use Carbon\CarbonImmutable;
use PHPUnit\Framework\TestCase;
@@ -24,10 +25,9 @@ class PersonalDataVerificationRequiredCheckTest extends TestCase
public function testAppliesToTeamerUsers(): void
{
- $check = new PersonalDataVerificationRequiredCheck();
$user = (new User())->setRoles(['ROLE_TEAMER']);
- $this->assertTrue($check->appliesTo($user));
+ $this->assertTrue($this->createCheck()->appliesTo($user));
}
/**
@@ -35,10 +35,9 @@ class PersonalDataVerificationRequiredCheckTest extends TestCase
*/
public function testDoesNotApplyToExcludedMixedRoleUsersWithTeamerRole(string $excludedRole): void
{
- $check = new PersonalDataVerificationRequiredCheck();
$user = (new User())->setRoles([$excludedRole, 'ROLE_TEAMER']);
- $this->assertFalse($check->appliesTo($user));
+ $this->assertFalse($this->createCheck()->appliesTo($user));
}
public function mixedRoleProvider(): array
@@ -50,115 +49,63 @@ class PersonalDataVerificationRequiredCheckTest extends TestCase
];
}
+ public function testRedirectsToItsOwnConfirmationPage(): void
+ {
+ // not the profile form, which demands the whole 'profile' group and can
+ // therefore be impossible to submit
+ $this->assertSame('app_teamer_check_personal_data', $this->createCheck()->getRouteName());
+ }
+
public function testIsNotSatisfiedWhenTeamerIsMissing(): void
{
- $check = new PersonalDataVerificationRequiredCheck();
$user = (new User())->setRoles(['ROLE_TEAMER']);
- $this->assertFalse($check->isSatisfied($user));
+ $this->assertFalse($this->createCheck()->isSatisfied($user));
}
- public function testIsSatisfiedBeforeFirstDeadlineEvenWithoutVerification(): void
+ public function testToleratesAnUnpersistedTeamerWithoutACreationDate(): void
{
- CarbonImmutable::setTestNow('2026-01-10 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
+ CarbonImmutable::setTestNow('2026-01-20 12:00:00');
- $teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2025-11-05 12:00:00'));
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
-
- $this->assertTrue($check->isSatisfied($user));
+ // Teamer::getCreatedAt() is declared non-nullable but throws until flushed
+ $this->assertFalse($this->createCheck()->isSatisfied($this->createUser(new Teamer())));
}
- public function testIsNotSatisfiedAfterFirstDeadlineWhenNeverVerified(): void
+ public function testDelegatesTheDueDecisionToItsSchedule(): void
{
CarbonImmutable::setTestNow('2026-01-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
$teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2025-11-05 12:00:00'));
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
+ $user = $this->createUser($teamer);
- $this->assertFalse($check->isSatisfied($user));
+ $this->assertFalse($this->createCheck()->isSatisfied($user));
+
+ $teamer->setDataVerifiedAt(new \DateTimeImmutable('2026-01-16 10:00:00'));
+
+ $this->assertTrue($this->createCheck()->isSatisfied($user));
}
- public function testIsSatisfiedWhenVerifiedAfterCurrentDeadline(): void
+ public function testAnEmptyScheduleNeverForcesVerification(): void
{
- CarbonImmutable::setTestNow('2026-05-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
+ CarbonImmutable::setTestNow('2026-01-20 12:00:00');
- $teamer = (new Teamer())
- ->setCreatedAt(new \DateTimeImmutable('2025-11-05 12:00:00'))
- ->setDataVerifiedAt(new \DateTimeImmutable('2026-01-16 10:00:00'))
- ;
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
+ $teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2025-11-05 12:00:00'));
- $this->assertTrue($check->isSatisfied($user));
+ $check = new PersonalDataVerificationRequiredCheck(new RecurringDeadlines([]));
+
+ $this->assertTrue($check->isSatisfied($this->createUser($teamer)));
}
- public function testIsNotSatisfiedWhenVerificationIsOutdated(): void
+ private function createCheck(): PersonalDataVerificationRequiredCheck
{
- CarbonImmutable::setTestNow('2026-07-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
-
- $teamer = (new Teamer())
- ->setCreatedAt(new \DateTimeImmutable('2025-11-05 12:00:00'))
- ->setDataVerifiedAt(new \DateTimeImmutable('2026-02-01 10:00:00'))
- ;
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
-
- $this->assertFalse($check->isSatisfied($user));
+ return new PersonalDataVerificationRequiredCheck(new RecurringDeadlines(['01-15', '07-15']));
}
- public function testIsSatisfiedAfterFirstDeadlineWhenRegisteredAfterThatDeadline(): void
+ private function createUser(Teamer $teamer): User
{
- CarbonImmutable::setTestNow('2026-05-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
-
- $teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2026-03-01 10:00:00'));
- $user = (new User())
+ return (new User())
->setRoles(['ROLE_TEAMER'])
->setTeamer($teamer)
;
-
- $this->assertTrue($check->isSatisfied($user));
- }
-
- public function testIsSatisfiedAfterSecondDeadlineWhenRegisteredAfterThatDeadline(): void
- {
- CarbonImmutable::setTestNow('2026-11-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
-
- $teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2026-10-01 10:00:00'));
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
-
- $this->assertTrue($check->isSatisfied($user));
- }
-
- public function testIsNotSatisfiedAfterSecondDeadlineWhenRegisteredBeforeCurrentYearDeadlines(): void
- {
- CarbonImmutable::setTestNow('2026-11-20 12:00:00');
- $check = new PersonalDataVerificationRequiredCheck(['01-15', '07-15']);
-
- $teamer = (new Teamer())->setCreatedAt(new \DateTimeImmutable('2025-10-01 10:00:00'));
- $user = (new User())
- ->setRoles(['ROLE_TEAMER'])
- ->setTeamer($teamer)
- ;
-
- $this->assertFalse($check->isSatisfied($user));
}
}
diff --git a/tests/Security/RequiredCheck/RecurringDeadlinesTest.php b/tests/Security/RequiredCheck/RecurringDeadlinesTest.php
new file mode 100644
index 0000000..3fcc06d
--- /dev/null
+++ b/tests/Security/RequiredCheck/RecurringDeadlinesTest.php
@@ -0,0 +1,125 @@
+assertFalse($deadlines->isDue(null, new \DateTimeImmutable('2020-01-01'), CarbonImmutable::parse('2026-08-10')));
+ $this->assertNull($deadlines->getCurrentDeadline(CarbonImmutable::parse('2026-08-10')));
+ }
+
+ public function testIsNotDueBeforeTheFirstDeadlineEvenWhenNeverConfirmed(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertFalse($deadlines->isDue(
+ null,
+ new \DateTimeImmutable('2025-11-05 12:00:00'),
+ CarbonImmutable::parse('2026-01-10 12:00:00'),
+ ));
+ }
+
+ public function testIsDueAfterTheFirstDeadlineWhenNeverConfirmed(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertTrue($deadlines->isDue(
+ null,
+ new \DateTimeImmutable('2025-11-05 12:00:00'),
+ CarbonImmutable::parse('2026-01-20 12:00:00'),
+ ));
+ }
+
+ public function testIsNotDueWhenConfirmedAfterTheCurrentDeadline(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertFalse($deadlines->isDue(
+ new \DateTimeImmutable('2026-01-16 10:00:00'),
+ new \DateTimeImmutable('2025-11-05 12:00:00'),
+ CarbonImmutable::parse('2026-05-20 12:00:00'),
+ ));
+ }
+
+ public function testIsDueWhenTheConfirmationPredatesTheCurrentDeadline(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertTrue($deadlines->isDue(
+ new \DateTimeImmutable('2026-02-01 10:00:00'),
+ new \DateTimeImmutable('2025-11-05 12:00:00'),
+ CarbonImmutable::parse('2026-07-20 12:00:00'),
+ ));
+ }
+
+ public function testIsNotDueWhenRegisteredAfterTheFirstDeadline(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertFalse($deadlines->isDue(
+ null,
+ new \DateTimeImmutable('2026-03-01 10:00:00'),
+ CarbonImmutable::parse('2026-05-20 12:00:00'),
+ ));
+ }
+
+ public function testIsNotDueWhenRegisteredAfterTheSecondDeadline(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertFalse($deadlines->isDue(
+ null,
+ new \DateTimeImmutable('2026-10-01 10:00:00'),
+ CarbonImmutable::parse('2026-11-20 12:00:00'),
+ ));
+ }
+
+ public function testIsDueWhenRegisteredBeforeAllCurrentYearDeadlines(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertTrue($deadlines->isDue(
+ null,
+ new \DateTimeImmutable('2025-10-01 10:00:00'),
+ CarbonImmutable::parse('2026-11-20 12:00:00'),
+ ));
+ }
+
+ public function testAMissingRegistrationDateDoesNotExempt(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertTrue($deadlines->isDue(null, null, CarbonImmutable::parse('2026-01-20 12:00:00')));
+ }
+
+ public function testGetCurrentDeadlineReturnsTheMostRecentlyPassedOne(): void
+ {
+ $deadlines = new RecurringDeadlines(['01-15', '07-15']);
+
+ $this->assertSame(
+ '2026-07-15',
+ $deadlines->getCurrentDeadline(CarbonImmutable::parse('2026-08-10'))?->format('Y-m-d'),
+ );
+ $this->assertNull($deadlines->getCurrentDeadline(CarbonImmutable::parse('2026-01-10')));
+ }
+}