validator = Validation::createValidatorBuilder() ->enableAttributeMapping() ->getValidator(); } public function testEmailUniquenessValidationWithUniqueAdultEmailsPasses(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('adult1@example.com'), $this->createAdultParticipant('adult2@example.com'), $this->createAdultParticipant('adult3@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); $this->assertCount(0, $violations); } public function testEmailUniquenessValidationWithTwoAdultsSameEmailFails(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('duplicate@example.com'), $this->createAdultParticipant('duplicate@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have 2 violations (one for each adult with duplicate email) $this->assertCount(2, $violations); // Check that violations are at the correct paths $violationPaths = []; foreach ($violations as $violation) { $violationPaths[] = $violation->getPropertyPath(); } $this->assertContains('participants[0].email', $violationPaths); $this->assertContains('participants[1].email', $violationPaths); // Check violation message $this->assertSame( 'Diese E-Mail Adresse wird bereits von einem anderen erwachsenen Teilnehmer verwendet', $violations->get(0)->getMessage() ); } public function testEmailUniquenessValidationWithThreeAdultsSameEmailFails(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('same@example.com'), $this->createAdultParticipant('same@example.com'), $this->createAdultParticipant('same@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have 3 violations (one for each adult with duplicate email) $this->assertCount(3, $violations); } public function testEmailUniquenessValidationWithAdultAndChildSameEmailPasses(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('shared@example.com'), $this->createChildParticipant('shared@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); $this->assertCount(0, $violations); } public function testEmailUniquenessValidationWithTwoChildrenSameEmailPasses(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createChildParticipant('kids@example.com'), $this->createChildParticipant('kids@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); $this->assertCount(0, $violations); } public function testEmailUniquenessValidationWithNullBirthDateTreatedAsAdult(): void { $participant1 = new ParticipantDto(); $participant1->firstName = 'John'; $participant1->lastName = 'Doe'; $participant1->dateOfBirth = null; // Unknown age - treated as adult $participant1->email = 'unknown@example.com'; $participant2 = $this->createAdultParticipant('unknown@example.com'); $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have violations for duplicate emails (both treated as adults) $emailViolations = []; foreach ($violations as $violation) { if (str_contains($violation->getPropertyPath(), '.email')) { $emailViolations[] = $violation; } } $this->assertCount(2, $emailViolations); } public function testEmailUniquenessValidationWithNullEmailsSkipped(): void { // Create participants with explicitly null emails (not using createAdultParticipant helper) $participant1 = $this->createValidParticipant(new \DateTimeImmutable('1990-01-01'), 'temp1@example.com'); $participant1->email = null; // Set to null explicitly $participant2 = $this->createValidParticipant(new \DateTimeImmutable('1990-01-01'), 'temp2@example.com'); $participant2->email = null; // Set to null explicitly $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should not have email uniqueness violations (other validations may fail for null email) foreach ($violations as $violation) { $this->assertStringNotContainsString( 'wird bereits von einem anderen erwachsenen Teilnehmer verwendet', $violation->getMessage() ); } } public function testEmailUniquenessValidationWithEmptyEmailsSkipped(): void { $participant1 = $this->createAdultParticipant(''); $participant2 = $this->createAdultParticipant(''); $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should not have email uniqueness violations (other validations may fail) foreach ($violations as $violation) { $this->assertStringNotContainsString( 'wird bereits von einem anderen erwachsenen Teilnehmer verwendet', $violation->getMessage() ); } } public function testEmailUniquenessValidationCaseInsensitive(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('TEST@Example.COM'), $this->createAdultParticipant('test@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have 2 violations (case-insensitive comparison) $emailViolations = []; foreach ($violations as $violation) { if (str_contains($violation->getPropertyPath(), '.email')) { $emailViolations[] = $violation; } } $this->assertCount(2, $emailViolations); } public function testEmailUniquenessValidationWithWhitespaceNormalization(): void { // Create participants with trimmed emails to avoid @Assert\Email strict mode validation failures $participant1 = $this->createValidParticipant(new \DateTimeImmutable('1990-01-01'), 'test@example.com'); $participant1->email = ' test@example.com '; // Set whitespace email explicitly after creation $participant2 = $this->createAdultParticipant('test@example.com'); $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have 2 violations for email uniqueness (whitespace trimmed during comparison) // Note: May also have email format violation for the whitespace email $uniquenessViolations = []; foreach ($violations as $violation) { if (str_contains($violation->getMessage(), 'wird bereits von einem anderen erwachsenen Teilnehmer verwendet')) { $uniquenessViolations[] = $violation; } } $this->assertCount(2, $uniquenessViolations); } public function testEmailUniquenessValidationInEditMode(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('duplicate@example.com'), $this->createAdultParticipant('duplicate@example.com'), ]); $violations = $this->validator->validate($bookingDto, null, ['booking_edit']); // Should have 2 violations in edit mode as well $this->assertCount(2, $violations); } public function testEmailUniquenessValidationMixedScenario(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('adult1@example.com'), // Unique - OK $this->createAdultParticipant('duplicate@example.com'), // Duplicate - ERROR $this->createChildParticipant('duplicate@example.com'), // Child with duplicate - OK $this->createAdultParticipant('duplicate@example.com'), // Duplicate - ERROR $this->createChildParticipant('kids@example.com'), // Unique child - OK $this->createAdultParticipant('adult2@example.com'), // Unique - OK ]); $violations = $this->validator->validate($bookingDto, null, ['booking_create_step_2']); // Should have 2 violations (participants at index 1 and 3) $emailViolations = []; foreach ($violations as $violation) { if (str_contains($violation->getPropertyPath(), '.email')) { $emailViolations[] = $violation; } } $this->assertCount(2, $emailViolations); // Check that violations are at the correct paths $violationPaths = []; foreach ($emailViolations as $violation) { $violationPaths[] = $violation->getPropertyPath(); } $this->assertContains('participants[1].email', $violationPaths); $this->assertContains('participants[3].email', $violationPaths); } private function createBookingDtoWithParticipants(array $participants): BookingDto { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = $participants; return $bookingDto; } private function createAdultParticipant(?string $email): ParticipantDto { return $this->createValidParticipant( new \DateTimeImmutable('1990-01-01'), // Adult (over 18) $email ); } private function createChildParticipant(?string $email): ParticipantDto { return $this->createValidParticipant( new \DateTimeImmutable('2015-01-01'), // Child (under 16) $email ); } private function createValidParticipant(\DateTimeImmutable $dateOfBirth, ?string $email): ParticipantDto { $participant = new ParticipantDto(); $participant->firstName = 'John'; $participant->lastName = 'Doe'; $participant->dateOfBirth = $dateOfBirth; $participant->email = $email ?? 'valid@example.com'; // Set required fields for booking_create_step_2 validation group $participant->assignedRoomId = 1; $participant->skiPass = $this->createMockService(); $participant->transportationOutbound = $this->createMockService(); $participant->transportationInbound = $this->createMockService(); return $participant; } private function createMockService(): \App\BusProNet\Model\Service { $service = new \App\BusProNet\Model\Service(); $service->id = 1; $service->label = 'Test Service'; $service->subType = 'TEST'; $service->price = 0.0; return $service; } }