validator = Validation::createValidatorBuilder() ->enableAttributeMapping() ->getValidator(); } public function testAdultWithUniqueEmailPassesValidation(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createValidAdultParticipant('unique1@example.com'), $this->createValidAdultParticipant('unique2@example.com'), ]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); $this->assertCount(0, $violations); } public function testAdultWithDuplicateEmailFailsValidation(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('duplicate@example.com'), $this->createAdultParticipant('duplicate@example.com'), ]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); $this->assertCount(1, $violations); $this->assertSame('participant.email', $violations->get(0)->getPropertyPath()); $this->assertSame( 'Diese E-Mail Adresse wird bereits von einem anderen Teilnehmer verwendet', $violations->get(0)->getMessage() ); } public function testChildWithDuplicateEmailPassesValidation(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('shared@example.com'), $this->createChildParticipant('shared@example.com'), ]); // Validate the child participant (index 1) $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[1], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); $this->assertCount(0, $violations); } public function testEmptyEmailPassesValidation(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant(''), $this->createAdultParticipant(''), ]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); // Should not trigger uniqueness validation (handled by @Email and @NotBlank constraints) $violations = $this->validator->validate($wrapper, null, ['booking_create']); // May have other violations, but not uniqueness $hasUniquenessViolation = false; foreach ($violations as $violation) { if (str_contains($violation->getMessage(), 'wird bereits von einem anderen Teilnehmer verwendet')) { $hasUniquenessViolation = true; break; } } $this->assertFalse($hasUniquenessViolation, 'Should not have uniqueness violation for empty emails'); } public function testNullEmailPassesValidation(): void { $participant1 = $this->createAdultParticipant('temp@example.com'); $participant1->email = null; $participant2 = $this->createAdultParticipant('temp@example.com'); $participant2->email = null; $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); // Should not trigger uniqueness validation (handled by @Email and @NotBlank constraints) $violations = $this->validator->validate($wrapper, null, ['booking_create']); // May have other violations, but not uniqueness $hasUniquenessViolation = false; foreach ($violations as $violation) { if (str_contains($violation->getMessage(), 'wird bereits von einem anderen Teilnehmer verwendet')) { $hasUniquenessViolation = true; break; } } $this->assertFalse($hasUniquenessViolation, 'Should not have uniqueness violation for null emails'); } public function testCaseInsensitiveEmailComparison(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('TEST@Example.COM'), $this->createAdultParticipant('test@example.com'), ]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); // Should have uniqueness violation (case-insensitive comparison) $uniquenessViolations = array_filter( iterator_to_array($violations), fn ($v) => str_contains($v->getMessage(), 'wird bereits von einem anderen Teilnehmer verwendet') ); $this->assertCount(1, $uniquenessViolations); } public function testWhitespaceNormalizationInEmailComparison(): void { $participant1 = $this->createAdultParticipant('test@example.com'); $participant1->email = ' test@example.com '; $participant2 = $this->createAdultParticipant('test@example.com'); $bookingDto = $this->createBookingDtoWithParticipants([$participant1, $participant2]); $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); // Should have uniqueness violation (whitespace trimmed during comparison) $uniquenessViolations = array_filter( iterator_to_array($violations), fn ($v) => str_contains($v->getMessage(), 'wird bereits von einem anderen Teilnehmer verwendet') ); $this->assertCount(1, $uniquenessViolations); } public function testMultipleDuplicatesDetectedCorrectly(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('same@example.com'), $this->createAdultParticipant('same@example.com'), $this->createAdultParticipant('same@example.com'), ]); // Validate first participant - should fail $wrapper1 = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations1 = $this->validator->validate($wrapper1, null, ['booking_create']); $this->assertCount(1, $violations1); // Validate second participant - should fail $wrapper2 = new ParticipantEditDto( participant: $bookingDto->participants[1], bookingContext: $bookingDto, ); $violations2 = $this->validator->validate($wrapper2, null, ['booking_create']); $this->assertCount(1, $violations2); // Validate third participant - should fail $wrapper3 = new ParticipantEditDto( participant: $bookingDto->participants[2], bookingContext: $bookingDto, ); $violations3 = $this->validator->validate($wrapper3, null, ['booking_create']); $this->assertCount(1, $violations3); } public function testSelfComparisonSkipped(): void { $bookingDto = $this->createBookingDtoWithParticipants([ $this->createAdultParticipant('only@example.com'), ]); // Participant should not validate against itself $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_create']); $this->assertCount(0, $violations); } public function testValidationInEditMode(): void { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); $bookingDto = new BookingDto($travel, 1); // hotelId must be int // Create mock booking to simulate edit mode $mockBooking = new \App\BusProNet\Model\Booking(); $bookingDto->booking = $mockBooking; // Create valid participants with all required fields for strict validation $participant1 = $this->createAdultParticipant('duplicate@example.com'); $participant1->index = 0; $participant1->mutable = false; // Immutable - strict validation applies $participant1->mobile = '+49 123 456789'; // Required for applicant $participant1->address = new \App\BusProNet\Model\Address(); $participant1->address->street = 'Test Street 1'; $participant1->address->postCode = '12345'; $participant1->address->city = 'Test City'; $participant1->address->country = 'DE'; $participant2 = $this->createAdultParticipant('duplicate@example.com'); $participant2->index = 1; $participant2->mutable = false; $bookingDto->participants = [$participant1, $participant2]; $wrapper = new ParticipantEditDto( participant: $bookingDto->participants[0], bookingContext: $bookingDto, ); $violations = $this->validator->validate($wrapper, null, ['booking_edit']); // Should have uniqueness violation (edit mode with immutable applicant = strict validation) $this->assertCount(1, $violations); } public function testMixedScenario(): 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 ]); // Validate participant at index 0 - should pass (unique) $wrapper0 = new ParticipantEditDto($bookingDto->participants[0], $bookingDto); $violations0 = $this->validator->validate($wrapper0, null, ['booking_create']); $this->assertCount(0, $violations0); // Validate participant at index 1 - should fail (duplicate) $wrapper1 = new ParticipantEditDto($bookingDto->participants[1], $bookingDto); $violations1 = $this->validator->validate($wrapper1, null, ['booking_create']); $this->assertCount(1, $violations1); // Validate participant at index 2 - should pass (child exempt) $wrapper2 = new ParticipantEditDto($bookingDto->participants[2], $bookingDto); $violations2 = $this->validator->validate($wrapper2, null, ['booking_create']); $this->assertCount(0, $violations2); // Validate participant at index 3 - should fail (duplicate) $wrapper3 = new ParticipantEditDto($bookingDto->participants[3], $bookingDto); $violations3 = $this->validator->validate($wrapper3, null, ['booking_create']); $this->assertCount(1, $violations3); // Validate participant at index 4 - should pass (unique child) $wrapper4 = new ParticipantEditDto($bookingDto->participants[4], $bookingDto); $violations4 = $this->validator->validate($wrapper4, null, ['booking_create']); $this->assertCount(0, $violations4); // Validate participant at index 5 - should pass (unique) $wrapper5 = new ParticipantEditDto($bookingDto->participants[5], $bookingDto); $violations5 = $this->validator->validate($wrapper5, null, ['booking_create']); $this->assertCount(0, $violations5); } 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); // hotelId must be int // Set index for each participant foreach ($participants as $index => $participant) { $participant->index = $index; // First participant (applicant) needs address and mobile if (0 === $index) { $participant->mobile = '+49 123 456789'; if (null === $participant->address) { $participant->address = new \App\Form\Model\AddressDto(); } $participant->address->street = 'Test Street 1'; $participant->address->postCode = '12345'; $participant->address->city = 'Test City'; $participant->address->country = 'DE'; } } $bookingDto->participants = $participants; return $bookingDto; } private function createAdultParticipant(string $email): ParticipantDto { $participant = $this->createValidParticipant( new \DateTimeImmutable('1990-01-01'), // Adult (over 18) $email ); // Add required fields for full validation $participant->assignedRoomId = 1; $participant->skiPass = $this->createMockService(); $participant->transportationOutbound = $this->createMockService(); $participant->transportationInbound = $this->createMockService(); $participant->insurance = $this->createMockInsurance(); return $participant; } private function createValidAdultParticipant(string $email): ParticipantDto { // Same as createAdultParticipant - all adults need full validation return $this->createAdultParticipant($email); } private function createChildParticipant(string $email): ParticipantDto { $participant = $this->createValidParticipant( new \DateTimeImmutable('2015-01-01'), // Child (under 16) $email ); // Children also need transportation (required for all participants) $participant->assignedRoomId = 1; $participant->skiPass = $this->createMockService(); $participant->transportationOutbound = $this->createMockService(); $participant->transportationInbound = $this->createMockService(); $participant->insurance = $this->createMockInsurance(); return $participant; } private function createValidParticipant(\DateTimeImmutable $dateOfBirth, string $email): ParticipantDto { $participant = new ParticipantDto(); $participant->firstName = 'John'; $participant->lastName = 'Doe'; $participant->dateOfBirth = $dateOfBirth; $participant->email = $email; 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; } private function createMockInsurance(): \App\BusProNet\Model\Insurance { $insurance = new \App\BusProNet\Model\Insurance(); $insurance->id = '1'; $insurance->label = 'Test Insurance'; $insurance->price = 10.0; return $insurance; } }