c4f5f8e7fb
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 38s
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 6s
1749 lines
69 KiB
PHP
1749 lines
69 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Mitgliederverwaltung\Tests\Unit;
|
|
|
|
use OCA\Mitgliederverwaltung\Db\Address;
|
|
use OCA\Mitgliederverwaltung\Db\AddressMapper;
|
|
use OCA\Mitgliederverwaltung\Db\Email;
|
|
use OCA\Mitgliederverwaltung\Db\EmailMapper;
|
|
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
|
|
use OCA\Mitgliederverwaltung\Db\Member;
|
|
use OCA\Mitgliederverwaltung\Db\MemberMapper;
|
|
use OCA\Mitgliederverwaltung\Db\Phone;
|
|
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
|
|
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
|
|
use OCA\Mitgliederverwaltung\Service\AuditService;
|
|
use OCA\Mitgliederverwaltung\Service\DuplicateMemberException;
|
|
use OCA\Mitgliederverwaltung\Service\EncryptionService;
|
|
use OCA\Mitgliederverwaltung\Service\MemberService;
|
|
use OCA\Mitgliederverwaltung\Service\ValidationException;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\IDBConnection;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class MemberServiceTest extends TestCase {
|
|
|
|
private MemberService $service;
|
|
private MemberMapper&MockObject $memberMapper;
|
|
private MockObject $addressMapper;
|
|
private MockObject $phoneMapper;
|
|
private MockObject $emailMapper;
|
|
private FamilyMapper&MockObject $familyMapper;
|
|
private StufeHistoryMapper&MockObject $stufeHistoryMapper;
|
|
private AuditService&MockObject $auditService;
|
|
private LoggerInterface&MockObject $logger;
|
|
private IDBConnection&MockObject $db;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->memberMapper = $this->createMock(MemberMapper::class);
|
|
$this->addressMapper = $this->getMockBuilder(AddressMapper::class)
|
|
->disableOriginalConstructor()
|
|
->onlyMethods(['findAll', 'findByMemberId', 'deleteByMemberId', 'insert', 'update', 'delete'])
|
|
->addMethods(['find'])
|
|
->getMock();
|
|
$this->phoneMapper = $this->getMockBuilder(PhoneMapper::class)
|
|
->disableOriginalConstructor()
|
|
->onlyMethods(['findAll', 'findByMemberId', 'deleteByMemberId', 'insert', 'update', 'delete'])
|
|
->addMethods(['find'])
|
|
->getMock();
|
|
$this->emailMapper = $this->getMockBuilder(EmailMapper::class)
|
|
->disableOriginalConstructor()
|
|
->onlyMethods(['findAll', 'findByMemberId', 'deleteByMemberId', 'insert', 'update', 'delete'])
|
|
->addMethods(['find'])
|
|
->getMock();
|
|
$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
|
|
);
|
|
}
|
|
|
|
// ── Helper factories ────────────────────────────────────────────
|
|
|
|
private function createMember(
|
|
int $id = 1,
|
|
string $vorname = 'Max',
|
|
string $nachname = 'Mustermann',
|
|
string $geburtsdatum = '2010-01-15',
|
|
string $eintritt = '2020-01-01',
|
|
string $status = 'aktiv',
|
|
string $rolle = 'mitglied',
|
|
?int $familyId = null
|
|
): Member {
|
|
$member = new Member();
|
|
$member->setId($id);
|
|
$member->setVorname($vorname);
|
|
$member->setNachname($nachname);
|
|
$member->setGeburtsdatum($geburtsdatum);
|
|
$member->setEintritt($eintritt);
|
|
$member->setStatus($status);
|
|
$member->setRolle($rolle);
|
|
$member->setFamilyId($familyId);
|
|
$member->setCreatedAt('2020-01-01 00:00:00');
|
|
$member->setUpdatedAt('2020-01-01 00:00:00');
|
|
return $member;
|
|
}
|
|
|
|
private function createAddress(int $id = 1, int $memberId = 1): Address {
|
|
$address = new Address();
|
|
$address->setId($id);
|
|
$address->setMemberId($memberId);
|
|
$address->setStrasse('Musterstrasse 1');
|
|
$address->setPlz('12345');
|
|
$address->setOrt('Musterstadt');
|
|
$address->setLand('Deutschland');
|
|
$address->setIsPrimary(true);
|
|
return $address;
|
|
}
|
|
|
|
private function createPhone(int $id = 1, int $memberId = 1): Phone {
|
|
$phone = new Phone();
|
|
$phone->setId($id);
|
|
$phone->setMemberId($memberId);
|
|
$phone->setNumberE164('+4917612345678');
|
|
$phone->setLabel('Mobil');
|
|
return $phone;
|
|
}
|
|
|
|
private function createEmail(int $id = 1, int $memberId = 1): Email {
|
|
$email = new Email();
|
|
$email->setId($id);
|
|
$email->setMemberId($memberId);
|
|
$email->setEmail('max@example.com');
|
|
$email->setLabel('Privat');
|
|
return $email;
|
|
}
|
|
|
|
private function getValidMemberData(): array {
|
|
return [
|
|
'vorname' => 'Max',
|
|
'nachname' => 'Mustermann',
|
|
'geburtsdatum' => '2010-01-15',
|
|
'eintritt' => '2020-01-01',
|
|
];
|
|
}
|
|
|
|
private function mockSubEntitiesForMember(int $memberId): void {
|
|
$this->addressMapper->method('findByMemberId')
|
|
->with($memberId)
|
|
->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')
|
|
->with($memberId)
|
|
->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')
|
|
->with($memberId)
|
|
->willReturn([]);
|
|
}
|
|
|
|
// ── find() ──────────────────────────────────────────────────────
|
|
|
|
public function testFindReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1);
|
|
$address = $this->createAddress(1, 1);
|
|
$phone = $this->createPhone(1, 1);
|
|
$email = $this->createEmail(1, 1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->addressMapper->method('findByMemberId')->with(1)->willReturn([$address]);
|
|
$this->phoneMapper->method('findByMemberId')->with(1)->willReturn([$phone]);
|
|
$this->emailMapper->method('findByMemberId')->with(1)->willReturn([$email]);
|
|
|
|
$result = $this->service->find(1);
|
|
|
|
$this->assertSame('Max', $result['vorname']);
|
|
$this->assertSame('Mustermann', $result['nachname']);
|
|
$this->assertCount(1, $result['addresses']);
|
|
$this->assertCount(1, $result['phones']);
|
|
$this->assertCount(1, $result['emails']);
|
|
}
|
|
|
|
public function testFindThrowsDoesNotExistException(): void {
|
|
$this->memberMapper->method('findById')
|
|
->willThrowException(new DoesNotExistException('Not found'));
|
|
|
|
$this->expectException(DoesNotExistException::class);
|
|
|
|
$this->service->find(999);
|
|
}
|
|
|
|
// ── findAll() ───────────────────────────────────────────────────
|
|
|
|
public function testFindAllReturnsList(): void {
|
|
$member1 = $this->createMember(1, 'Max');
|
|
$member2 = $this->createMember(2, 'Anna', 'Schmidt');
|
|
|
|
$this->memberMapper->method('findAllWithRelations')->willReturn([
|
|
$member1->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
$member2->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->findAll();
|
|
|
|
$this->assertCount(2, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
$this->assertSame('Anna', $result[1]['vorname']);
|
|
}
|
|
|
|
public function testFindAllWithPagination(): void {
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findAllWithRelations')
|
|
->with(10, 5)
|
|
->willReturn([]);
|
|
|
|
$this->service->findAll(10, 5);
|
|
}
|
|
|
|
public function testFindAllReturnsEmptyArray(): void {
|
|
$this->memberMapper->method('findAllWithRelations')->willReturn([]);
|
|
|
|
$result = $this->service->findAll();
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
// ── create() ────────────────────────────────────────────────────
|
|
|
|
public function testCreateValidMember(): void {
|
|
$data = $this->getValidMemberData();
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
|
|
$this->auditService->expects($this->once())->method('logCreate');
|
|
|
|
$result = $this->service->create($data);
|
|
|
|
$this->assertSame('Max', $result['vorname']);
|
|
$this->assertSame('Mustermann', $result['nachname']);
|
|
}
|
|
|
|
public function testCreateTrimsWhitespace(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['vorname'] = ' Max ';
|
|
$data['nachname'] = ' Mustermann ';
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('Max', $m->getVorname());
|
|
$this->assertSame('Mustermann', $m->getNachname());
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateSetsDefaults(): void {
|
|
$data = $this->getValidMemberData();
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('mitglied', $m->getRolle());
|
|
$this->assertSame('aktiv', $m->getStatus());
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateWithSubEntities(): void {
|
|
$data = $this->getValidMemberData();
|
|
$addresses = [['strasse' => 'Hauptstr. 1', 'plz' => '12345', 'ort' => 'Berlin']];
|
|
$emails = [['email' => 'test@example.com']];
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
|
|
$this->addressMapper->method('insert')
|
|
->willReturnCallback(function (Address $a) {
|
|
$a->setId(1);
|
|
return $a;
|
|
});
|
|
$this->emailMapper->method('insert')
|
|
->willReturnCallback(function (Email $e) {
|
|
$e->setId(1);
|
|
return $e;
|
|
});
|
|
|
|
$this->addressMapper->expects($this->once())->method('insert');
|
|
$this->emailMapper->expects($this->once())->method('insert');
|
|
|
|
$this->service->create($data, $addresses, [], $emails);
|
|
}
|
|
|
|
public function testCreateMissingVornameThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
unset($data['vorname']);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('vorname');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateMissingNachnameThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
unset($data['nachname']);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('nachname');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateWithoutGeburtsdatumSucceeds(): void {
|
|
$data = $this->getValidMemberData();
|
|
unset($data['geburtsdatum']);
|
|
|
|
$member = $this->createMember(1, geburtsdatum: '');
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')->willReturnCallback(function (Member $m) use ($member) {
|
|
$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 testCreateMissingEintrittThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
unset($data['eintritt']);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('eintritt');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateEmptyVornameThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['vorname'] = ' ';
|
|
|
|
$this->expectException(ValidationException::class);
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateFutureBirthdayThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['geburtsdatum'] = '2099-01-01';
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('Zukunft');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateVeryOldBirthdayThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['geburtsdatum'] = '1800-01-01';
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('120');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateEintrittBeforeBirthdayThrowsValidationException(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['geburtsdatum'] = '2010-06-15';
|
|
$data['eintritt'] = '2005-01-01';
|
|
|
|
$this->expectException(ValidationException::class);
|
|
$this->expectExceptionMessage('Eintrittsdatum');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateDuplicateThrowsDuplicateMemberException(): void {
|
|
$data = $this->getValidMemberData();
|
|
|
|
$existingMember = $this->createMember(99, 'Max', 'Mustermann', '2010-01-15');
|
|
|
|
$this->memberMapper->method('search')
|
|
->with('Mustermann')
|
|
->willReturn([$existingMember]);
|
|
|
|
$this->expectException(DuplicateMemberException::class);
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateDuplicateDetectionIsCaseInsensitive(): void {
|
|
$data = $this->getValidMemberData();
|
|
$data['vorname'] = 'max';
|
|
$data['nachname'] = 'mustermann';
|
|
|
|
$existingMember = $this->createMember(99, 'Max', 'Mustermann', '2010-01-15');
|
|
|
|
$this->memberMapper->method('search')
|
|
->willReturn([$existingMember]);
|
|
|
|
$this->expectException(DuplicateMemberException::class);
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateNoDuplicateWhenDifferentBirthday(): void {
|
|
$data = $this->getValidMemberData();
|
|
|
|
$existingMember = $this->createMember(99, 'Max', 'Mustermann', '2011-01-15');
|
|
|
|
$this->memberMapper->method('search')->willReturn([$existingMember]);
|
|
$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('Max', $result['vorname']);
|
|
}
|
|
|
|
// ── update() ────────────────────────────────────────────────────
|
|
|
|
public function testUpdatePartialFields(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$result = $this->service->update(1, ['notizen' => 'Test']);
|
|
|
|
$this->assertSame('Max', $result['vorname']);
|
|
}
|
|
|
|
public function testUpdateTrimsVorname(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('Moritz', $m->getVorname());
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->service->update(1, ['vorname' => ' Moritz ']);
|
|
}
|
|
|
|
public function testUpdateValidationTriggeredOnKeyFields(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
|
|
$this->service->update(1, ['vorname' => '']);
|
|
}
|
|
|
|
public function testUpdateLogsAudit(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->auditService->expects($this->once())->method('logUpdate');
|
|
|
|
$this->service->update(1, ['notizen' => 'New notes']);
|
|
}
|
|
|
|
public function testUpdateThrowsForMissingMember(): void {
|
|
$this->memberMapper->method('findById')
|
|
->willThrowException(new DoesNotExistException('Not found'));
|
|
|
|
$this->expectException(DoesNotExistException::class);
|
|
|
|
$this->service->update(999, ['vorname' => 'Test']);
|
|
}
|
|
|
|
public function testUpdateSetsMultipleFields(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('Moritz', $m->getVorname());
|
|
$this->assertSame('Schmidt', $m->getNachname());
|
|
$this->assertSame('inaktiv', $m->getStatus());
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->service->update(1, [
|
|
'vorname' => 'Moritz',
|
|
'nachname' => 'Schmidt',
|
|
'status' => 'inaktiv',
|
|
]);
|
|
}
|
|
|
|
public function testUpdateFiltersOutSystemFields(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('Moritz', $m->getVorname());
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
// Send editable fields plus system fields that should be ignored
|
|
$result = $this->service->update(1, [
|
|
'vorname' => 'Moritz',
|
|
'createdAt' => '1999-01-01 00:00:00',
|
|
'updatedAt' => '1999-01-01 00:00:00',
|
|
'deletedAt' => '1999-01-01 00:00:00',
|
|
'id' => 999,
|
|
]);
|
|
|
|
// createdAt should NOT have been changed to the request value
|
|
$this->assertSame('Moritz', $result['vorname']);
|
|
}
|
|
|
|
public function testUpdateAddsNewAddressWhenSyncingEmptyList(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnArgument(0);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$inserted = new Address();
|
|
$inserted->setStrasse('Musterstr. 1');
|
|
$inserted->setPlz('12345');
|
|
$inserted->setOrt('Berlin');
|
|
$inserted->setLand('Deutschland');
|
|
$inserted->setMemberId(1);
|
|
$inserted->setIsPrimary(false);
|
|
|
|
$this->addressMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturn($inserted);
|
|
$this->addressMapper->expects($this->never())->method('update');
|
|
$this->addressMapper->expects($this->never())->method('delete');
|
|
|
|
$result = $this->service->update(1, [
|
|
'vorname' => 'Moritz',
|
|
'addresses' => [
|
|
['strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland'],
|
|
],
|
|
]);
|
|
|
|
$this->assertSame('Moritz', $result['vorname']);
|
|
}
|
|
|
|
public function testUpdateUpdatesExistingAddressById(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$existing = new Address();
|
|
$existing->setId(42);
|
|
$existing->setMemberId(1);
|
|
$existing->setStrasse('Alt');
|
|
$existing->setPlz('00000');
|
|
$existing->setOrt('Alt');
|
|
$existing->setLand('Deutschland');
|
|
$existing->setIsPrimary(false);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnArgument(0);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([$existing]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->addressMapper->method('find')->with(42)->willReturn($existing);
|
|
$this->addressMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnArgument(0);
|
|
$this->addressMapper->expects($this->never())->method('insert');
|
|
$this->addressMapper->expects($this->never())->method('delete');
|
|
|
|
$this->service->update(1, [
|
|
'addresses' => [
|
|
['id' => 42, 'strasse' => 'Neu 1', 'plz' => '12345', 'ort' => 'Neu'],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function testUpdateDeletesAddressesNotInPayload(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$keep = new Address();
|
|
$keep->setId(42);
|
|
$keep->setMemberId(1);
|
|
$keep->setStrasse('Keep');
|
|
$keep->setPlz('11111');
|
|
$keep->setOrt('Keep');
|
|
$keep->setLand('Deutschland');
|
|
$keep->setIsPrimary(false);
|
|
|
|
$remove = new Address();
|
|
$remove->setId(43);
|
|
$remove->setMemberId(1);
|
|
$remove->setStrasse('Remove');
|
|
$remove->setPlz('22222');
|
|
$remove->setOrt('Remove');
|
|
$remove->setLand('Deutschland');
|
|
$remove->setIsPrimary(false);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnArgument(0);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([$keep, $remove]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->addressMapper->method('find')->willReturnMap([
|
|
[42, $keep],
|
|
[43, $remove],
|
|
]);
|
|
$this->addressMapper->expects($this->once())->method('update')->willReturnArgument(0);
|
|
$this->addressMapper->expects($this->once())->method('delete')->with($remove);
|
|
$this->addressMapper->expects($this->never())->method('insert');
|
|
|
|
$this->service->update(1, [
|
|
'addresses' => [
|
|
['id' => 42, 'strasse' => 'Keep', 'plz' => '11111', 'ort' => 'Keep'],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function testUpdateWithoutSubEntitiesKeysDoesNotTouchAddresses(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnArgument(0);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
// Only mapper calls allowed: no insert/update/delete on sub-entity mappers
|
|
$this->addressMapper->expects($this->never())->method('insert');
|
|
$this->addressMapper->expects($this->never())->method('update');
|
|
$this->addressMapper->expects($this->never())->method('delete');
|
|
$this->phoneMapper->expects($this->never())->method('insert');
|
|
$this->phoneMapper->expects($this->never())->method('update');
|
|
$this->phoneMapper->expects($this->never())->method('delete');
|
|
$this->emailMapper->expects($this->never())->method('insert');
|
|
$this->emailMapper->expects($this->never())->method('update');
|
|
$this->emailMapper->expects($this->never())->method('delete');
|
|
|
|
$this->service->update(1, ['vorname' => 'Moritz']);
|
|
}
|
|
|
|
// ── revealAllAllergies ─────────────────────────────────────────────
|
|
|
|
public function testRevealAllAllergiesDecryptsEveryMember(): void {
|
|
$m1 = $this->createMember(1, 'A', 'Aa');
|
|
$m1->setAllergienEncrypted('CT1');
|
|
$m2 = $this->createMember(2, 'B', 'Bb');
|
|
$m2->setAllergienEncrypted(null);
|
|
$m3 = $this->createMember(3, 'C', 'Cc');
|
|
$m3->setAllergienEncrypted('CT3');
|
|
|
|
$this->memberMapper->method('findAll')->willReturn([$m1, $m2, $m3]);
|
|
|
|
$encryption = $this->createMock(EncryptionService::class);
|
|
$encryption->method('decrypt')->willReturnMap([
|
|
['CT1', 'Nussallergie'],
|
|
['CT3', 'Laktose'],
|
|
]);
|
|
|
|
$service = new MemberService(
|
|
$this->memberMapper,
|
|
$this->addressMapper,
|
|
$this->phoneMapper,
|
|
$this->emailMapper,
|
|
$this->familyMapper,
|
|
$this->stufeHistoryMapper,
|
|
$this->auditService,
|
|
$this->logger,
|
|
$encryption,
|
|
$this->db
|
|
);
|
|
|
|
$map = $service->revealAllAllergies('alice');
|
|
|
|
$this->assertSame('Nussallergie', $map[1]);
|
|
$this->assertNull($map[2]);
|
|
$this->assertSame('Laktose', $map[3]);
|
|
}
|
|
|
|
public function testRevealAllAllergiesLogsCountsNotValues(): void {
|
|
$m1 = $this->createMember(1);
|
|
$m1->setAllergienEncrypted('CT');
|
|
|
|
$this->memberMapper->method('findAll')->willReturn([$m1]);
|
|
|
|
$encryption = $this->createMock(EncryptionService::class);
|
|
$encryption->method('decrypt')->willReturn('Nuss');
|
|
|
|
$this->logger->expects($this->once())
|
|
->method('info')
|
|
->with(
|
|
$this->stringContains('Bulk-Reveal'),
|
|
$this->callback(function ($ctx) {
|
|
$flat = json_encode($ctx);
|
|
return !str_contains($flat, 'Nuss');
|
|
})
|
|
);
|
|
|
|
$service = new MemberService(
|
|
$this->memberMapper,
|
|
$this->addressMapper,
|
|
$this->phoneMapper,
|
|
$this->emailMapper,
|
|
$this->familyMapper,
|
|
$this->stufeHistoryMapper,
|
|
$this->auditService,
|
|
$this->logger,
|
|
$encryption,
|
|
$this->db
|
|
);
|
|
|
|
$service->revealAllAllergies('alice');
|
|
}
|
|
|
|
public function testRevealAllAllergiesThrowsWithoutEncryptionService(): void {
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->service->revealAllAllergies('alice');
|
|
}
|
|
|
|
public function testUpdateImportedMemberSucceeds(): void {
|
|
// Simulate a member created by import (has all fields set via import path)
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', '2020-01-01');
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('Max Updated', $m->getVorname());
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
// Update with same payload structure the frontend sends
|
|
$result = $this->service->update(1, [
|
|
'vorname' => 'Max Updated',
|
|
'nachname' => 'Mustermann',
|
|
'geburtsdatum' => '2010-01-15',
|
|
'eintritt' => '2020-01-01',
|
|
'geschlecht' => null,
|
|
'rolle' => 'mitglied',
|
|
'stufeId' => null,
|
|
'austritt' => null,
|
|
'status' => 'aktiv',
|
|
'allergienEncrypted' => null,
|
|
'notizen' => null,
|
|
'zusatzNotizen' => null,
|
|
'kvTyp' => null,
|
|
'kvName' => null,
|
|
'familyId' => null,
|
|
'frozenFeeRate' => null,
|
|
'einwilligungDatum' => null,
|
|
]);
|
|
|
|
$this->assertSame('Max Updated', $result['vorname']);
|
|
}
|
|
|
|
// ── softDelete() ────────────────────────────────────────────────
|
|
|
|
public function testSoftDeleteSetsStatusGeloescht(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('geloescht', $m->getStatus());
|
|
$this->assertNotNull($m->getDeletedAt());
|
|
return $m;
|
|
});
|
|
|
|
$this->addressMapper->expects($this->once())->method('deleteByMemberId')->with(1);
|
|
$this->phoneMapper->expects($this->once())->method('deleteByMemberId')->with(1);
|
|
$this->emailMapper->expects($this->once())->method('deleteByMemberId')->with(1);
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeletePurgesSensitiveFields(): void {
|
|
$member = $this->createMember(1);
|
|
$member->setAllergienEncrypted('ENC:sensitive');
|
|
$member->setNotizen('Some notes');
|
|
$member->setZusatzNotizen('Extra notes');
|
|
$member->setKvTyp('gesetzlich');
|
|
$member->setKvName('AOK');
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertNull($m->getAllergienEncrypted());
|
|
$this->assertNull($m->getNotizen());
|
|
$this->assertNull($m->getZusatzNotizen());
|
|
$this->assertNull($m->getKvTyp());
|
|
$this->assertNull($m->getKvName());
|
|
$this->assertNull($m->getFamilyId());
|
|
$this->assertNull($m->getFrozenFeeRate());
|
|
return $m;
|
|
});
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeleteSetsAustrittIfNotSet(): void {
|
|
$member = $this->createMember(1);
|
|
// austritt is null by default
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertNotNull($m->getAustritt());
|
|
return $m;
|
|
});
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeleteKeepsExistingAustritt(): void {
|
|
$member = $this->createMember(1);
|
|
$member->setAustritt('2025-06-15');
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(function (Member $m) {
|
|
$this->assertSame('2025-06-15', $m->getAustritt());
|
|
return $m;
|
|
});
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeleteLogsAudit(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
|
|
$this->auditService->expects($this->once())->method('logSoftDelete')->with('member', 1);
|
|
// Also logs the purge data via logCreate
|
|
$this->auditService->expects($this->once())->method('logCreate');
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeleteLogsToLogger(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
|
|
$this->logger->expects($this->once())->method('info');
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testDeleteDelegatesToSoftDelete(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->expects($this->once())->method('update');
|
|
|
|
$this->service->delete(1);
|
|
}
|
|
|
|
// ── search() ────────────────────────────────────────────────────
|
|
|
|
public function testSearchReturnsMatchingMembers(): void {
|
|
$member = $this->createMember(1);
|
|
$this->memberMapper->method('searchWithRelations')->with('Max')->willReturn([
|
|
$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->search('Max');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
}
|
|
|
|
public function testSearchReturnsEmptyForNoMatch(): void {
|
|
$this->memberMapper->method('searchWithRelations')->willReturn([]);
|
|
|
|
$result = $this->service->search('Nonexistent');
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
// ── fullTextSearch() ────────────────────────────────────────────
|
|
|
|
public function testFullTextSearchReturnsResultsWithMatchContext(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('fullTextSearchWithRelations')
|
|
->with('Max', 20)
|
|
->willReturn([$memberData]);
|
|
|
|
$result = $this->service->fullTextSearch('Max');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertArrayHasKey('matchContext', $result[0]);
|
|
$this->assertStringContainsString('Name:', $result[0]['matchContext']);
|
|
}
|
|
|
|
public function testFullTextSearchMatchesAddress(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstrasse 1', 'plz' => '12345', 'ort' => 'Musterstadt', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('fullTextSearchWithRelations')
|
|
->willReturn([$memberData]);
|
|
|
|
$result = $this->service->fullTextSearch('Musterstrasse');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertStringContainsString('Adresse:', $result[0]['matchContext']);
|
|
}
|
|
|
|
public function testFullTextSearchMatchesNotizen(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$member->setNotizen('Hat einen besonderen Wunsch fuer Training');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('fullTextSearchWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->fullTextSearch('Training');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertStringContainsString('Notizen:', $result[0]['matchContext']);
|
|
}
|
|
|
|
public function testFullTextSearchMatchesZusatzNotizen(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$member->setZusatzNotizen('Zusaetzliche Information');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('fullTextSearchWithRelations')->willReturn([$memberData]);
|
|
|
|
// Search for something not in name or notizen, but in zusatzNotizen
|
|
$result = $this->service->fullTextSearch('zusaetzliche');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertStringContainsString('Zusatz-Notizen', $result[0]['matchContext']);
|
|
}
|
|
|
|
// ── findByBirthdayThisMonth() ───────────────────────────────────
|
|
|
|
public function testFindByBirthdayThisMonthCallsMapperWithCurrentMonth(): void {
|
|
$currentMonth = (int)(new \DateTime())->format('m');
|
|
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findByBirthdayMonthWithRelations')
|
|
->with($currentMonth)
|
|
->willReturn([]);
|
|
|
|
$this->service->findByBirthdayThisMonth();
|
|
}
|
|
|
|
public function testFindByBirthdayThisMonthReturnsMembers(): void {
|
|
$member = $this->createMember(1);
|
|
$this->memberMapper->method('findByBirthdayMonthWithRelations')->willReturn([
|
|
$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->findByBirthdayThisMonth();
|
|
|
|
$this->assertCount(1, $result);
|
|
}
|
|
|
|
// ── findWithUnpaidFees() ────────────────────────────────────────
|
|
|
|
public function testFindWithUnpaidFeesCallsMapperWithCurrentYear(): void {
|
|
$currentYear = (int)(new \DateTime())->format('Y');
|
|
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findWithUnpaidFeesWithRelations')
|
|
->with($currentYear)
|
|
->willReturn([]);
|
|
|
|
$this->service->findWithUnpaidFees();
|
|
}
|
|
|
|
public function testFindWithUnpaidFeesReturnsMembers(): void {
|
|
$member = $this->createMember(1);
|
|
$this->memberMapper->method('findWithUnpaidFeesWithRelations')->willReturn([
|
|
$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->findWithUnpaidFees();
|
|
|
|
$this->assertCount(1, $result);
|
|
}
|
|
|
|
// ── findFiltered() ────────────────────────────────────────────────
|
|
|
|
public function testFindFilteredDelegatesToMapper(): void {
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findFilteredWithRelations')
|
|
->with('aktiv', 'mitglied', true, false)
|
|
->willReturn([]);
|
|
|
|
$this->service->findFiltered('aktiv', 'mitglied', true, false);
|
|
}
|
|
|
|
public function testFindFilteredNoFilters(): void {
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findFilteredWithRelations')
|
|
->with(null, null, false, false)
|
|
->willReturn([]);
|
|
|
|
$result = $this->service->findFiltered();
|
|
$this->assertCount(0, $result);
|
|
}
|
|
|
|
public function testFindFilteredReturnsWithSubEntities(): void {
|
|
$member = $this->createMember(1);
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('findFilteredWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findFiltered('aktiv');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame(1, $result[0]['id']);
|
|
$this->assertArrayHasKey('addresses', $result[0]);
|
|
$this->assertCount(1, $result[0]['addresses']);
|
|
}
|
|
|
|
public function testFindFilteredByRolleOnly(): void {
|
|
$member = $this->createMember(1, rolle: 'erziehungsberechtigter');
|
|
$this->memberMapper->method('findFilteredWithRelations')
|
|
->with(null, 'erziehungsberechtigter', false, false)
|
|
->willReturn([$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []]]);
|
|
|
|
$result = $this->service->findFiltered(null, 'erziehungsberechtigter');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('erziehungsberechtigter', $result[0]['rolle']);
|
|
}
|
|
|
|
public function testFindFilteredCombinedAllParams(): void {
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findFilteredWithRelations')
|
|
->with('inaktiv', 'mitglied', true, true)
|
|
->willReturn([]);
|
|
|
|
$result = $this->service->findFiltered('inaktiv', 'mitglied', true, true);
|
|
$this->assertCount(0, $result);
|
|
}
|
|
|
|
// ── Sub-entity management: Addresses ────────────────────────────
|
|
|
|
public function testAddAddressCreatesAddress(): void {
|
|
$this->addressMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturnCallback(function (Address $a) {
|
|
$this->assertSame(1, $a->getMemberId());
|
|
$this->assertSame('Hauptstr. 1', $a->getStrasse());
|
|
$this->assertSame('12345', $a->getPlz());
|
|
$this->assertSame('Berlin', $a->getOrt());
|
|
$this->assertSame('Deutschland', $a->getLand());
|
|
$a->setId(1);
|
|
return $a;
|
|
});
|
|
|
|
$result = $this->service->addAddress(1, [
|
|
'strasse' => 'Hauptstr. 1',
|
|
'plz' => '12345',
|
|
'ort' => 'Berlin',
|
|
]);
|
|
|
|
$this->assertInstanceOf(Address::class, $result);
|
|
}
|
|
|
|
public function testUpdateAddressUpdatesFields(): void {
|
|
$address = $this->createAddress(1, 1);
|
|
|
|
$this->addressMapper->method('find')->with(1)->willReturn($address);
|
|
$this->addressMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function (Address $a) {
|
|
$this->assertSame('Neue Strasse 2', $a->getStrasse());
|
|
return $a;
|
|
});
|
|
|
|
$this->service->updateAddress(1, ['strasse' => 'Neue Strasse 2']);
|
|
}
|
|
|
|
public function testDeleteAddressDeletesAddress(): void {
|
|
$address = $this->createAddress(1, 1);
|
|
|
|
$this->addressMapper->method('find')->with(1)->willReturn($address);
|
|
$this->addressMapper->expects($this->once())->method('delete')->with($address);
|
|
|
|
$this->service->deleteAddress(1);
|
|
}
|
|
|
|
// ── Sub-entity management: Phones ───────────────────────────────
|
|
|
|
public function testAddPhoneValidatesAndNormalizesNumber(): void {
|
|
$this->phoneMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturnCallback(function (Phone $p) {
|
|
$this->assertSame('+4917612345678', $p->getNumberE164());
|
|
$p->setId(1);
|
|
return $p;
|
|
});
|
|
|
|
$result = $this->service->addPhone(1, [
|
|
'numberE164' => '017612345678',
|
|
'label' => 'Mobil',
|
|
]);
|
|
|
|
$this->assertInstanceOf(Phone::class, $result);
|
|
}
|
|
|
|
public function testAddPhoneInvalidNumberThrowsValidationException(): void {
|
|
$this->expectException(ValidationException::class);
|
|
|
|
$this->service->addPhone(1, ['numberE164' => 'invalid']);
|
|
}
|
|
|
|
public function testUpdatePhoneValidatesNumber(): void {
|
|
$phone = $this->createPhone(1, 1);
|
|
|
|
$this->phoneMapper->method('find')->with(1)->willReturn($phone);
|
|
$this->phoneMapper->method('update')->willReturnCallback(fn(Phone $p) => $p);
|
|
|
|
$result = $this->service->updatePhone(1, ['numberE164' => '+4917699999999']);
|
|
$this->assertInstanceOf(Phone::class, $result);
|
|
}
|
|
|
|
public function testUpdatePhoneInvalidNumberThrowsValidationException(): void {
|
|
$phone = $this->createPhone(1, 1);
|
|
|
|
$this->phoneMapper->method('find')->with(1)->willReturn($phone);
|
|
|
|
$this->expectException(ValidationException::class);
|
|
|
|
$this->service->updatePhone(1, ['numberE164' => 'abc']);
|
|
}
|
|
|
|
public function testDeletePhoneDeletesPhone(): void {
|
|
$phone = $this->createPhone(1, 1);
|
|
|
|
$this->phoneMapper->method('find')->with(1)->willReturn($phone);
|
|
$this->phoneMapper->expects($this->once())->method('delete')->with($phone);
|
|
|
|
$this->service->deletePhone(1);
|
|
}
|
|
|
|
// ── Sub-entity management: Emails ───────────────────────────────
|
|
|
|
public function testAddEmailCreatesEmail(): void {
|
|
$this->emailMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturnCallback(function (Email $e) {
|
|
$this->assertSame(1, $e->getMemberId());
|
|
$this->assertSame('test@example.com', $e->getEmail());
|
|
$e->setId(1);
|
|
return $e;
|
|
});
|
|
|
|
$result = $this->service->addEmail(1, ['email' => ' test@example.com ']);
|
|
|
|
$this->assertInstanceOf(Email::class, $result);
|
|
}
|
|
|
|
public function testUpdateEmailUpdatesFields(): void {
|
|
$email = $this->createEmail(1, 1);
|
|
|
|
$this->emailMapper->method('find')->with(1)->willReturn($email);
|
|
$this->emailMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function (Email $e) {
|
|
$this->assertSame('new@example.com', $e->getEmail());
|
|
return $e;
|
|
});
|
|
|
|
$this->service->updateEmail(1, ['email' => ' new@example.com ']);
|
|
}
|
|
|
|
public function testDeleteEmailDeletesEmail(): void {
|
|
$email = $this->createEmail(1, 1);
|
|
|
|
$this->emailMapper->method('find')->with(1)->willReturn($email);
|
|
$this->emailMapper->expects($this->once())->method('delete')->with($email);
|
|
|
|
$this->service->deleteEmail(1);
|
|
}
|
|
|
|
// ── countAll() ──────────────────────────────────────────────────
|
|
|
|
public function testCountAllDelegatesToMapper(): void {
|
|
$this->memberMapper->method('countAll')->willReturn(42);
|
|
|
|
$this->assertSame(42, $this->service->countAll());
|
|
}
|
|
|
|
// ── findByFamily() ──────────────────────────────────────────────
|
|
|
|
public function testFindByFamilyReturnsMembers(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', '2020-01-01', 'aktiv', 'mitglied', 5);
|
|
|
|
$this->memberMapper->method('findByFamilyWithRelations')->with(5)->willReturn([
|
|
$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->findByFamily(5);
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
}
|
|
|
|
// ── findByStatus() ──────────────────────────────────────────────
|
|
|
|
public function testFindByStatusReturnsMembers(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', '2020-01-01', 'aktiv');
|
|
|
|
$this->memberMapper->method('findByStatusWithRelations')->with('aktiv')->willReturn([
|
|
$member->jsonSerialize() + ['addresses' => [], 'phones' => [], 'emails' => []],
|
|
]);
|
|
|
|
$result = $this->service->findByStatus('aktiv');
|
|
|
|
$this->assertCount(1, $result);
|
|
}
|
|
|
|
// ── findArchived() ──────────────────────────────────────────────
|
|
|
|
public function testFindArchivedDelegatesToMapper(): void {
|
|
// Issue #202: findArchived() now delegates to MemberMapper::findArchived()
|
|
// which uses a SQL WHERE deleted_at IS NOT NULL query instead of
|
|
// loading all members into memory and filtering in PHP.
|
|
$deletedMember = $this->createMember(1, 'Anna', 'Schmidt');
|
|
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
|
|
$deletedMember->setStatus('geloescht');
|
|
$deletedMember->setEintritt('2018-01-01');
|
|
$deletedMember->setAustritt('2025-12-31');
|
|
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findArchived')
|
|
->with(null, null)
|
|
->willReturn([$deletedMember]);
|
|
|
|
$result = $this->service->findArchived();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Anna', $result[0]['vorname']);
|
|
$this->assertArrayHasKey('mitgliedsdauer', $result[0]);
|
|
$this->assertArrayHasKey('deletedAt', $result[0]);
|
|
}
|
|
|
|
public function testFindArchivedPassesPaginationToMapper(): void {
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findArchived')
|
|
->with(10, 20)
|
|
->willReturn([]);
|
|
|
|
$this->service->findArchived(10, 20);
|
|
}
|
|
|
|
public function testFindArchivedReturnsEmptyArrayWhenNoArchivedMembers(): void {
|
|
$this->memberMapper->method('findArchived')->willReturn([]);
|
|
|
|
$result = $this->service->findArchived();
|
|
$this->assertSame([], $result);
|
|
}
|
|
|
|
public function testFindArchivedReturnsOnlyRetainedFields(): void {
|
|
$deletedMember = $this->createMember(1, 'Anna', 'Schmidt');
|
|
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
|
|
$deletedMember->setEintritt('2018-01-01');
|
|
$deletedMember->setAustritt('2025-12-31');
|
|
|
|
$this->memberMapper->method('findArchived')->willReturn([$deletedMember]);
|
|
|
|
$result = $this->service->findArchived();
|
|
|
|
$this->assertCount(1, $result);
|
|
$entry = $result[0];
|
|
// Retained fields should be present
|
|
$this->assertArrayHasKey('id', $entry);
|
|
$this->assertArrayHasKey('vorname', $entry);
|
|
$this->assertArrayHasKey('nachname', $entry);
|
|
$this->assertArrayHasKey('geburtsdatum', $entry);
|
|
$this->assertArrayHasKey('eintritt', $entry);
|
|
$this->assertArrayHasKey('austritt', $entry);
|
|
$this->assertArrayHasKey('deletedAt', $entry);
|
|
$this->assertArrayHasKey('mitgliedsdauer', $entry);
|
|
// Sensitive fields should NOT be present
|
|
$this->assertArrayNotHasKey('notizen', $entry);
|
|
$this->assertArrayNotHasKey('allergienEncrypted', $entry);
|
|
$this->assertArrayNotHasKey('kvTyp', $entry);
|
|
}
|
|
|
|
// ── countArchived() ──────────────────────────────────────────────
|
|
|
|
public function testCountArchivedReturnsCorrectCount(): void {
|
|
// Issue #201: countArchived() now delegates to MemberMapper::countArchived()
|
|
// which uses a SQL COUNT query instead of loading all members into memory.
|
|
$this->memberMapper->method('countArchived')->willReturn(1);
|
|
|
|
$this->assertSame(1, $this->service->countArchived());
|
|
}
|
|
|
|
public function testCountArchivedReturnsZeroWhenNoArchived(): void {
|
|
$this->memberMapper->method('countArchived')->willReturn(0);
|
|
|
|
$this->assertSame(0, $this->service->countArchived());
|
|
}
|
|
|
|
// ── Joined methods: findAllWithRelations ──────────────────────
|
|
|
|
public function testFindAllWithRelationsReturnsMemberWithNestedSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [
|
|
['id' => 1, 'memberId' => 1, 'label' => 'Privat', 'strasse' => 'Hauptstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true],
|
|
['id' => 2, 'memberId' => 1, 'label' => 'Arbeit', 'strasse' => 'Büroweg 5', 'plz' => '12346', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => false],
|
|
],
|
|
'phones' => [
|
|
['id' => 1, 'memberId' => 1, 'label' => 'Mobil', 'numberE164' => '+4917612345678'],
|
|
],
|
|
'emails' => [
|
|
['id' => 1, 'memberId' => 1, 'label' => 'Privat', 'email' => 'max@example.com'],
|
|
],
|
|
];
|
|
|
|
$this->memberMapper->method('findAllWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findAll();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
$this->assertCount(2, $result[0]['addresses']);
|
|
$this->assertCount(1, $result[0]['phones']);
|
|
$this->assertCount(1, $result[0]['emails']);
|
|
$this->assertSame('Hauptstr. 1', $result[0]['addresses'][0]['strasse']);
|
|
$this->assertSame('+4917612345678', $result[0]['phones'][0]['numberE164']);
|
|
$this->assertSame('max@example.com', $result[0]['emails'][0]['email']);
|
|
}
|
|
|
|
public function testFindAllWithRelationsHandlesMemberWithoutSubEntities(): void {
|
|
$member = $this->createMember(1);
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('findAllWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findAll();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertCount(0, $result[0]['addresses']);
|
|
$this->assertCount(0, $result[0]['phones']);
|
|
$this->assertCount(0, $result[0]['emails']);
|
|
}
|
|
|
|
// ── Joined methods: searchWithRelations ───────────────────────
|
|
|
|
public function testSearchWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('searchWithRelations')->with('Max')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->search('Max');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
$this->assertCount(1, $result[0]['addresses']);
|
|
}
|
|
|
|
// ── Joined methods: findByFamilyWithRelations ─────────────────
|
|
|
|
public function testFindByFamilyWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', '2020-01-01', 'aktiv', 'mitglied', 5);
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [['id' => 1, 'memberId' => 1, 'label' => 'Mobil', 'numberE164' => '+4917612345678']],
|
|
'emails' => [['id' => 1, 'memberId' => 1, 'label' => 'Privat', 'email' => 'max@example.com']],
|
|
];
|
|
|
|
$this->memberMapper->method('findByFamilyWithRelations')->with(5)->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findByFamily(5);
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
$this->assertCount(1, $result[0]['addresses']);
|
|
$this->assertCount(1, $result[0]['phones']);
|
|
$this->assertCount(1, $result[0]['emails']);
|
|
}
|
|
|
|
// ── Joined methods: findByStatusWithRelations ─────────────────
|
|
|
|
public function testFindByStatusWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-01-15', '2020-01-01', 'aktiv');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('findByStatusWithRelations')->with('aktiv')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findByStatus('aktiv');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
}
|
|
|
|
// ── Joined methods: findByBirthdayMonthWithRelations ──────────
|
|
|
|
public function testFindByBirthdayMonthWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$currentMonth = (int)(new \DateTime())->format('m');
|
|
$member = $this->createMember(1, 'Max', 'Mustermann', '2010-06-15', '2020-01-01');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->expects($this->once())
|
|
->method('findByBirthdayMonthWithRelations')
|
|
->with($currentMonth)
|
|
->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findByBirthdayThisMonth();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
}
|
|
|
|
// ── Joined methods: findWithUnpaidFeesWithRelations ───────────
|
|
|
|
public function testFindWithUnpaidFeesWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('findWithUnpaidFeesWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findWithUnpaidFees();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
}
|
|
|
|
// ── Joined methods: findFilteredWithRelations ─────────────────
|
|
|
|
public function testFindFilteredWithRelationsReturnsMemberWithSubEntities(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('findFilteredWithRelations')
|
|
->with('aktiv', 'mitglied', false, false)
|
|
->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findFiltered('aktiv', 'mitglied');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertSame('Max', $result[0]['vorname']);
|
|
$this->assertCount(1, $result[0]['addresses']);
|
|
}
|
|
|
|
// ── Joined methods: fullTextSearchWithRelations ───────────────
|
|
|
|
public function testFullTextSearchWithRelationsReturnsResultsWithMatchContext(): void {
|
|
$member = $this->createMember(1, 'Max', 'Mustermann');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Musterstrasse 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [],
|
|
'emails' => [],
|
|
];
|
|
|
|
$this->memberMapper->method('fullTextSearchWithRelations')
|
|
->with('Musterstrasse', 20)
|
|
->willReturn([$memberData]);
|
|
|
|
$result = $this->service->fullTextSearch('Musterstrasse');
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertArrayHasKey('matchContext', $result[0]);
|
|
$this->assertStringContainsString('Adresse:', $result[0]['matchContext']);
|
|
$this->assertStringContainsString('Musterstrasse 1', $result[0]['matchContext']);
|
|
}
|
|
|
|
// ── Backward compatibility: findAll shape ─────────────────────
|
|
|
|
public function testFindAllReturnsSameShapeAsBefore(): void {
|
|
// The new method returns the same shape as the old one:
|
|
// an array of member arrays with addresses/phones/emails sub-arrays
|
|
$member = $this->createMember(1, 'Max');
|
|
$memberData = $member->jsonSerialize() + [
|
|
'addresses' => [['id' => 1, 'memberId' => 1, 'label' => null, 'strasse' => 'Hauptstr. 1', 'plz' => '12345', 'ort' => 'Berlin', 'land' => 'Deutschland', 'isPrimary' => true]],
|
|
'phones' => [['id' => 1, 'memberId' => 1, 'label' => 'Mobil', 'numberE164' => '+4917612345678']],
|
|
'emails' => [['id' => 1, 'memberId' => 1, 'label' => 'Privat', 'email' => 'max@example.com']],
|
|
];
|
|
|
|
$this->memberMapper->method('findAllWithRelations')->willReturn([$memberData]);
|
|
|
|
$result = $this->service->findAll();
|
|
|
|
// Verify the shape matches what the frontend expects
|
|
$this->assertIsArray($result[0]);
|
|
$this->assertArrayHasKey('id', $result[0]);
|
|
$this->assertArrayHasKey('vorname', $result[0]);
|
|
$this->assertArrayHasKey('nachname', $result[0]);
|
|
$this->assertArrayHasKey('addresses', $result[0]);
|
|
$this->assertArrayHasKey('phones', $result[0]);
|
|
$this->assertArrayHasKey('emails', $result[0]);
|
|
}
|
|
|
|
// ── Database Transactions ───────────────────────────────────────
|
|
|
|
public function testCreateCommitsTransactionOnSuccess(): void {
|
|
$data = $this->getValidMemberData();
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('commit');
|
|
$this->db->expects($this->never())->method('rollback');
|
|
|
|
$this->service->create($data);
|
|
}
|
|
|
|
public function testCreateRollbacksTransactionWhenSubEntityInsertFails(): void {
|
|
$data = $this->getValidMemberData();
|
|
$phones = [['numberE164' => '+4917612345678']];
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
$this->phoneMapper->method('insert')->willThrowException(new \RuntimeException('DB error'));
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('rollback');
|
|
$this->db->expects($this->never())->method('commit');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('DB error');
|
|
|
|
$this->service->create($data, [], $phones, []);
|
|
}
|
|
|
|
public function testCreateRollbacksTransactionWhenAddressInsertFails(): void {
|
|
$data = $this->getValidMemberData();
|
|
$addresses = [['strasse' => 'Hauptstr. 1', 'plz' => '12345', 'ort' => 'Berlin']];
|
|
|
|
$this->memberMapper->method('search')->willReturn([]);
|
|
$this->memberMapper->method('insert')
|
|
->willReturnCallback(function (Member $m) {
|
|
$m->setId(1);
|
|
return $m;
|
|
});
|
|
$this->addressMapper->method('insert')->willThrowException(new \RuntimeException('Constraint violation'));
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('rollback');
|
|
$this->db->expects($this->never())->method('commit');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->service->create($data, $addresses, [], []);
|
|
}
|
|
|
|
public function testSoftDeleteCommitsTransactionOnSuccess(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('commit');
|
|
$this->db->expects($this->never())->method('rollback');
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testSoftDeleteRollbacksTransactionWhenDeleteFails(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
$this->phoneMapper->method('deleteByMemberId')->willThrowException(new \RuntimeException('DB error'));
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('rollback');
|
|
$this->db->expects($this->never())->method('commit');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->service->softDelete(1);
|
|
}
|
|
|
|
public function testUpdateCommitsTransactionOnSuccess(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('commit');
|
|
$this->db->expects($this->never())->method('rollback');
|
|
|
|
$this->service->update(1, ['notizen' => 'Test']);
|
|
}
|
|
|
|
public function testUpdateRollbacksTransactionWhenSubEntitySyncFails(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$existing = new Address();
|
|
$existing->setId(42);
|
|
$existing->setMemberId(1);
|
|
$existing->setStrasse('Alt');
|
|
$existing->setPlz('00000');
|
|
$existing->setOrt('Alt');
|
|
$existing->setLand('Deutschland');
|
|
$existing->setIsPrimary(false);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([$existing]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
// find() is called by syncAddresses -> updateAddress
|
|
$this->addressMapper->method('find')->with(42)->willReturn($existing);
|
|
$this->addressMapper->method('update')->willThrowException(new \RuntimeException('DB error'));
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('rollback');
|
|
$this->db->expects($this->never())->method('commit');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->service->update(1, [
|
|
'addresses' => [
|
|
['id' => 42, 'strasse' => 'Neu', 'plz' => '12345', 'ort' => 'Neu'],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function testUpdateRollbacksTransactionWhenPhoneSyncFails(): void {
|
|
$member = $this->createMember(1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($member);
|
|
$this->memberMapper->method('update')->willReturnCallback(fn(Member $m) => $m);
|
|
$this->addressMapper->method('findByMemberId')->willReturn([]);
|
|
$this->phoneMapper->method('findByMemberId')->willReturn([]);
|
|
$this->emailMapper->method('findByMemberId')->willReturn([]);
|
|
|
|
// syncPhones will try to insert a new phone
|
|
$this->phoneMapper->method('insert')->willThrowException(new \RuntimeException('DB error'));
|
|
|
|
$this->db->expects($this->once())->method('beginTransaction');
|
|
$this->db->expects($this->once())->method('rollback');
|
|
$this->db->expects($this->never())->method('commit');
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
|
|
$this->service->update(1, [
|
|
'phones' => [['numberE164' => '+4917612345678']],
|
|
]);
|
|
}
|
|
}
|