memberMapper = $this->createMock(MemberMapper::class); $this->addressMapper = $this->createMock(AddressMapper::class); $this->phoneMapper = $this->createMock(PhoneMapper::class); $this->emailMapper = $this->createMock(EmailMapper::class); $this->familyMapper = $this->createMock(FamilyMapper::class); $this->stufeHistoryMapper = $this->createMock(StufeHistoryMapper::class); $this->auditService = $this->createMock(AuditService::class); $this->logger = $this->createMock(LoggerInterface::class); $this->db = $this->createMock(IDBConnection::class); $this->service = new MemberService( $this->memberMapper, $this->addressMapper, $this->phoneMapper, $this->emailMapper, $this->familyMapper, $this->stufeHistoryMapper, $this->auditService, $this->logger, null, $this->db ); // Default: no duplicates found $this->memberMapper->method('search')->willReturn([]); } // ── Helper ────────────────────────────────────────────────────── private function validData(): array { return [ 'vorname' => 'Max', 'nachname' => 'Mustermann', 'geburtsdatum' => '2010-06-15', 'eintritt' => '2020-09-01', 'rolle' => 'mitglied', ]; } // ── Required fields ───────────────────────────────────────────── public function testMissingVornameThrowsValidationException(): void { $data = $this->validData(); unset($data['vorname']); $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/vorname/'); $this->service->create($data); } public function testMissingNachnameThrowsValidationException(): void { $data = $this->validData(); unset($data['nachname']); $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/nachname/'); $this->service->create($data); } public function testMissingGeburtsdatumIsAllowed(): void { $data = $this->validData(); unset($data['geburtsdatum']); // geburtsdatum is optional since Issue #162 — no exception expected $this->memberMapper->method('search')->willReturn([]); $this->memberMapper->method('insert')->willReturnCallback(function (Member $m) { $m->setId(1); return $m; }); $this->addressMapper->method('findByMemberId')->willReturn([]); $this->phoneMapper->method('findByMemberId')->willReturn([]); $this->emailMapper->method('findByMemberId')->willReturn([]); $result = $this->service->create($data); $this->assertSame(1, $result['id']); } public function testMissingEintrittThrowsValidationException(): void { $data = $this->validData(); unset($data['eintritt']); $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/eintritt/'); $this->service->create($data); } public function testEmptyVornameThrowsValidationException(): void { $data = $this->validData(); $data['vorname'] = ''; $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/vorname/'); $this->service->create($data); } public function testWhitespaceOnlyVornameThrowsValidationException(): void { $data = $this->validData(); $data['vorname'] = ' '; $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/vorname/'); $this->service->create($data); } public function testMultipleMissingFieldsListedInError(): void { $data = []; try { $this->service->create($data); $this->fail('Expected ValidationException'); } catch (ValidationException $e) { $message = $e->getMessage(); $this->assertStringContainsString('vorname', $message); $this->assertStringContainsString('nachname', $message); $this->assertStringContainsString('eintritt', $message); // geburtsdatum is optional since Issue #162 $this->assertStringNotContainsString('geburtsdatum', $message); } } // ── Date plausibility: birthday in future ─────────────────────── public function testBirthdayInFutureThrowsValidationException(): void { $data = $this->validData(); $data['geburtsdatum'] = '2030-01-01'; $data['eintritt'] = '2030-09-01'; $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/Zukunft/'); $this->service->create($data); } // ── Date plausibility: birthday > 120 years ago ───────────────── public function testBirthdayMoreThan120YearsAgoThrowsValidationException(): void { $data = $this->validData(); $data['geburtsdatum'] = '1850-01-01'; $data['eintritt'] = '1870-01-01'; $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/120/'); $this->service->create($data); } // ── Date plausibility: eintritt before birthday ───────────────── public function testEintrittBeforeBirthdayThrowsValidationException(): void { $data = $this->validData(); $data['geburtsdatum'] = '2010-06-15'; $data['eintritt'] = '2005-09-01'; $this->expectException(ValidationException::class); $this->expectExceptionMessageMatches('/Eintrittsdatum.*Geburtsdatum/'); $this->service->create($data); } // ── Date plausibility: valid dates pass ───────────────────────── public function testValidDatesPassValidation(): void { $data = $this->validData(); $insertedMember = new Member(); $insertedMember->setId(1); $insertedMember->setVorname('Max'); $insertedMember->setNachname('Mustermann'); $insertedMember->setGeburtsdatum('2010-06-15'); $insertedMember->setEintritt('2020-09-01'); $insertedMember->setRolle('mitglied'); $insertedMember->setStatus('aktiv'); $insertedMember->setCreatedAt('2026-01-01 00:00:00'); $insertedMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper->method('insert')->willReturn($insertedMember); $this->addressMapper->method('findByMemberId')->willReturn([]); $this->phoneMapper->method('findByMemberId')->willReturn([]); $this->emailMapper->method('findByMemberId')->willReturn([]); $result = $this->service->create($data); $this->assertSame('Max', $result['vorname']); $this->assertSame('Mustermann', $result['nachname']); } // ── Duplicate detection ───────────────────────────────────────── public function testDuplicateDetectionByNameAndBirthdayThrows(): void { $data = $this->validData(); $existingMember = new Member(); $existingMember->setId(42); $existingMember->setVorname('Max'); $existingMember->setNachname('Mustermann'); $existingMember->setGeburtsdatum('2010-06-15'); $existingMember->setEintritt('2020-09-01'); $existingMember->setRolle('mitglied'); $existingMember->setStatus('aktiv'); $existingMember->setCreatedAt('2026-01-01 00:00:00'); $existingMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper = $this->createMock(MemberMapper::class); $this->memberMapper->method('search')->willReturn([$existingMember]); // Rebuild service with new mapper $this->service = new MemberService( $this->memberMapper, $this->addressMapper, $this->phoneMapper, $this->emailMapper, $this->familyMapper, $this->stufeHistoryMapper, $this->auditService, $this->logger, null, $this->db ); $this->expectException(DuplicateMemberException::class); $this->expectExceptionMessageMatches('/ID: 42/'); $this->service->create($data); } public function testDuplicateDetectionCaseInsensitive(): void { $data = $this->validData(); $data['vorname'] = 'MAX'; $data['nachname'] = 'mustermann'; $existingMember = new Member(); $existingMember->setId(42); $existingMember->setVorname('max'); $existingMember->setNachname('Mustermann'); $existingMember->setGeburtsdatum('2010-06-15'); $existingMember->setEintritt('2020-09-01'); $existingMember->setRolle('mitglied'); $existingMember->setStatus('aktiv'); $existingMember->setCreatedAt('2026-01-01 00:00:00'); $existingMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper = $this->createMock(MemberMapper::class); $this->memberMapper->method('search')->willReturn([$existingMember]); $this->service = new MemberService( $this->memberMapper, $this->addressMapper, $this->phoneMapper, $this->emailMapper, $this->familyMapper, $this->stufeHistoryMapper, $this->auditService, $this->logger, null, $this->db ); $this->expectException(DuplicateMemberException::class); $this->service->create($data); } public function testSameNameDifferentBirthdayNoDuplicate(): void { $data = $this->validData(); $existingMember = new Member(); $existingMember->setId(42); $existingMember->setVorname('Max'); $existingMember->setNachname('Mustermann'); $existingMember->setGeburtsdatum('2012-03-20'); // Different birthday $existingMember->setEintritt('2020-09-01'); $existingMember->setRolle('mitglied'); $existingMember->setStatus('aktiv'); $existingMember->setCreatedAt('2026-01-01 00:00:00'); $existingMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper = $this->createMock(MemberMapper::class); $this->memberMapper->method('search')->willReturn([$existingMember]); $insertedMember = new Member(); $insertedMember->setId(43); $insertedMember->setVorname('Max'); $insertedMember->setNachname('Mustermann'); $insertedMember->setGeburtsdatum('2010-06-15'); $insertedMember->setEintritt('2020-09-01'); $insertedMember->setRolle('mitglied'); $insertedMember->setStatus('aktiv'); $insertedMember->setCreatedAt('2026-01-01 00:00:00'); $insertedMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper->method('insert')->willReturn($insertedMember); $this->service = new MemberService( $this->memberMapper, $this->addressMapper, $this->phoneMapper, $this->emailMapper, $this->familyMapper, $this->stufeHistoryMapper, $this->auditService, $this->logger, null, $this->db ); $this->addressMapper->method('findByMemberId')->willReturn([]); $this->phoneMapper->method('findByMemberId')->willReturn([]); $this->emailMapper->method('findByMemberId')->willReturn([]); // Should not throw — different birthday means not a duplicate $result = $this->service->create($data); $this->assertSame(43, $result['id']); } public function testDuplicateDetectionTrimsWhitespace(): void { $data = $this->validData(); $data['vorname'] = ' Max '; $data['nachname'] = ' Mustermann '; $existingMember = new Member(); $existingMember->setId(42); $existingMember->setVorname('Max'); $existingMember->setNachname('Mustermann'); $existingMember->setGeburtsdatum('2010-06-15'); $existingMember->setEintritt('2020-09-01'); $existingMember->setRolle('mitglied'); $existingMember->setStatus('aktiv'); $existingMember->setCreatedAt('2026-01-01 00:00:00'); $existingMember->setUpdatedAt('2026-01-01 00:00:00'); $this->memberMapper = $this->createMock(MemberMapper::class); $this->memberMapper->method('search')->willReturn([$existingMember]); $this->service = new MemberService( $this->memberMapper, $this->addressMapper, $this->phoneMapper, $this->emailMapper, $this->familyMapper, $this->stufeHistoryMapper, $this->auditService, $this->logger, null, $this->db ); $this->expectException(DuplicateMemberException::class); $this->service->create($data); } }