release 1.0
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
|
||||
namespace UbsCsvTransformer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use UbsCsvTransformer\ColumnTransformer;
|
||||
use UbsCsvTransformer\DebugLogger;
|
||||
|
||||
class ColumnTransformerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
DebugLogger::reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: build a transformer with one rule and apply it to the given row.
|
||||
*/
|
||||
private function applyOne(array $config, array $row, array $metadata = []): array
|
||||
{
|
||||
return (new ColumnTransformer([$config], $metadata))->transformRow($row);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// map
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testMapPassthrough(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Name', 'outputColumn' => 'Name', 'type' => 'map'],
|
||||
['Name' => 'Alice']
|
||||
);
|
||||
$this->assertSame('Alice', $result['Name']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// replace
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testReplace(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'replace',
|
||||
'search' => 'foo',
|
||||
'replace' => 'bar',
|
||||
], ['Col' => 'foo baz foo']);
|
||||
$this->assertSame('bar baz bar', $result['Col']);
|
||||
}
|
||||
|
||||
public function testReplaceEmptySearchReturnsOriginal(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'replace',
|
||||
'search' => '',
|
||||
'replace' => 'bar',
|
||||
], ['Col' => 'hello']);
|
||||
$this->assertSame('hello', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// dateformat
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testDateFormat(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Date',
|
||||
'outputColumn' => 'Date',
|
||||
'type' => 'dateformat',
|
||||
'fromFormat' => 'd.m.Y',
|
||||
'toFormat' => 'Y-m-d',
|
||||
], ['Date' => '15.03.2024']);
|
||||
$this->assertSame('2024-03-15', $result['Date']);
|
||||
}
|
||||
|
||||
public function testDateFormatInvalidValueReturnsOriginal(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Date',
|
||||
'outputColumn' => 'Date',
|
||||
'type' => 'dateformat',
|
||||
'fromFormat' => 'd.m.Y',
|
||||
'toFormat' => 'Y-m-d',
|
||||
], ['Date' => 'not-a-date']);
|
||||
$this->assertSame('not-a-date', $result['Date']);
|
||||
}
|
||||
|
||||
public function testDateFormatEmptyValueReturnsEmpty(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Date',
|
||||
'outputColumn' => 'Date',
|
||||
'type' => 'dateformat',
|
||||
'fromFormat' => 'd.m.Y',
|
||||
'toFormat' => 'Y-m-d',
|
||||
], ['Date' => '']);
|
||||
$this->assertSame('', $result['Date']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// split
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testSplitPart0(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'split',
|
||||
'delimiter' => ';',
|
||||
'part' => 0,
|
||||
], ['Col' => 'Coop Pronto;7007 Chur']);
|
||||
$this->assertSame('Coop Pronto', $result['Col']);
|
||||
}
|
||||
|
||||
public function testSplitPart1(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'split',
|
||||
'delimiter' => ';',
|
||||
'part' => 1,
|
||||
], ['Col' => 'Coop Pronto;7007 Chur']);
|
||||
$this->assertSame('7007 Chur', $result['Col']);
|
||||
}
|
||||
|
||||
public function testSplitPartOutOfBoundsReturnsOriginal(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'split',
|
||||
'delimiter' => ';',
|
||||
'part' => 5,
|
||||
], ['Col' => 'A;B']);
|
||||
$this->assertSame('A;B', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// regexextract
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testRegexExtract(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Zip',
|
||||
'type' => 'regexextract',
|
||||
'pattern' => '(\d{4})',
|
||||
], ['Col' => 'Shop 7007 Chur', 'Zip' => '']);
|
||||
$this->assertSame('7007', $result['Zip']);
|
||||
}
|
||||
|
||||
public function testRegexExtractNoMatchReturnsEmpty(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Zip',
|
||||
'type' => 'regexextract',
|
||||
'pattern' => '(\d{4})',
|
||||
], ['Col' => 'No digits here', 'Zip' => '']);
|
||||
$this->assertSame('', $result['Zip']);
|
||||
}
|
||||
|
||||
public function testRegexExtractEmptyValueReturnsEmpty(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Zip',
|
||||
'type' => 'regexextract',
|
||||
'pattern' => '(\d{4})',
|
||||
], ['Col' => '', 'Zip' => '']);
|
||||
$this->assertSame('', $result['Zip']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// trim
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testTrim(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'trim'],
|
||||
['Col' => ' hello world ']
|
||||
);
|
||||
$this->assertSame('hello world', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// uppercase
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testUppercase(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'uppercase'],
|
||||
['Col' => 'Hello World']
|
||||
);
|
||||
$this->assertSame('HELLO WORLD', $result['Col']);
|
||||
}
|
||||
|
||||
public function testUppercaseUnicode(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'uppercase'],
|
||||
['Col' => 'zürich']
|
||||
);
|
||||
$this->assertSame('ZÜRICH', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// lowercase
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testLowercase(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'lowercase'],
|
||||
['Col' => 'Hello World']
|
||||
);
|
||||
$this->assertSame('hello world', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ucwordsfirst
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testUcwordsFirst(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'],
|
||||
['Col' => 'COOP PRONTO CHUR']
|
||||
);
|
||||
$this->assertSame('Coop Pronto Chur', $result['Col']);
|
||||
}
|
||||
|
||||
public function testUcwordsFirstHyphen(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'],
|
||||
['Col' => 'SAINT-JEAN-DE-MAURIENNE']
|
||||
);
|
||||
$this->assertSame('Saint-Jean-De-Maurienne', $result['Col']);
|
||||
}
|
||||
|
||||
public function testUcwordsFirstApostrophe(): void
|
||||
{
|
||||
$result = $this->applyOne(
|
||||
['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'],
|
||||
['Col' => "O'NEILL STORE"]
|
||||
);
|
||||
$this->assertSame("O'Neill Store", $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// truncate
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testTruncate(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'truncate',
|
||||
'maxLength' => 5,
|
||||
], ['Col' => 'Hello World']);
|
||||
$this->assertSame('Hello', $result['Col']);
|
||||
}
|
||||
|
||||
public function testTruncateShorterThanMaxIsUnchanged(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'truncate',
|
||||
'maxLength' => 100,
|
||||
], ['Col' => 'Short']);
|
||||
$this->assertSame('Short', $result['Col']);
|
||||
}
|
||||
|
||||
public function testTruncateUnicode(): void
|
||||
{
|
||||
// 'ü' counts as 1 Unicode character, so maxLength=3 gives 3 chars: Z, ü, r
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'truncate',
|
||||
'maxLength' => 3,
|
||||
], ['Col' => 'Zürich']);
|
||||
$this->assertSame('Zür', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// constantvalue
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testConstantValue(): void
|
||||
{
|
||||
$transformer = new ColumnTransformer([[
|
||||
'sourceColumn' => '_constant_',
|
||||
'outputColumn' => 'Currency',
|
||||
'type' => 'constantvalue',
|
||||
'metadataKey' => 'currency_code',
|
||||
]], ['currency_code' => 'CHF']);
|
||||
$result = $transformer->transformRow(['Currency' => '']);
|
||||
$this->assertSame('CHF', $result['Currency']);
|
||||
}
|
||||
|
||||
public function testConstantValueMissingKeyReturnsEmpty(): void
|
||||
{
|
||||
$transformer = new ColumnTransformer([[
|
||||
'sourceColumn' => '_constant_',
|
||||
'outputColumn' => 'Currency',
|
||||
'type' => 'constantvalue',
|
||||
'metadataKey' => 'nonexistent',
|
||||
]], []);
|
||||
$result = $transformer->transformRow(['Currency' => '']);
|
||||
$this->assertSame('', $result['Currency']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// pipeline
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testPipeline(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'pipeline',
|
||||
'steps' => [
|
||||
['type' => 'trim'],
|
||||
['type' => 'lowercase'],
|
||||
['type' => 'ucwordsfirst'],
|
||||
],
|
||||
], ['Col' => ' COOP PRONTO ']);
|
||||
$this->assertSame('Coop Pronto', $result['Col']);
|
||||
}
|
||||
|
||||
public function testPipelineEmptyStepsReturnsOriginal(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'pipeline',
|
||||
'steps' => [],
|
||||
], ['Col' => 'hello']);
|
||||
$this->assertSame('hello', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Inline transformations[] array (flat pipeline per column entry)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testInlineTransformationsArray(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'map',
|
||||
'transformations' => [
|
||||
['type' => 'trim'],
|
||||
['type' => 'uppercase'],
|
||||
],
|
||||
], ['Col' => ' hello ']);
|
||||
$this->assertSame('HELLO', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// normalizeTransformType: snake_case and kebab-case aliases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testNormalizeTypeSnakeCase(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Date',
|
||||
'outputColumn' => 'Date',
|
||||
'type' => 'date_format',
|
||||
'fromFormat' => 'd.m.Y',
|
||||
'toFormat' => 'Y-m-d',
|
||||
], ['Date' => '15.03.2024']);
|
||||
$this->assertSame('2024-03-15', $result['Date']);
|
||||
}
|
||||
|
||||
public function testNormalizeTypeKebabCase(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'Col',
|
||||
'outputColumn' => 'Col',
|
||||
'type' => 'ucwords-first',
|
||||
], ['Col' => 'HELLO WORLD']);
|
||||
$this->assertSame('Hello World', $result['Col']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// outputAction
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testOutputActionOverwrite(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'A',
|
||||
'outputColumn' => 'B',
|
||||
'type' => 'map',
|
||||
'outputAction' => 'overwrite',
|
||||
], ['A' => 'new', 'B' => 'old']);
|
||||
$this->assertSame('new', $result['B']);
|
||||
}
|
||||
|
||||
public function testOutputActionCreate(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'A',
|
||||
'outputColumn' => 'NewCol',
|
||||
'type' => 'map',
|
||||
'outputAction' => 'create',
|
||||
], ['A' => 'hello']);
|
||||
$this->assertSame('hello', $result['NewCol']);
|
||||
}
|
||||
|
||||
public function testOutputActionAppend(): void
|
||||
{
|
||||
$result = $this->applyOne([
|
||||
'sourceColumn' => 'A',
|
||||
'outputColumn' => 'B',
|
||||
'type' => 'map',
|
||||
'outputAction' => 'append',
|
||||
], ['A' => ' World', 'B' => 'Hello']);
|
||||
$this->assertSame('Hello World', $result['B']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// multi-output split
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testMultiOutputSplit(): void
|
||||
{
|
||||
$transformer = new ColumnTransformer([[
|
||||
'outputs' => ['FirstName', 'LastName'],
|
||||
'sourceColumn' => 'FullName',
|
||||
'type' => 'split',
|
||||
'delimiter' => ' ',
|
||||
]]);
|
||||
$result = $transformer->transformRow(['FullName' => 'John Doe']);
|
||||
$this->assertSame('John', $result['FirstName']);
|
||||
$this->assertSame('Doe', $result['LastName']);
|
||||
}
|
||||
|
||||
public function testMultiOutputSplitFewerPartsYieldsEmptyString(): void
|
||||
{
|
||||
$transformer = new ColumnTransformer([[
|
||||
'outputs' => ['Col1', 'Col2', 'Col3'],
|
||||
'sourceColumn' => 'Source',
|
||||
'type' => 'split',
|
||||
'delimiter' => ';',
|
||||
]]);
|
||||
$result = $transformer->transformRow(['Source' => 'A;B']);
|
||||
$this->assertSame('A', $result['Col1']);
|
||||
$this->assertSame('B', $result['Col2']);
|
||||
$this->assertSame('', $result['Col3']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testMissingOutputColumnThrows(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$transformer = new ColumnTransformer([
|
||||
['sourceColumn' => 'A', 'type' => 'map'],
|
||||
]);
|
||||
$transformer->transformRow(['A' => 'x']);
|
||||
}
|
||||
|
||||
public function testMultiOutputNonSplitTypeThrows(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$transformer = new ColumnTransformer([[
|
||||
'outputs' => ['Col1', 'Col2'],
|
||||
'sourceColumn' => 'Source',
|
||||
'type' => 'uppercase',
|
||||
]]);
|
||||
$transformer->transformRow(['Source' => 'hello']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getOutputColumns
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testGetOutputColumnsCountsUniqueColumns(): void
|
||||
{
|
||||
$transformer = new ColumnTransformer([
|
||||
['sourceColumn' => 'A', 'outputColumn' => 'X', 'type' => 'map'],
|
||||
['sourceColumn' => 'B', 'outputColumn' => 'Y', 'type' => 'map'],
|
||||
['sourceColumn' => 'C', 'outputColumn' => 'X', 'type' => 'map'], // duplicate output
|
||||
]);
|
||||
$transformer->transformRow(['A' => '1', 'B' => '2', 'C' => '3']);
|
||||
$this->assertSame(2, $transformer->getOutputColumns());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace UbsCsvTransformer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use UbsCsvTransformer\ConfigurationLoader;
|
||||
use UbsCsvTransformer\TransformerEngine;
|
||||
|
||||
/**
|
||||
* Integrations-Tests für Konfigurationsdateien
|
||||
*
|
||||
* Prüft, dass jede Konfigurationsdatei mit ihrem Fixture-Input
|
||||
* die erwartete CSV-Ausgabe produziert (Golden-File-Test).
|
||||
*
|
||||
* Fixtures liegen unter tests/fixtures/<config-name>/:
|
||||
* - input.csv → minimaler CSV-Input passend zur Konfiguration
|
||||
* - expected.csv → erwartete Ausgabe nach Transformation
|
||||
*
|
||||
* Neue Fixtures hinzufügen:
|
||||
* 1. Verzeichnis tests/fixtures/<config-name>/ anlegen
|
||||
* 2. input.csv und expected.csv ablegen
|
||||
* 3. sicherstellen dass config/<config-name>.json existiert
|
||||
*/
|
||||
class ConfigIntegrationTest extends TestCase
|
||||
{
|
||||
/** @var string Temporäres Output-Verzeichnis für diesen Test-Lauf */
|
||||
private string $tempOutputDir = '';
|
||||
|
||||
/** @var string Temporäre Konfigurationsdatei mit überschriebenem Output-Pfad */
|
||||
private string $tempConfigFile = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempOutputDir = sys_get_temp_dir() . '/ubscsv_test_' . uniqid('', true);
|
||||
mkdir($this->tempOutputDir, 0755, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->tempOutputDir !== '' && is_dir($this->tempOutputDir)) {
|
||||
foreach (glob($this->tempOutputDir . '/*') ?: [] as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
rmdir($this->tempOutputDir);
|
||||
}
|
||||
|
||||
if ($this->tempConfigFile !== '' && file_exists($this->tempConfigFile)) {
|
||||
unlink($this->tempConfigFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle Fixture-Paare als DataProvider
|
||||
*
|
||||
* Entdeckt automatisch alle Unterverzeichnisse in tests/fixtures/
|
||||
* und erwartet für jedes eine passende config/<name>.json.
|
||||
*
|
||||
* @return array<string, array{configName: string, fixtureDir: string}>
|
||||
*/
|
||||
public static function fixtureProvider(): array
|
||||
{
|
||||
$fixtureBase = __DIR__ . '/fixtures';
|
||||
$dirs = glob($fixtureBase . '/*', GLOB_ONLYDIR);
|
||||
$cases = [];
|
||||
|
||||
foreach ($dirs ?: [] as $dir) {
|
||||
$configName = basename($dir);
|
||||
$cases[$configName] = [
|
||||
'configName' => $configName,
|
||||
'fixtureDir' => $dir,
|
||||
];
|
||||
}
|
||||
|
||||
return $cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft dass eine Konfigurationsdatei mit dem Fixture-Input die erwartete Ausgabe erzeugt
|
||||
*
|
||||
* @dataProvider fixtureProvider
|
||||
*/
|
||||
public function testConfigProducesExpectedOutput(string $configName, string $fixtureDir): void
|
||||
{
|
||||
$realConfigPath = __DIR__ . '/../config/' . $configName . '.json';
|
||||
$inputFile = $fixtureDir . '/input.csv';
|
||||
$expectedFile = $fixtureDir . '/expected.csv';
|
||||
|
||||
$this->assertFileExists($realConfigPath, "Config-Datei nicht gefunden: {$realConfigPath}");
|
||||
$this->assertFileExists($inputFile, "Fixture input.csv fehlt: {$inputFile}");
|
||||
$this->assertFileExists($expectedFile, "Fixture expected.csv fehlt: {$expectedFile}");
|
||||
|
||||
// Temporäre Konfiguration mit überschriebenem Output-Verzeichnis erstellen
|
||||
$rawConfig = file_get_contents($realConfigPath);
|
||||
$this->assertNotFalse($rawConfig, "Konfigurationsdatei konnte nicht gelesen werden");
|
||||
|
||||
/** @var array<string, mixed> $configData */
|
||||
$configData = json_decode((string) $rawConfig, true);
|
||||
$this->assertIsArray($configData, "Ungültiges JSON in Konfigurationsdatei");
|
||||
|
||||
$configData['directories']['output'] = $this->tempOutputDir;
|
||||
|
||||
$this->tempConfigFile = sys_get_temp_dir() . '/ubscsv_config_' . uniqid('', true) . '.json';
|
||||
file_put_contents($this->tempConfigFile, json_encode($configData, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// Transformation ausführen
|
||||
$loader = new ConfigurationLoader($this->tempConfigFile);
|
||||
$loader->load();
|
||||
$engine = new TransformerEngine($loader);
|
||||
$result = $engine->transform($inputFile);
|
||||
|
||||
$this->assertTrue(
|
||||
$result['success'],
|
||||
"Transformation fehlgeschlagen: " . ($result['error'] ?? 'unbekannter Fehler')
|
||||
);
|
||||
|
||||
$outputFile = $result['outputFile'];
|
||||
$this->assertFileExists($outputFile, "Output-Datei wurde nicht erstellt: {$outputFile}");
|
||||
|
||||
$actual = rtrim((string) file_get_contents($outputFile));
|
||||
$expected = rtrim((string) file_get_contents($expectedFile));
|
||||
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
$actual,
|
||||
"CSV-Output stimmt nicht mit expected.csv überein.\n" .
|
||||
"Zum Aktualisieren: php bin/transformer.php transform {$inputFile} {$realConfigPath} " .
|
||||
"(Output nach {$expectedFile} kopieren)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace UbsCsvTransformer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use UbsCsvTransformer\ConfigurationLoader;
|
||||
|
||||
class ConfigurationLoaderTest extends TestCase
|
||||
{
|
||||
private string $tempFile = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// uniqid gives a unique name; .json extension satisfies the loader's check
|
||||
$this->tempFile = sys_get_temp_dir() . '/cfgloader_test_' . uniqid() . '.json';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->tempFile)) {
|
||||
unlink($this->tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private function writeJson(array $data): void
|
||||
{
|
||||
file_put_contents($this->tempFile, json_encode($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal valid configuration that passes all validation checks.
|
||||
*/
|
||||
private function validConfig(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => [
|
||||
'extractionRules' => [
|
||||
['name' => 'account_iban', 'lineNumber' => 1, 'regex' => '(.+)'],
|
||||
],
|
||||
],
|
||||
'csvStructure' => [
|
||||
'headerLine' => 1,
|
||||
'inputDelimiter' => ';',
|
||||
'encoding' => 'UTF-8',
|
||||
],
|
||||
'columnTransformations' => [
|
||||
['sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Happy path
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testLoadValidConfigReturnsArray(): void
|
||||
{
|
||||
$this->writeJson($this->validConfig());
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$config = $loader->load();
|
||||
$this->assertIsArray($config);
|
||||
$this->assertArrayHasKey('metadata', $config);
|
||||
$this->assertArrayHasKey('csvStructure', $config);
|
||||
$this->assertArrayHasKey('columnTransformations', $config);
|
||||
}
|
||||
|
||||
public function testGetAllReturnsFullConfig(): void
|
||||
{
|
||||
$this->writeJson($this->validConfig());
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$loader->load();
|
||||
$all = $loader->getAll();
|
||||
$this->assertSame(1, $all['csvStructure']['headerLine']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// get() dot-notation accessor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testGetDotNotationReturnsValue(): void
|
||||
{
|
||||
$this->writeJson($this->validConfig());
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$loader->load();
|
||||
$this->assertSame(1, $loader->get('csvStructure.headerLine'));
|
||||
$this->assertSame(';', $loader->get('csvStructure.inputDelimiter'));
|
||||
}
|
||||
|
||||
public function testGetNonExistentKeyReturnsNull(): void
|
||||
{
|
||||
$this->writeJson($this->validConfig());
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$loader->load();
|
||||
$this->assertNull($loader->get('nonexistent.key'));
|
||||
}
|
||||
|
||||
public function testGetNonExistentKeyReturnsDefault(): void
|
||||
{
|
||||
$this->writeJson($this->validConfig());
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$loader->load();
|
||||
$this->assertSame('fallback', $loader->get('nonexistent', 'fallback'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// File not found / bad extension
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFileNotFoundThrows(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$loader = new ConfigurationLoader('/nonexistent/path/config.json');
|
||||
$loader->load();
|
||||
}
|
||||
|
||||
public function testNonJsonExtensionThrows(): void
|
||||
{
|
||||
$yamlPath = sys_get_temp_dir() . '/test_config_' . uniqid() . '.yaml';
|
||||
file_put_contents($yamlPath, 'key: value');
|
||||
try {
|
||||
$loader = new ConfigurationLoader($yamlPath);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$loader->load();
|
||||
} finally {
|
||||
if (file_exists($yamlPath)) {
|
||||
unlink($yamlPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidJsonThrows(): void
|
||||
{
|
||||
file_put_contents($this->tempFile, '{invalid json content}');
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Required top-level sections
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testMissingMetadataSectionThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
unset($config['metadata']);
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
public function testMissingExtractionRulesThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
unset($config['metadata']['extractionRules']);
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
public function testMissingCsvStructureSectionThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
unset($config['csvStructure']);
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
public function testMissingHeaderLineThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
unset($config['csvStructure']['headerLine']);
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
public function testMissingColumnTransformationsThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
unset($config['columnTransformations']);
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// csvStructure value validation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testInvalidEncodingThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
$config['csvStructure']['encoding'] = 'LATIN1';
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\Exception::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
public function testSupportedEncodingsPass(): void
|
||||
{
|
||||
foreach (['UTF-8', 'ISO-8859-1', 'CP1252'] as $encoding) {
|
||||
$config = $this->validConfig();
|
||||
$config['csvStructure']['encoding'] = $encoding;
|
||||
$this->writeJson($config);
|
||||
$loader = new ConfigurationLoader($this->tempFile);
|
||||
$result = $loader->load();
|
||||
$this->assertSame($encoding, $result['csvStructure']['encoding']);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// directories validation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testDirectoriesAllKeysPass(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
$config['directories'] = [
|
||||
'source' => '/tmp/source',
|
||||
'output' => '/tmp/output',
|
||||
'archive' => '/tmp/archive',
|
||||
'error' => '/tmp/error',
|
||||
];
|
||||
$this->writeJson($config);
|
||||
$result = (new ConfigurationLoader($this->tempFile))->load();
|
||||
$this->assertArrayHasKey('directories', $result);
|
||||
}
|
||||
|
||||
public function testDirectoriesMissingSubkeyThrows(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
$config['directories'] = [
|
||||
'source' => '/tmp/source',
|
||||
'output' => '/tmp/output',
|
||||
// 'archive' and 'error' intentionally missing
|
||||
];
|
||||
$this->writeJson($config);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
(new ConfigurationLoader($this->tempFile))->load();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Absent optional 'directories' key does not trigger validation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testAbsentDirectoriesKeyDoesNotThrow(): void
|
||||
{
|
||||
$config = $this->validConfig();
|
||||
$this->assertArrayNotHasKey('directories', $config);
|
||||
$this->writeJson($config);
|
||||
$result = (new ConfigurationLoader($this->tempFile))->load();
|
||||
$this->assertArrayNotHasKey('directories', $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace UbsCsvTransformer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use UbsCsvTransformer\CsvReader;
|
||||
|
||||
class CsvReaderTest extends TestCase
|
||||
{
|
||||
private string $tempFile = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempFile = tempnam(sys_get_temp_dir(), 'csvreader_test_');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if (file_exists($this->tempFile)) {
|
||||
unlink($this->tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private function write(string $content): void
|
||||
{
|
||||
file_put_contents($this->tempFile, $content);
|
||||
}
|
||||
|
||||
private function reader(int $headerLine = 1, string $delimiter = ';', bool $hasBom = false): CsvReader
|
||||
{
|
||||
return new CsvReader(
|
||||
$this->tempFile,
|
||||
['inputDelimiter' => $delimiter, 'headerLine' => $headerLine, 'hasBom' => $hasBom]
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// readCsvData – basic parsing
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testReadCsvDataSimple(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\nBob;25\n");
|
||||
$data = $this->reader()->readCsvData();
|
||||
$this->assertCount(2, $data);
|
||||
$this->assertSame('Alice', $data[0]['Name']);
|
||||
$this->assertSame('30', $data[0]['Age']);
|
||||
$this->assertSame('Bob', $data[1]['Name']);
|
||||
}
|
||||
|
||||
public function testReadCsvDataTrimsWhitespace(): void
|
||||
{
|
||||
$this->write("Name ; Age\n Alice ; 30 \n");
|
||||
$data = $this->reader()->readCsvData();
|
||||
$this->assertSame('Alice', $data[0]['Name']);
|
||||
$this->assertSame('30', $data[0]['Age']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// headerLine offset
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testHeaderLineOffset(): void
|
||||
{
|
||||
// Lines 1+2 are metadata, header is line 3
|
||||
$this->write("Meta1\nMeta2\nColA;ColB\nval1;val2\n");
|
||||
$data = $this->reader(3)->readCsvData();
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertSame('val1', $data[0]['ColA']);
|
||||
$this->assertSame('val2', $data[0]['ColB']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// readMetadataLines
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testReadMetadataLines(): void
|
||||
{
|
||||
$this->write("Line1\nLine2\nHeader;Col\nData;Row\n");
|
||||
$meta = $this->reader(3)->readMetadataLines();
|
||||
$this->assertCount(2, $meta);
|
||||
$this->assertSame('Line1', $meta[0]);
|
||||
$this->assertSame('Line2', $meta[1]);
|
||||
}
|
||||
|
||||
public function testReadMetadataLinesHeaderOnLine1IsEmpty(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\n");
|
||||
$meta = $this->reader(1)->readMetadataLines();
|
||||
$this->assertSame([], $meta);
|
||||
}
|
||||
|
||||
public function testReadMetadataLinesHeaderOnLine2ReturnsOneLine(): void
|
||||
{
|
||||
$this->write("MetaInfo\nName;Age\nAlice;30\n");
|
||||
$meta = $this->reader(2)->readMetadataLines();
|
||||
$this->assertCount(1, $meta);
|
||||
$this->assertSame('MetaInfo', $meta[0]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// maxDataRows limit
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testMaxDataRowsLimit(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\nBob;25\nCarol;20\n");
|
||||
$data = $this->reader()->readCsvData(2);
|
||||
$this->assertCount(2, $data);
|
||||
$this->assertSame('Alice', $data[0]['Name']);
|
||||
$this->assertSame('Bob', $data[1]['Name']);
|
||||
}
|
||||
|
||||
public function testMaxDataRowsZeroMeansAll(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\nBob;25\nCarol;20\n");
|
||||
$data = $this->reader()->readCsvData(0);
|
||||
$this->assertCount(3, $data);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Empty line skipping
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testEmptyLinesAreSkipped(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\n\nBob;25\n\n");
|
||||
$data = $this->reader()->readCsvData();
|
||||
$this->assertCount(2, $data);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// BOM removal
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testBomIsRemovedFromFirstColumnName(): void
|
||||
{
|
||||
// UTF-8 BOM followed immediately by the header
|
||||
$this->write("\xEF\xBB\xBFName;Age\nAlice;30\n");
|
||||
$data = $this->reader(1, ';', true)->readCsvData();
|
||||
$this->assertCount(1, $data);
|
||||
// The column must be 'Name', not the BOM-prefixed version
|
||||
$this->assertArrayHasKey('Name', $data[0]);
|
||||
$this->assertSame('Alice', $data[0]['Name']);
|
||||
}
|
||||
|
||||
public function testNoBomFlagLeavesHeaderIntact(): void
|
||||
{
|
||||
// If hasBom is false, BOM bytes stay and the key will be mangled — we only
|
||||
// assert that the clean path (hasBom=true) works, tested above.
|
||||
// Here we just verify normal CSV without BOM also works when hasBom=false.
|
||||
$this->write("Name;Age\nAlice;30\n");
|
||||
$data = $this->reader(1, ';', false)->readCsvData();
|
||||
$this->assertArrayHasKey('Name', $data[0]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getHeaders
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testGetHeaders(): void
|
||||
{
|
||||
$this->write("Col1;Col2;Col3\nA;B;C\n");
|
||||
$headers = $this->reader()->getHeaders();
|
||||
$this->assertSame(['Col1', 'Col2', 'Col3'], $headers);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Row with fewer columns than headers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testShortRowPaddedWithEmpty(): void
|
||||
{
|
||||
$this->write("A;B;C\n1;2\n");
|
||||
$data = $this->reader()->readCsvData();
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertSame('1', $data[0]['A']);
|
||||
$this->assertSame('2', $data[0]['B']);
|
||||
$this->assertSame('', $data[0]['C']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testFileNotFoundThrowsRuntimeException(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$reader = new CsvReader('/nonexistent/path/file.csv', ['inputDelimiter' => ';', 'headerLine' => 1]);
|
||||
$reader->readLines();
|
||||
}
|
||||
|
||||
public function testHeaderLineBeyondFileLengthThrows(): void
|
||||
{
|
||||
$this->write("Name;Age\nAlice;30\n");
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->reader(99)->readCsvData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace UbsCsvTransformer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use UbsCsvTransformer\DebugLogger;
|
||||
use UbsCsvTransformer\MetadataExtractor;
|
||||
|
||||
class MetadataExtractorTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
DebugLogger::reset();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Happy path
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExtractBasicCapture(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'account_iban',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '([A-Z]{2}\d{2}[\w]+)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['Account: CH9300762011623852957']);
|
||||
$this->assertSame('CH9300762011623852957', $result['account_iban']);
|
||||
}
|
||||
|
||||
public function testExtractFromSecondLine(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'currency',
|
||||
'lineNumber' => 2,
|
||||
'regex' => '(CHF|EUR|USD)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$lines = ['Account info', 'Currency: CHF'];
|
||||
$result = $extractor->extract($lines);
|
||||
$this->assertSame('CHF', $result['currency']);
|
||||
}
|
||||
|
||||
public function testExtractCaptureGroup0ReturnsFullMatch(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'label',
|
||||
'lineNumber' => 1,
|
||||
'regex' => 'CHF',
|
||||
'captureGroup' => 0,
|
||||
]]);
|
||||
$result = $extractor->extract(['Balance: CHF 1000']);
|
||||
$this->assertSame('CHF', $result['label']);
|
||||
}
|
||||
|
||||
public function testExtractMultipleRules(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([
|
||||
[
|
||||
'name' => 'iban',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '([A-Z]{2}\d{2}\w+)',
|
||||
'captureGroup' => 1,
|
||||
],
|
||||
[
|
||||
'name' => 'currency',
|
||||
'lineNumber' => 2,
|
||||
'regex' => '(CHF|EUR)',
|
||||
'captureGroup' => 1,
|
||||
],
|
||||
]);
|
||||
$result = $extractor->extract(['IBAN: CH9300762011623852957', 'Currency: CHF']);
|
||||
$this->assertSame('CH9300762011623852957', $result['iban']);
|
||||
$this->assertSame('CHF', $result['currency']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Line not available
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExtractMissingLineIsSkipped(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'account_iban',
|
||||
'lineNumber' => 5,
|
||||
'regex' => '(CH\d+)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['Only one line']);
|
||||
$this->assertArrayNotHasKey('account_iban', $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Invalid regex
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExtractInvalidRegexDoesNotCrash(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'bad_rule',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '[invalid(regex',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['some line content']);
|
||||
$this->assertArrayNotHasKey('bad_rule', $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// No match
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExtractNoMatchIsSkipped(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'account_iban',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '(NOMATCH_\d+)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['Account: CH9300762011623852957']);
|
||||
$this->assertArrayNotHasKey('account_iban', $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testExtractEmptyRulesReturnsEmptyArray(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([]);
|
||||
$result = $extractor->extract(['some line']);
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testExtractEmptyLinesArrayReturnsEmpty(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'iban',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '(CH\d+)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract([]);
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testRuleWithMissingNameIsSkipped(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'regex' => '(\d+)',
|
||||
'lineNumber' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['123']);
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testRuleWithMissingRegexIsSkipped(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'test',
|
||||
'lineNumber' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['some line']);
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getRuleCount
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testGetRuleCount(): void
|
||||
{
|
||||
$extractor = new MetadataExtractor([
|
||||
['name' => 'a', 'regex' => 'x', 'lineNumber' => 1],
|
||||
['name' => 'b', 'regex' => 'y', 'lineNumber' => 2],
|
||||
['name' => 'c', 'regex' => 'z', 'lineNumber' => 3],
|
||||
]);
|
||||
$this->assertSame(3, $extractor->getRuleCount());
|
||||
}
|
||||
|
||||
public function testGetRuleCountEmptyIsZero(): void
|
||||
{
|
||||
$this->assertSame(0, (new MetadataExtractor([]))->getRuleCount());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Regex containing '#' delimiter character
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function testRegexContainingHashIsHandled(): void
|
||||
{
|
||||
// Pattern contains '#' which is the internal delimiter — must be escaped
|
||||
$extractor = new MetadataExtractor([[
|
||||
'name' => 'tag',
|
||||
'lineNumber' => 1,
|
||||
'regex' => '(#\d+)',
|
||||
'captureGroup' => 1,
|
||||
]]);
|
||||
$result = $extractor->extract(['Issue #42 resolved']);
|
||||
$this->assertSame('#42', $result['tag']);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
Belastung,Gutschrift,date,process_date,opposing_name,tags,description,opposing_account,notes,account_iban,account_currency
|
||||
-600.00,,2022-12-30,2022-12-30,"David Peter Reindl",Dauerauftrag,"Steuerrueckstellung
|
||||
David Peter Reindl;8906 Bonstetten","CH37 0026 7267 9314 35M2 P","9967864LK2659211
|
||||
8906 Bonstetten","CH18 0026 7267 9314 3540 D",CHF
|
||||
-46.35,,2022-12-30,2022-12-31,"UBS AG",,"Periode: 2022-10-01 - 2022-12-30
|
||||
Zinsabschluss",,9900365AP6356307,"CH18 0026 7267 9314 3540 D",CHF
|
||||
-39.90,,2022-12-30,2022-12-30,"Swisscom Grossunternehme",TWINT,"Swisscom Grossunternehme; Zahlung UBS TWINT",,"9967364GK5707142
|
||||
8004 Zuerich","CH18 0026 7267 9314 3540 D",CHF
|
||||
-8.75,,2022-12-28,2022-12-27,"Coop Pronto Chur",Debitkarte,"18279748-0 08/24
|
||||
Coop Pronto Chur;7007 Chur",,"9930862BN7826808
|
||||
7007 Chur","CH18 0026 7267 9314 3540 D",CHF
|
||||
-1800.00,,2022-12-27,2022-12-27,"Janine Geigele",e-banking,"Skiferien Dolomiten
|
||||
Janine Geigele;Am Wasser 36; 8049 Zuerich; CH","CH63 0023 2232 5560 5988 0","9967361TI3188436
|
||||
8049 Zuerich","CH18 0026 7267 9314 3540 D",CHF
|
||||
,9.00,2022-12-22,2022-12-22,"Friis, Daniela Silvia",TWINT,"Friis, Daniela Silvia",,9930356GK0440989,"CH18 0026 7267 9314 3540 D",CHF
|
||||
,19764.80,2022-11-25,2022-11-25,SBB,Gutschrift,"SBB;Corporate Treasury",,9901820E67741531,"CH18 0026 7267 9314 3540 D",CHF
|
||||
-14.00,,2022-08-22,2022-08-21,"Friis-Loop, Daniela",TWINT,"Friis-Loop, Daniela; Belastung UBS TWINT",,9967233GK1553933,"CH18 0026 7267 9314 3540 D",CHF
|
||||
|
+18
@@ -0,0 +1,18 @@
|
||||
Kontonummer:;0267 00931435.40;
|
||||
IBAN:;CH18 0026 7267 9314 3540 D;
|
||||
Von:;2022-01-03;
|
||||
Bis:;2022-12-30;
|
||||
Anfangssaldo:;3917.29;
|
||||
Schlusssaldo:;-238.80;
|
||||
Bewertet in:;CHF;
|
||||
Anzahl Transaktionen in diesem Zeitraum:;742;
|
||||
|
||||
Abschlussdatum;Abschlusszeit;Buchungsdatum;Valutadatum;Währung;Belastung;Gutschrift;Einzelbetrag;Saldo;Transaktions-Nr.;Beschreibung1;Beschreibung2;Beschreibung3;Fussnoten;
|
||||
2022-12-30;;2022-12-30;2022-12-30;CHF;-600.00;;;-238.80;9967864LK2659211;"""David Peter Reindl;8906 Bonstetten""";"""STEUERRUECKSTELLUNG; Dauerauftrag""";"""Konto-Nr. IBAN: CH37 0026 7267 9314 35M2 P; Transaktions-Nr. 9967864LK2659211""";;
|
||||
2022-12-30;;2022-12-30;2022-12-31;CHF;-46.35;;;361.20;9900365AP6356307;"Saldo Zinsabschluss";"Periode: 2022-10-01 - 2022-12-30";"Transaktions-Nr. 9900365AP6356307";;
|
||||
2022-12-30;;2022-12-30;2022-12-30;CHF;-39.90;;;407.55;9967364GK5707142;"""SWISSCOM GROSSUNTERNEHME; Zahlung UBS TWINT""";;"""Zahlungsgrund: Muellerstrasse 16 na, 8004 Zuerich TWINT-Acc.:+41796305690; Transaktions-Nr. 9967364GK5707142""";;
|
||||
2022-12-27;19:57:53;2022-12-28;2022-12-27;CHF;-8.75;;;7592.45;9930862BN7826808;"""Coop Pronto Chur;7007 Chur""";"""18279748-0 08/24; Zahlung Debitkarte""";"Transaktions-Nr. 9930862BN7826808";;
|
||||
2022-12-27;;2022-12-27;2022-12-27;CHF;-1800.00;;;7601.20;9967361TI3188436;"""Janine Geigele;Am Wasser 36; 8049 Zuerich; CH""";"""SKIFERIEN DOLOMITEN; e-banking-Vergütungsauftrag""";"""Zahlungsgrund: Wohnung Dolomiten, 2 Personen; Konto-Nr. IBAN: CH63 0023 2232 5560 5988 0; Kosten: E-Banking Inland; Transaktions-Nr. 9967361TI3188436""";;
|
||||
2022-12-22;;2022-12-22;2022-12-22;CHF;;9.00;;10436.45;9930356GK0440989;"Friis, Daniela Silvia";"Gutschrift UBS TWINT";"""Zahlungsgrund: +41796741245; TWINT-Acc.:+41796305690; Transaktions-Nr. 9930356GK0440989""";;
|
||||
2022-11-25;;2022-11-25;2022-11-25;CHF;;19764.80;;15748.12;9901820E67741531;"""SBB;Corporate Treasury""";"Gutschrift";"""Zahlungsgrund: Lohn/Gehalt 00229537/202211; Transaktions-Nr. 9901820E67741531""";;
|
||||
2022-08-21;;2022-08-22;2022-08-21;CHF;-14.00;;;-1544.23;9967233GK1553933;"""FRIIS-LOOP, DANIELA; Belastung UBS TWINT""";;"""Zahlungsgrund: +41796741245; TWINT-Acc.:+41796305690; Transaktions-Nr. 9967233GK1553933""";;
|
||||
|
Reference in New Issue
Block a user