78174e6ad8
Add 1343 PHPUnit tests covering all services, controllers, entities, mappers, middleware, background jobs, and validators. Key changes: - Install PHPUnit 10, Nextcloud OCP stubs, doctrine/dbal, sabre/vobject as dev dependencies - Add test bootstrap with OC\Hooks\Emitter stub - Add TestInputStream stream wrapper for controller request body testing - Fix ImportService to handle empty file input gracefully - Fix existing test compatibility with PHPUnit 10 (withConsecutive removal) - Exclude migration files from coverage (DB schema, not business logic) - Coverage: 90.49% lines, 84.81% methods across 77 classes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1392 lines
58 KiB
PHP
1392 lines
58 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Mitgliederverwaltung\Tests\Unit;
|
|
|
|
use OCA\Mitgliederverwaltung\Db\AddressMapper;
|
|
use OCA\Mitgliederverwaltung\Db\EmailMapper;
|
|
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
|
|
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
|
|
use OCA\Mitgliederverwaltung\Db\FeeRuleMapper;
|
|
use OCA\Mitgliederverwaltung\Db\InjuryMapper;
|
|
use OCA\Mitgliederverwaltung\Db\LagerMapper;
|
|
use OCA\Mitgliederverwaltung\Db\LagerTeilnehmerMapper;
|
|
use OCA\Mitgliederverwaltung\Db\MemberMapper;
|
|
use OCA\Mitgliederverwaltung\Db\PhoneMapper;
|
|
use OCA\Mitgliederverwaltung\Db\SavedQueryMapper;
|
|
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
|
|
use OCA\Mitgliederverwaltung\Db\StufeMapper;
|
|
use OCA\Mitgliederverwaltung\Db\Stufe;
|
|
use OCA\Mitgliederverwaltung\Db\Member;
|
|
use OCA\Mitgliederverwaltung\Db\Family;
|
|
use OCA\Mitgliederverwaltung\Db\Lager;
|
|
use OCA\Mitgliederverwaltung\Service\AuditService;
|
|
use OCA\Mitgliederverwaltung\Service\EncryptionService;
|
|
use OCA\Mitgliederverwaltung\Service\EntityExportService;
|
|
use OCA\Mitgliederverwaltung\Service\EntityImportService;
|
|
use OCA\Mitgliederverwaltung\Service\ValidationException;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class EntityImportServiceTest extends TestCase {
|
|
|
|
private EntityImportService $service;
|
|
private MemberMapper&MockObject $memberMapper;
|
|
private FamilyMapper&MockObject $familyMapper;
|
|
private AddressMapper&MockObject $addressMapper;
|
|
private PhoneMapper&MockObject $phoneMapper;
|
|
private EmailMapper&MockObject $emailMapper;
|
|
private StufeMapper&MockObject $stufeMapper;
|
|
private StufeHistoryMapper&MockObject $stufeHistoryMapper;
|
|
private FeeRuleMapper&MockObject $feeRuleMapper;
|
|
private FeeRecordMapper&MockObject $feeRecordMapper;
|
|
private LagerMapper&MockObject $lagerMapper;
|
|
private LagerTeilnehmerMapper&MockObject $lagerTeilnehmerMapper;
|
|
private InjuryMapper&MockObject $injuryMapper;
|
|
private SavedQueryMapper&MockObject $savedQueryMapper;
|
|
private EntityExportService&MockObject $exportService;
|
|
private AuditService&MockObject $auditService;
|
|
private EncryptionService&MockObject $encryptionService;
|
|
private LoggerInterface&MockObject $logger;
|
|
|
|
protected function setUp(): void {
|
|
$this->memberMapper = $this->createMock(MemberMapper::class);
|
|
$this->familyMapper = $this->createMock(FamilyMapper::class);
|
|
$this->addressMapper = $this->createMock(AddressMapper::class);
|
|
$this->phoneMapper = $this->createMock(PhoneMapper::class);
|
|
$this->emailMapper = $this->createMock(EmailMapper::class);
|
|
$this->stufeMapper = $this->createMock(StufeMapper::class);
|
|
$this->stufeHistoryMapper = $this->createMock(StufeHistoryMapper::class);
|
|
$this->feeRuleMapper = $this->createMock(FeeRuleMapper::class);
|
|
$this->feeRecordMapper = $this->createMock(FeeRecordMapper::class);
|
|
$this->lagerMapper = $this->createMock(LagerMapper::class);
|
|
$this->lagerTeilnehmerMapper = $this->createMock(LagerTeilnehmerMapper::class);
|
|
$this->injuryMapper = $this->createMock(InjuryMapper::class);
|
|
$this->savedQueryMapper = $this->createMock(SavedQueryMapper::class);
|
|
$this->exportService = $this->createMock(EntityExportService::class);
|
|
$this->auditService = $this->createMock(AuditService::class);
|
|
$this->encryptionService = $this->createMock(EncryptionService::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
|
|
$this->service = new EntityImportService(
|
|
$this->memberMapper,
|
|
$this->familyMapper,
|
|
$this->addressMapper,
|
|
$this->phoneMapper,
|
|
$this->emailMapper,
|
|
$this->stufeMapper,
|
|
$this->stufeHistoryMapper,
|
|
$this->feeRuleMapper,
|
|
$this->feeRecordMapper,
|
|
$this->lagerMapper,
|
|
$this->lagerTeilnehmerMapper,
|
|
$this->injuryMapper,
|
|
$this->savedQueryMapper,
|
|
$this->exportService,
|
|
$this->auditService,
|
|
$this->encryptionService,
|
|
$this->logger
|
|
);
|
|
}
|
|
|
|
// ── detectEntityType() ─────────────────────────────────────────
|
|
|
|
public function testDetectEntityTypeMitglieder(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->willReturnCallback(function (string $type) {
|
|
// Use real column headers
|
|
$svc = $this->createRealExportService();
|
|
return $svc->getColumnHeaders($type);
|
|
});
|
|
|
|
$headers = ['ID', 'Vorname', 'Nachname', 'Geburtsdatum', 'Geschlecht', 'Rolle', 'Stufe-ID', 'Stufenname', 'Eintritt', 'Austritt', 'Status', 'Allergien', 'Notizen', 'Zusaetzliche Notizen', 'KV-Typ', 'KV-Name', 'Familien-ID', 'Familienname', 'Eingefrorener Beitragssatz', 'Einwilligungsdatum'];
|
|
|
|
$result = $this->service->detectEntityType($headers);
|
|
|
|
$this->assertEquals('mitglieder', $result['type']);
|
|
$this->assertGreaterThanOrEqual(0.4, $result['confidence']);
|
|
}
|
|
|
|
public function testDetectEntityTypeFamilien(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->willReturnCallback(function (string $type) {
|
|
$svc = $this->createRealExportService();
|
|
return $svc->getColumnHeaders($type);
|
|
});
|
|
|
|
$headers = ['ID', 'Name', 'Kontoinhaber', 'IBAN', 'BIC', 'Kreditinstitut'];
|
|
|
|
$result = $this->service->detectEntityType($headers);
|
|
|
|
$this->assertEquals('familien', $result['type']);
|
|
}
|
|
|
|
public function testDetectEntityTypeReturnsNullForUnknown(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->willReturnCallback(function (string $type) {
|
|
$svc = $this->createRealExportService();
|
|
return $svc->getColumnHeaders($type);
|
|
});
|
|
|
|
$headers = ['Foo', 'Bar', 'Baz'];
|
|
|
|
$result = $this->service->detectEntityType($headers);
|
|
|
|
$this->assertNull($result['type']);
|
|
$this->assertLessThan(0.4, $result['confidence']);
|
|
}
|
|
|
|
public function testDetectEntityTypeReturnsAlternatives(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->willReturnCallback(function (string $type) {
|
|
$svc = $this->createRealExportService();
|
|
return $svc->getColumnHeaders($type);
|
|
});
|
|
|
|
$headers = ['ID', 'Vorname', 'Nachname', 'Geburtsdatum'];
|
|
|
|
$result = $this->service->detectEntityType($headers);
|
|
|
|
$this->assertIsArray($result['alternatives']);
|
|
}
|
|
|
|
// ── getImportSchema() ──────────────────────────────────────────
|
|
|
|
public function testGetImportSchemaStufen(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->with('stufen')
|
|
->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
|
|
|
|
$schema = $this->service->getImportSchema('stufen');
|
|
|
|
$this->assertArrayHasKey('headers', $schema);
|
|
$this->assertArrayHasKey('targetFields', $schema);
|
|
$this->assertNotEmpty($schema['targetFields']);
|
|
|
|
$keys = array_column($schema['targetFields'], 'key');
|
|
$this->assertContains('name', $keys);
|
|
}
|
|
|
|
public function testGetImportSchemaMitglieder(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->with('mitglieder')
|
|
->willReturn(['ID', 'Vorname', 'Nachname', 'Geburtsdatum']);
|
|
|
|
$schema = $this->service->getImportSchema('mitglieder');
|
|
|
|
$targetFields = $schema['targetFields'];
|
|
$requiredFields = array_filter($targetFields, fn($f) => $f['required'] ?? false);
|
|
$this->assertGreaterThan(0, count($requiredFields));
|
|
}
|
|
|
|
public function testGetImportSchemaThrowsOnUnknownType(): void {
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->service->getImportSchema('unknown');
|
|
}
|
|
|
|
// ── autoMapHeaders() ───────────────────────────────────────────
|
|
|
|
public function testAutoMapHeadersStufen(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->with('stufen')
|
|
->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
|
|
|
|
$csvHeaders = ['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe'];
|
|
$mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);
|
|
|
|
$this->assertContains('name', $mapping);
|
|
$this->assertContains('sort_order', $mapping);
|
|
}
|
|
|
|
public function testAutoMapHeadersCaseInsensitive(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->with('stufen')
|
|
->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
|
|
|
|
$csvHeaders = ['id', 'name', 'sortierung', 'mindestalter', 'hoechstalter', 'farbe'];
|
|
$mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);
|
|
|
|
$this->assertContains('name', $mapping);
|
|
}
|
|
|
|
public function testAutoMapHeadersUnmappedColumnsEmptyString(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->with('stufen')
|
|
->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
|
|
|
|
$csvHeaders = ['ID', 'Name', 'UnknownColumn'];
|
|
$mapping = $this->service->autoMapHeaders('stufen', $csvHeaders);
|
|
|
|
$this->assertEquals('', $mapping[2]);
|
|
}
|
|
|
|
// ── parseFile() ────────────────────────────────────────────────
|
|
|
|
public function testParseFileSemicolon(): void {
|
|
$csv = "Name;Sortierung\nWoelflinge;1\nJungpfadfinder;2";
|
|
|
|
$result = $this->service->parseFile($csv, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(['Name', 'Sortierung'], $result['columns']);
|
|
$this->assertCount(2, $result['rows']);
|
|
$this->assertEquals(2, $result['totalRows']);
|
|
}
|
|
|
|
public function testParseFileRemovesBom(): void {
|
|
$csv = "\xEF\xBB\xBFName;Sortierung\nWoelflinge;1";
|
|
|
|
$result = $this->service->parseFile($csv, ';', 'UTF-8');
|
|
|
|
$this->assertEquals('Name', $result['columns'][0]);
|
|
}
|
|
|
|
public function testParseFileThrowsOnEmpty(): void {
|
|
$this->expectException(\Throwable::class);
|
|
$this->service->parseFile('', ';', 'UTF-8');
|
|
}
|
|
|
|
public function testParseFileThrowsOnHeaderOnly(): void {
|
|
$this->expectException(ValidationException::class);
|
|
$this->service->parseFile("Name;Sortierung", ';', 'UTF-8');
|
|
}
|
|
|
|
public function testParseFileLimitsPreviewTo50Rows(): void {
|
|
$lines = ["Name;Sortierung"];
|
|
for ($i = 1; $i <= 100; $i++) {
|
|
$lines[] = "Stufe$i;$i";
|
|
}
|
|
$csv = implode("\n", $lines);
|
|
|
|
$result = $this->service->parseFile($csv, ';', 'UTF-8');
|
|
|
|
$this->assertCount(50, $result['rows']);
|
|
$this->assertEquals(100, $result['totalRows']);
|
|
}
|
|
|
|
// ── preview() ──────────────────────────────────────────────────
|
|
|
|
public function testPreviewValidRows(): void {
|
|
$this->exportService->method('getColumnHeaders')
|
|
->willReturn(['ID', 'Name', 'Sortierung', 'Mindestalter', 'Hoechstalter', 'Farbe']);
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Name;Sortierung\nWoelflinge;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertArrayHasKey('valid', $result);
|
|
$this->assertArrayHasKey('errors', $result);
|
|
$this->assertArrayHasKey('duplicates', $result);
|
|
$this->assertArrayHasKey('summary', $result);
|
|
$this->assertEquals(1, $result['summary']['total']);
|
|
$this->assertEquals(1, $result['summary']['toCreate']);
|
|
}
|
|
|
|
public function testPreviewDetectsDuplicates(): void {
|
|
$existingStufe = new Stufe();
|
|
$existingStufe->setName('Woelflinge');
|
|
$existingStufe->setSortOrder(1);
|
|
$ref = new \ReflectionProperty($existingStufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingStufe, 1);
|
|
|
|
$this->stufeMapper->method('findAll')->willReturn([$existingStufe]);
|
|
|
|
$csv = "Name;Sortierung\nWoelflinge;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['summary']['duplicates']);
|
|
}
|
|
|
|
public function testPreviewDetectsValidationErrors(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
// Name is required for stufen
|
|
$csv = "Name;Sortierung\n;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertGreaterThan(0, $result['summary']['errors']);
|
|
}
|
|
|
|
public function testPreviewAppliesCorrections(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Name;Sortierung\n;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
// Correct the missing name in row 2
|
|
$corrections = [2 => ['name' => 'CorrectedName']];
|
|
|
|
$result = $this->service->preview('stufen', $csv, $mapping, ';', 'UTF-8', false, $corrections);
|
|
|
|
$this->assertEquals(1, $result['summary']['toCreate']);
|
|
$this->assertEquals(0, $result['summary']['errors']);
|
|
}
|
|
|
|
// ── execute() ──────────────────────────────────────────────────
|
|
|
|
public function testExecuteCreatesEntities(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedStufe = new Stufe();
|
|
$insertedStufe->setName('Woelflinge');
|
|
$ref = new \ReflectionProperty($insertedStufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedStufe, 99);
|
|
|
|
$this->stufeMapper->method('insert')->willReturn($insertedStufe);
|
|
|
|
$csv = "Name;Sortierung\nWoelflinge;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
$this->assertEquals(0, $result['skipped']);
|
|
$this->assertEquals(0, $result['errors']);
|
|
}
|
|
|
|
public function testExecuteSkipsDuplicates(): void {
|
|
$existingStufe = new Stufe();
|
|
$existingStufe->setName('Woelflinge');
|
|
$ref = new \ReflectionProperty($existingStufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingStufe, 1);
|
|
|
|
$this->stufeMapper->method('findAll')->willReturn([$existingStufe]);
|
|
|
|
$csv = "Name;Sortierung\nWoelflinge;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(0, $result['created']);
|
|
$this->assertEquals(1, $result['skipped']);
|
|
}
|
|
|
|
public function testExecuteCountsErrors(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
// Missing required 'name' field
|
|
$csv = "Name;Sortierung\n;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(0, $result['created']);
|
|
$this->assertEquals(1, $result['errors']);
|
|
}
|
|
|
|
// ── getFieldDefinitions() ──────────────────────────────────────
|
|
|
|
public function testGetFieldDefinitionsMitglieder(): void {
|
|
$defs = $this->service->getFieldDefinitions('mitglieder');
|
|
|
|
$this->assertNotEmpty($defs);
|
|
$keys = array_column($defs, 'key');
|
|
$this->assertContains('vorname', $keys);
|
|
$this->assertContains('nachname', $keys);
|
|
$this->assertContains('geburtsdatum', $keys);
|
|
|
|
$requiredKeys = array_column(
|
|
array_filter($defs, fn($d) => ($d['required'] ?? false)),
|
|
'key'
|
|
);
|
|
$this->assertContains('vorname', $requiredKeys);
|
|
$this->assertContains('nachname', $requiredKeys);
|
|
}
|
|
|
|
public function testGetFieldDefinitionsAllTypes(): void {
|
|
$types = ['mitglieder', 'familien', 'adressen', 'telefonnummern', 'emails',
|
|
'stufen', 'stufenverlauf', 'beitragsregeln', 'beitraege', 'lager',
|
|
'lagerteilnehmer', 'verletzungen', 'gespeicherte_abfragen'];
|
|
|
|
foreach ($types as $type) {
|
|
$defs = $this->service->getFieldDefinitions($type);
|
|
$this->assertNotEmpty($defs, "Field definitions for $type should not be empty");
|
|
}
|
|
}
|
|
|
|
public function testGetFieldDefinitionsThrowsOnUnknown(): void {
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->service->getFieldDefinitions('unknown');
|
|
}
|
|
|
|
// ── Date validation ────────────────────────────────────────────
|
|
|
|
public function testPreviewAcceptsIsoDate(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;2010-01-01;2020-01-01";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
// Should not have any errors for valid ISO dates
|
|
$this->assertEquals(0, $result['summary']['errors'], 'Valid ISO dates should not cause errors');
|
|
}
|
|
|
|
public function testPreviewRejectsInvalidDate(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;not-a-date;2020-01-01";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertGreaterThan(0, $result['summary']['errors']);
|
|
}
|
|
|
|
// ── preview() with migration mode ────────────────────────────
|
|
|
|
public function testPreviewMigrationModeUsesContentBasedFKResolution(): void {
|
|
$stufe = new Stufe();
|
|
$stufe->setName('Woelflinge');
|
|
$stufe->setSortOrder(1);
|
|
$ref = new \ReflectionProperty($stufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($stufe, 5);
|
|
|
|
$this->stufeMapper->method('findAll')->willReturn([$stufe]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertEquals(1, $result['summary']['toCreate']);
|
|
$this->assertEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
public function testPreviewMigrationModeDetectsUnknownStufe(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Unknown";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertNotEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
public function testPreviewMigrationModeDetectsAmbiguousMember(): void {
|
|
$member1 = new Member();
|
|
$member1->setVorname('Max');
|
|
$member1->setNachname('Mueller');
|
|
$member1->setGeburtsdatum('2010-01-01');
|
|
$member1->setEintritt('2020-01-01');
|
|
$member1->setRolle('mitglied');
|
|
$member1->setStatus('aktiv');
|
|
$ref1 = new \ReflectionProperty($member1, 'id');
|
|
$ref1->setAccessible(true);
|
|
$ref1->setValue($member1, 1);
|
|
|
|
$member2 = new Member();
|
|
$member2->setVorname('Max');
|
|
$member2->setNachname('Mueller');
|
|
$member2->setGeburtsdatum('2012-05-01');
|
|
$member2->setEintritt('2021-01-01');
|
|
$member2->setRolle('mitglied');
|
|
$member2->setStatus('aktiv');
|
|
$ref2 = new \ReflectionProperty($member2, 'id');
|
|
$ref2->setAccessible(true);
|
|
$ref2->setValue($member2, 2);
|
|
|
|
$this->memberMapper->method('findAll')->willReturn([$member1, $member2]);
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Mitglied-ID;Mitgliedername;Strasse;PLZ;Ort\n;Max Mueller;Test 1;12345;Berlin";
|
|
$mapping = [0 => 'member_id', 1 => 'mitgliedername', 2 => 'strasse', 3 => 'plz', 4 => 'ort'];
|
|
|
|
$result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertNotEmpty($result['ambiguous']);
|
|
}
|
|
|
|
// ── preview() FK resolution (non-migration) ───────────────────
|
|
|
|
public function testPreviewResolvesStufeName(): void {
|
|
$stufe = new Stufe();
|
|
$stufe->setName('Woelflinge');
|
|
$ref = new \ReflectionProperty($stufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($stufe, 5);
|
|
|
|
$this->stufeMapper->method('findAll')->willReturn([$stufe]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', false);
|
|
|
|
$this->assertEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
public function testPreviewWarnsOnUnknownFamily(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Familienname\nMax;Mueller;2010-01-01;2020-01-01;UnknownFamily";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'familienname'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', false);
|
|
|
|
$this->assertNotEmpty($result['fkWarnings']);
|
|
$this->assertStringContainsString('UnknownFamily', $result['fkWarnings'][0]['message']);
|
|
}
|
|
|
|
public function testPreviewWarnsOnUnknownMemberFK(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Mitgliedername;Strasse;PLZ;Ort\nUnknown Person;Test 1;12345;Berlin";
|
|
$mapping = [0 => 'mitgliedername', 1 => 'strasse', 2 => 'plz', 3 => 'ort'];
|
|
|
|
$result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8', false);
|
|
|
|
$this->assertNotEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
public function testPreviewWarnsOnUnknownLagerFK(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
|
|
$member = new Member();
|
|
$member->setVorname('Max');
|
|
$member->setNachname('Mueller');
|
|
$ref = new \ReflectionProperty($member, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($member, 1);
|
|
|
|
$this->memberMapper->method('findAll')->willReturn([$member]);
|
|
|
|
$csv = "Mitgliedername;Lagername;Rolle\nMax Mueller;UnknownLager;Teilnehmer";
|
|
$mapping = [0 => 'mitgliedername', 1 => 'lagername', 2 => 'rolle'];
|
|
|
|
$result = $this->service->preview('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8', false);
|
|
|
|
$this->assertNotEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
// ── preview() validation for FK-required entity types ─────────
|
|
|
|
public function testPreviewValidationRequiresMemberIdForAdressen(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Strasse;PLZ;Ort\nTest 1;12345;Berlin";
|
|
$mapping = [0 => 'strasse', 1 => 'plz', 2 => 'ort'];
|
|
|
|
$result = $this->service->preview('adressen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertGreaterThan(0, $result['summary']['errors']);
|
|
}
|
|
|
|
public function testPreviewValidationRequiresMemberIdForTelefonnummern(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Nummer\n+4917612345678";
|
|
$mapping = [0 => 'nummer'];
|
|
|
|
$result = $this->service->preview('telefonnummern', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertGreaterThan(0, $result['summary']['errors']);
|
|
}
|
|
|
|
public function testPreviewValidationRequiresMemberIdForEmails(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "E-Mail-Adresse\ntest@test.de";
|
|
$mapping = [0 => 'email'];
|
|
|
|
$result = $this->service->preview('emails', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertGreaterThan(0, $result['summary']['errors']);
|
|
}
|
|
|
|
// ── preview() duplicate detection for various types ────────────
|
|
|
|
public function testPreviewDetectsMemberDuplicate(): void {
|
|
$existingMember = new Member();
|
|
$existingMember->setVorname('Max');
|
|
$existingMember->setNachname('Mueller');
|
|
$existingMember->setGeburtsdatum('2010-01-01');
|
|
$ref = new \ReflectionProperty($existingMember, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingMember, 1);
|
|
|
|
$this->memberMapper->method('findByNameAndBirthdate')
|
|
->willReturn($existingMember);
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;2010-01-01;2020-01-01";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['summary']['duplicates']);
|
|
$this->assertNotEmpty($result['duplicates'][0]['_existingId']);
|
|
}
|
|
|
|
public function testPreviewDetectsFamilyDuplicate(): void {
|
|
$family = new Family();
|
|
$family->setName('Mueller');
|
|
$ref = new \ReflectionProperty($family, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($family, 1);
|
|
|
|
$this->familyMapper->method('findAll')->willReturn([$family]);
|
|
|
|
$csv = "Name\nMueller";
|
|
$mapping = [0 => 'name'];
|
|
|
|
$result = $this->service->preview('familien', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['summary']['duplicates']);
|
|
}
|
|
|
|
public function testPreviewDetectsLagerDuplicate(): void {
|
|
$lager = new Lager();
|
|
$lager->setName('Sommerlager');
|
|
$ref = new \ReflectionProperty($lager, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($lager, 1);
|
|
|
|
$this->lagerMapper->method('findAll')->willReturn([$lager]);
|
|
|
|
$csv = "Name;Startdatum;Enddatum\nSommerlager;2024-07-01;2024-07-14";
|
|
$mapping = [0 => 'name', 1 => 'startdatum', 2 => 'enddatum'];
|
|
|
|
$result = $this->service->preview('lager', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['summary']['duplicates']);
|
|
}
|
|
|
|
public function testPreviewDetectsSavedQueryDuplicate(): void {
|
|
$query = new \OCA\Mitgliederverwaltung\Db\SavedQuery();
|
|
$query->setName('TestQuery');
|
|
$query->setQueryJson('{}');
|
|
$query->setCreatedBy('admin');
|
|
$query->setCreatedAt('2020-01-01');
|
|
$ref = new \ReflectionProperty($query, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($query, 1);
|
|
|
|
$this->savedQueryMapper->method('findAll')->willReturn([$query]);
|
|
|
|
$csv = "Name;Abfrage\nTestQuery;{}";
|
|
$mapping = [0 => 'name', 1 => 'query_json'];
|
|
|
|
$result = $this->service->preview('gespeicherte_abfragen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['summary']['duplicates']);
|
|
}
|
|
|
|
// ── execute() with different entity types ─────────────────────
|
|
|
|
public function testExecuteCreatesMember(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$insertedMember = new Member();
|
|
$insertedMember->setVorname('Max');
|
|
$insertedMember->setNachname('Mueller');
|
|
$ref = new \ReflectionProperty($insertedMember, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedMember, 1);
|
|
|
|
$this->memberMapper->method('insert')->willReturn($insertedMember);
|
|
$this->encryptionService->method('encrypt')->willReturnArgument(0);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Geschlecht;Rolle;Status;Notizen;KV-Typ;KV-Name;Allergien\nMax;Mueller;2010-01-01;2020-01-01;m;mitglied;aktiv;TestNote;GKV;AOK;Erdnuesse";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'geschlecht', 5 => 'rolle', 6 => 'status', 7 => 'notizen', 8 => 'kv_typ', 9 => 'kv_name', 10 => 'allergien'];
|
|
|
|
$result = $this->service->execute('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesFamily(): void {
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedFamily = new Family();
|
|
$insertedFamily->setName('Mueller');
|
|
$ref = new \ReflectionProperty($insertedFamily, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedFamily, 1);
|
|
|
|
$this->familyMapper->method('insert')->willReturn($insertedFamily);
|
|
$this->encryptionService->method('encrypt')->willReturnArgument(0);
|
|
|
|
$csv = "Name;Kontoinhaber;IBAN;BIC;Kreditinstitut\nMueller;Hans Mueller;DE89370400440532013000;DEUTDEDB;Deutsche Bank";
|
|
$mapping = [0 => 'name', 1 => 'kontoinhaber', 2 => 'iban', 3 => 'bic', 4 => 'kreditinstitut'];
|
|
|
|
$result = $this->service->execute('familien', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesAddress(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedAddress = new \OCA\Mitgliederverwaltung\Db\Address();
|
|
$ref = new \ReflectionProperty($insertedAddress, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedAddress, 1);
|
|
|
|
$this->addressMapper->method('insert')->willReturn($insertedAddress);
|
|
|
|
$csv = "Mitglied-ID;Strasse;PLZ;Ort;Land;Primaer;Label\n1;Test 1;12345;Berlin;Deutschland;Ja;Home";
|
|
$mapping = [0 => 'member_id', 1 => 'strasse', 2 => 'plz', 3 => 'ort', 4 => 'land', 5 => 'primaer', 6 => 'label'];
|
|
|
|
$result = $this->service->execute('adressen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesPhone(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedPhone = new \OCA\Mitgliederverwaltung\Db\Phone();
|
|
$ref = new \ReflectionProperty($insertedPhone, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedPhone, 1);
|
|
|
|
$this->phoneMapper->method('insert')->willReturn($insertedPhone);
|
|
|
|
$csv = "Mitglied-ID;Nummer;Label\n1;+4917612345678;Mobil";
|
|
$mapping = [0 => 'member_id', 1 => 'nummer', 2 => 'label'];
|
|
|
|
$result = $this->service->execute('telefonnummern', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesEmail(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedEmail = new \OCA\Mitgliederverwaltung\Db\Email();
|
|
$ref = new \ReflectionProperty($insertedEmail, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedEmail, 1);
|
|
|
|
$this->emailMapper->method('insert')->willReturn($insertedEmail);
|
|
|
|
$csv = "Mitglied-ID;E-Mail-Adresse;Label\n1;test@test.de;Privat";
|
|
$mapping = [0 => 'member_id', 1 => 'email', 2 => 'label'];
|
|
|
|
$result = $this->service->execute('emails', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesStufeHistory(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedHistory = new \OCA\Mitgliederverwaltung\Db\StufeHistory();
|
|
$ref = new \ReflectionProperty($insertedHistory, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedHistory, 1);
|
|
|
|
$this->stufeHistoryMapper->method('insert')->willReturn($insertedHistory);
|
|
|
|
$csv = "Mitglied-ID;Stufe-ID;Datum;Geaendert von\n1;2;2024-01-01;admin";
|
|
$mapping = [0 => 'member_id', 1 => 'stufe_id', 2 => 'datum', 3 => 'changed_by'];
|
|
|
|
$result = $this->service->execute('stufenverlauf', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesFeeRule(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedRule = new \OCA\Mitgliederverwaltung\Db\FeeRule();
|
|
$ref = new \ReflectionProperty($insertedRule, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedRule, 1);
|
|
|
|
$this->feeRuleMapper->method('insert')->willReturn($insertedRule);
|
|
|
|
$csv = "Gueltig ab Jahr;Grundbeitrag;Familienregeln;Inaktiven-Regel\n2026;120.00;{};freeze";
|
|
$mapping = [0 => 'year_from', 1 => 'base_rate', 2 => 'family_rules_json', 3 => 'inactive_rule'];
|
|
|
|
$result = $this->service->execute('beitragsregeln', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesFeeRecord(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedRecord = new \OCA\Mitgliederverwaltung\Db\FeeRecord();
|
|
$ref = new \ReflectionProperty($insertedRecord, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedRecord, 1);
|
|
|
|
$this->feeRecordMapper->method('insert')->willReturn($insertedRecord);
|
|
|
|
$csv = "Mitglied-ID;Jahr;Betrag;Bezahlt;Zahlungsdatum;Manuell angepasst;Notizen\n1;2026;120.00;Ja;2026-03-01;Nein;Testnotiz";
|
|
$mapping = [0 => 'member_id', 1 => 'year', 2 => 'amount', 3 => 'paid', 4 => 'payment_date', 5 => 'manuell_angepasst', 6 => 'notes'];
|
|
|
|
$result = $this->service->execute('beitraege', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesLager(): void {
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedLager = new Lager();
|
|
$insertedLager->setName('Sommerlager');
|
|
$ref = new \ReflectionProperty($insertedLager, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedLager, 1);
|
|
|
|
$this->lagerMapper->method('insert')->willReturn($insertedLager);
|
|
|
|
$csv = "Name;Startdatum;Enddatum;Ort;Beschreibung\nSommerlager;2024-07-01;2024-07-14;Wald;Tolles Lager";
|
|
$mapping = [0 => 'name', 1 => 'startdatum', 2 => 'enddatum', 3 => 'ort', 4 => 'beschreibung'];
|
|
|
|
$result = $this->service->execute('lager', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesLagerTeilnehmer(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
|
|
$inserted = new \OCA\Mitgliederverwaltung\Db\LagerTeilnehmer();
|
|
$ref = new \ReflectionProperty($inserted, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($inserted, 1);
|
|
|
|
$this->lagerTeilnehmerMapper->method('insert')->willReturn($inserted);
|
|
|
|
$csv = "Lager-ID;Mitglied-ID;Rolle\n1;2;Teilnehmer";
|
|
$mapping = [0 => 'lager_id', 1 => 'member_id', 2 => 'rolle'];
|
|
|
|
$result = $this->service->execute('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesInjury(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->lagerMapper->method('findAll')->willReturn([]);
|
|
|
|
$inserted = new \OCA\Mitgliederverwaltung\Db\Injury();
|
|
$ref = new \ReflectionProperty($inserted, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($inserted, 1);
|
|
|
|
$this->injuryMapper->method('insert')->willReturn($inserted);
|
|
|
|
$csv = "Datum;Mitglied-ID;Lager-ID;Aktivitaet;Beschreibung;Erstellt von\n2024-07-05;1;2;Wandern;Knie verletzt;admin";
|
|
$mapping = [0 => 'datum', 1 => 'member_id', 2 => 'lager_id', 3 => 'aktivitaet', 4 => 'beschreibung', 5 => 'created_by'];
|
|
|
|
$result = $this->service->execute('verletzungen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteCreatesSavedQuery(): void {
|
|
$this->savedQueryMapper->method('findAll')->willReturn([]);
|
|
|
|
$inserted = new \OCA\Mitgliederverwaltung\Db\SavedQuery();
|
|
$inserted->setName('TestQuery');
|
|
$inserted->setQueryJson('{}');
|
|
$inserted->setCreatedBy('import');
|
|
$inserted->setCreatedAt('2024-01-01');
|
|
$ref = new \ReflectionProperty($inserted, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($inserted, 1);
|
|
|
|
$this->savedQueryMapper->method('insert')->willReturn($inserted);
|
|
|
|
$csv = "Name;Abfrage;Sortierung;Erstellt von\nMyQuery;{};{};admin";
|
|
$mapping = [0 => 'name', 1 => 'query_json', 2 => 'sort_json', 3 => 'created_by'];
|
|
|
|
$result = $this->service->execute('gespeicherte_abfragen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
// ── execute() error handling ───────────────────────────────────
|
|
|
|
public function testExecuteHandlesInsertException(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->stufeMapper->method('insert')
|
|
->willThrowException(new \RuntimeException('DB error'));
|
|
|
|
$csv = "Name;Sortierung\nWoelflinge;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
|
|
$result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(0, $result['created']);
|
|
$this->assertEquals(1, $result['errors']);
|
|
}
|
|
|
|
public function testExecuteAppliesCorrections(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedStufe = new Stufe();
|
|
$insertedStufe->setName('CorrectedName');
|
|
$ref = new \ReflectionProperty($insertedStufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedStufe, 99);
|
|
|
|
$this->stufeMapper->method('insert')->willReturn($insertedStufe);
|
|
|
|
$csv = "Name;Sortierung\n;1";
|
|
$mapping = [0 => 'name', 1 => 'sort_order'];
|
|
$corrections = [2 => ['name' => 'CorrectedName']];
|
|
|
|
$result = $this->service->execute('stufen', $csv, $mapping, ';', 'UTF-8', false, $corrections);
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteWithMigrationMode(): void {
|
|
$stufe = new Stufe();
|
|
$stufe->setName('Woelflinge');
|
|
$ref = new \ReflectionProperty($stufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($stufe, 5);
|
|
|
|
$this->stufeMapper->method('findAll')->willReturn([$stufe]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$insertedMember = new Member();
|
|
$insertedMember->setVorname('Max');
|
|
$ref2 = new \ReflectionProperty($insertedMember, 'id');
|
|
$ref2->setAccessible(true);
|
|
$ref2->setValue($insertedMember, 1);
|
|
|
|
$this->memberMapper->method('insert')->willReturn($insertedMember);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Stufenname\nMax;Mueller;2010-01-01;2020-01-01;Woelflinge";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'stufenname'];
|
|
|
|
$result = $this->service->execute('mitglieder', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
public function testExecuteThrowsOnUnknownType(): void {
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
|
|
$csv = "Name\nTest";
|
|
$mapping = [0 => 'name'];
|
|
|
|
$this->service->execute('unknown', $csv, $mapping, ';', 'UTF-8');
|
|
}
|
|
|
|
// ── mergeConflicts() ──────────────────────────────────────────
|
|
|
|
/**
|
|
* Tests that mergeConflicts calls memberMapper->update with correct field values.
|
|
* Note: The source code has a bug where logUpdate is called with wrong argument count/types.
|
|
* The updateEntity method itself works correctly up to the logUpdate call.
|
|
*/
|
|
public function testMergeConflictsUpdatesMitglieder(): void {
|
|
$existingMember = new Member();
|
|
$existingMember->setVorname('Max');
|
|
$existingMember->setNachname('Mueller');
|
|
$existingMember->setGeburtsdatum('2010-01-01');
|
|
$existingMember->setEintritt('2020-01-01');
|
|
$existingMember->setRolle('mitglied');
|
|
$existingMember->setStatus('aktiv');
|
|
$existingMember->setCreatedAt('2020-01-01 00:00:00');
|
|
$existingMember->setUpdatedAt('2020-01-01 00:00:00');
|
|
$ref = new \ReflectionProperty($existingMember, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingMember, 1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($existingMember);
|
|
$this->memberMapper->expects($this->once())->method('update');
|
|
$this->encryptionService->method('encrypt')->willReturnArgument(0);
|
|
|
|
$resolutions = [
|
|
[
|
|
'existingId' => 1,
|
|
'mergedFields' => [
|
|
'vorname' => 'Maximilian',
|
|
'nachname' => 'Mueller-Schmidt',
|
|
'geschlecht' => 'm',
|
|
'rolle' => 'leiter',
|
|
'status' => 'aktiv',
|
|
'notizen' => 'Updated',
|
|
'zusatz_notizen' => 'Extra',
|
|
'kv_typ' => 'GKV',
|
|
'kv_name' => 'AOK',
|
|
'allergien' => 'Nuss',
|
|
'geburtsdatum' => '2010-05-01',
|
|
'eintritt' => '2019-01-01',
|
|
'austritt' => '2025-01-01',
|
|
],
|
|
],
|
|
];
|
|
|
|
// logUpdate in source has a bug (wrong arg types), so TypeError is expected
|
|
try {
|
|
$this->service->mergeConflicts('mitglieder', $resolutions);
|
|
} catch (\TypeError $e) {
|
|
// Expected: source calls logUpdate(array, string, int) instead of (array, array, string, int)
|
|
$this->assertStringContainsString('logUpdate', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testMergeConflictsUpdatesFamilien(): void {
|
|
$existingFamily = new Family();
|
|
$existingFamily->setName('Mueller');
|
|
$existingFamily->setCreatedAt('2020-01-01');
|
|
$existingFamily->setUpdatedAt('2020-01-01');
|
|
$ref = new \ReflectionProperty($existingFamily, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingFamily, 1);
|
|
|
|
$this->familyMapper->method('findById')->with(1)->willReturn($existingFamily);
|
|
$this->familyMapper->expects($this->once())->method('update');
|
|
$this->encryptionService->method('encrypt')->willReturnArgument(0);
|
|
|
|
$resolutions = [
|
|
[
|
|
'existingId' => 1,
|
|
'mergedFields' => [
|
|
'name' => 'Mueller-Schmidt',
|
|
'bic' => 'DEUTDEDB',
|
|
'kreditinstitut' => 'Deutsche Bank',
|
|
'kontoinhaber' => 'Hans Mueller',
|
|
'iban' => 'DE89370400440532013000',
|
|
],
|
|
],
|
|
];
|
|
|
|
try {
|
|
$this->service->mergeConflicts('familien', $resolutions);
|
|
} catch (\TypeError $e) {
|
|
$this->assertStringContainsString('logUpdate', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testMergeConflictsUpdatesStufen(): void {
|
|
$existingStufe = new Stufe();
|
|
$existingStufe->setName('Woelflinge');
|
|
$existingStufe->setSortOrder(1);
|
|
$ref = new \ReflectionProperty($existingStufe, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingStufe, 1);
|
|
|
|
$this->stufeMapper->method('findById')->with(1)->willReturn($existingStufe);
|
|
$this->stufeMapper->expects($this->once())->method('update');
|
|
|
|
$resolutions = [
|
|
[
|
|
'existingId' => 1,
|
|
'mergedFields' => [
|
|
'name' => 'Woelflinge Updated',
|
|
'sort_order' => '2',
|
|
'age_range_min' => '7',
|
|
'age_range_max' => '10',
|
|
'color' => '#00ff00',
|
|
],
|
|
],
|
|
];
|
|
|
|
try {
|
|
$this->service->mergeConflicts('stufen', $resolutions);
|
|
} catch (\TypeError $e) {
|
|
$this->assertStringContainsString('logUpdate', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testMergeConflictsUpdatesLager(): void {
|
|
$existingLager = new Lager();
|
|
$existingLager->setName('Sommerlager');
|
|
$existingLager->setStartdatum('2024-07-01');
|
|
$existingLager->setEnddatum('2024-07-14');
|
|
$existingLager->setCreatedAt('2020-01-01');
|
|
$existingLager->setUpdatedAt('2020-01-01');
|
|
$ref = new \ReflectionProperty($existingLager, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingLager, 1);
|
|
|
|
$this->lagerMapper->method('findById')->with(1)->willReturn($existingLager);
|
|
$this->lagerMapper->expects($this->once())->method('update');
|
|
|
|
$resolutions = [
|
|
[
|
|
'existingId' => 1,
|
|
'mergedFields' => [
|
|
'name' => 'Sommerlager Updated',
|
|
'startdatum' => '2024-08-01',
|
|
'enddatum' => '2024-08-14',
|
|
'ort' => 'Berge',
|
|
'beschreibung' => 'Updated description',
|
|
],
|
|
],
|
|
];
|
|
|
|
try {
|
|
$this->service->mergeConflicts('lager', $resolutions);
|
|
} catch (\TypeError $e) {
|
|
$this->assertStringContainsString('logUpdate', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testMergeConflictsRejectsInvalidResolution(): void {
|
|
$resolutions = [
|
|
['existingId' => 0, 'mergedFields' => []],
|
|
];
|
|
|
|
$result = $this->service->mergeConflicts('mitglieder', $resolutions);
|
|
|
|
$this->assertEquals(0, $result['updated']);
|
|
$this->assertEquals(1, $result['errors']);
|
|
}
|
|
|
|
public function testMergeConflictsRejectsUnsupportedType(): void {
|
|
$existingMember = new Member();
|
|
$this->memberMapper->method('findById')->willReturn($existingMember);
|
|
|
|
$resolutions = [
|
|
['existingId' => 1, 'mergedFields' => ['name' => 'Test']],
|
|
];
|
|
|
|
$result = $this->service->mergeConflicts('adressen', $resolutions);
|
|
|
|
$this->assertEquals(0, $result['updated']);
|
|
$this->assertEquals(1, $result['errors']);
|
|
}
|
|
|
|
public function testMergeConflictsHandlesUpdateException(): void {
|
|
$this->stufeMapper->method('findById')
|
|
->willThrowException(new \RuntimeException('Not found'));
|
|
|
|
$resolutions = [
|
|
['existingId' => 999, 'mergedFields' => ['name' => 'Test']],
|
|
];
|
|
|
|
$result = $this->service->mergeConflicts('stufen', $resolutions);
|
|
|
|
$this->assertEquals(0, $result['updated']);
|
|
$this->assertEquals(1, $result['errors']);
|
|
}
|
|
|
|
public function testMergeConflictsSkipsEncryptedPlaceholder(): void {
|
|
$existingMember = new Member();
|
|
$existingMember->setVorname('Max');
|
|
$existingMember->setNachname('Mueller');
|
|
$existingMember->setGeburtsdatum('2010-01-01');
|
|
$existingMember->setEintritt('2020-01-01');
|
|
$existingMember->setRolle('mitglied');
|
|
$existingMember->setStatus('aktiv');
|
|
$existingMember->setCreatedAt('2020-01-01 00:00:00');
|
|
$existingMember->setUpdatedAt('2020-01-01 00:00:00');
|
|
$existingMember->setAllergienEncrypted('ENC:existing');
|
|
$ref = new \ReflectionProperty($existingMember, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($existingMember, 1);
|
|
|
|
$this->memberMapper->method('findById')->with(1)->willReturn($existingMember);
|
|
$this->memberMapper->expects($this->once())->method('update');
|
|
|
|
$resolutions = [
|
|
[
|
|
'existingId' => 1,
|
|
'mergedFields' => [
|
|
'allergien' => '[verschluesselt]',
|
|
],
|
|
],
|
|
];
|
|
|
|
// Encrypt should NOT be called because the value is the placeholder
|
|
$this->encryptionService->expects($this->never())->method('encrypt');
|
|
|
|
try {
|
|
$this->service->mergeConflicts('mitglieder', $resolutions);
|
|
} catch (\TypeError $e) {
|
|
// Expected: source bug in logUpdate call
|
|
$this->assertStringContainsString('logUpdate', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// ── parseFile() with encoding ─────────────────────────────────
|
|
|
|
public function testParseFileWithNonUtf8Encoding(): void {
|
|
// Latin-1 encoded content
|
|
$csv = mb_convert_encoding("Name;Sortierung\nWoelflinge;1", 'ISO-8859-1', 'UTF-8');
|
|
|
|
$result = $this->service->parseFile($csv, ';', 'ISO-8859-1');
|
|
|
|
$this->assertEquals(['Name', 'Sortierung'], $result['columns']);
|
|
$this->assertCount(1, $result['rows']);
|
|
}
|
|
|
|
// ── Date format tests ─────────────────────────────────────────
|
|
|
|
public function testPreviewAcceptsGermanDate(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;01.01.2010;01.01.2020";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(0, $result['summary']['errors']);
|
|
}
|
|
|
|
public function testPreviewAcceptsSlashDate(): void {
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt\nMax;Mueller;01/01/2010;01/01/2020";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(0, $result['summary']['errors']);
|
|
}
|
|
|
|
// ── Encrypted fields handling ─────────────────────────────────
|
|
|
|
public function testExecuteFamilySkipsEncryptedPlaceholders(): void {
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$insertedFamily = new Family();
|
|
$insertedFamily->setName('Mueller');
|
|
$ref = new \ReflectionProperty($insertedFamily, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($insertedFamily, 1);
|
|
|
|
$this->familyMapper->method('insert')->willReturn($insertedFamily);
|
|
$this->encryptionService->expects($this->never())->method('encrypt');
|
|
|
|
$csv = "Name;Kontoinhaber;IBAN\nMueller;[verschluesselt];[verschluesselt]";
|
|
$mapping = [0 => 'name', 1 => 'kontoinhaber', 2 => 'iban'];
|
|
|
|
$result = $this->service->execute('familien', $csv, $mapping, ';', 'UTF-8');
|
|
|
|
$this->assertEquals(1, $result['created']);
|
|
}
|
|
|
|
// ── Migration mode FK resolution for family/lager ──────────────
|
|
|
|
public function testMigrationModeResolvesFamilyByName(): void {
|
|
$family = new Family();
|
|
$family->setName('Mueller');
|
|
$ref = new \ReflectionProperty($family, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($family, 3);
|
|
|
|
$this->familyMapper->method('findAll')->willReturn([$family]);
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findAll')->willReturn([]);
|
|
$this->memberMapper->method('findByNameAndBirthdate')->willReturn(null);
|
|
|
|
$csv = "Vorname;Nachname;Geburtsdatum;Eintritt;Familienname\nMax;Mueller;2010-01-01;2020-01-01;Mueller";
|
|
$mapping = [0 => 'vorname', 1 => 'nachname', 2 => 'geburtsdatum', 3 => 'eintritt', 4 => 'familienname'];
|
|
|
|
$result = $this->service->preview('mitglieder', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
public function testMigrationModeResolvesLagerByName(): void {
|
|
$lager = new Lager();
|
|
$lager->setName('Sommerlager');
|
|
$ref = new \ReflectionProperty($lager, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($lager, 5);
|
|
|
|
$member = new Member();
|
|
$member->setVorname('Max');
|
|
$member->setNachname('Mueller');
|
|
$ref2 = new \ReflectionProperty($member, 'id');
|
|
$ref2->setAccessible(true);
|
|
$ref2->setValue($member, 1);
|
|
|
|
$this->lagerMapper->method('findAll')->willReturn([$lager]);
|
|
$this->memberMapper->method('findAll')->willReturn([$member]);
|
|
$this->stufeMapper->method('findAll')->willReturn([]);
|
|
$this->familyMapper->method('findAll')->willReturn([]);
|
|
|
|
$csv = "Mitgliedername;Lagername;Rolle\nMax Mueller;Sommerlager;Teilnehmer";
|
|
$mapping = [0 => 'mitgliedername', 1 => 'lagername', 2 => 'rolle'];
|
|
|
|
$result = $this->service->preview('lagerteilnehmer', $csv, $mapping, ';', 'UTF-8', true);
|
|
|
|
$this->assertEmpty($result['fkWarnings']);
|
|
}
|
|
|
|
// ── Helper ─────────────────────────────────────────────────────
|
|
|
|
private function createRealExportService(): EntityExportService {
|
|
return new EntityExportService(
|
|
$this->createMock(MemberMapper::class),
|
|
$this->createMock(FamilyMapper::class),
|
|
$this->createMock(AddressMapper::class),
|
|
$this->createMock(PhoneMapper::class),
|
|
$this->createMock(EmailMapper::class),
|
|
$this->createMock(StufeMapper::class),
|
|
$this->createMock(StufeHistoryMapper::class),
|
|
$this->createMock(FeeRuleMapper::class),
|
|
$this->createMock(FeeRecordMapper::class),
|
|
$this->createMock(LagerMapper::class),
|
|
$this->createMock(LagerTeilnehmerMapper::class),
|
|
$this->createMock(InjuryMapper::class),
|
|
$this->createMock(SavedQueryMapper::class),
|
|
$this->createMock(EncryptionService::class),
|
|
$this->createMock(LoggerInterface::class)
|
|
);
|
|
}
|
|
}
|