241 lines
8.8 KiB
PHP
241 lines
8.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Command;
|
|
|
|
use App\Command\MyEpImportCommand;
|
|
use App\MyEp\Api\ApiException;
|
|
use App\MyEp\Import\DestinationRow;
|
|
use App\MyEp\Import\TravelExport;
|
|
use App\MyEp\Import\TravelExportClient;
|
|
use Doctrine\DBAL\Connection;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\MockObject\Stub;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Tester\CommandTester;
|
|
|
|
/**
|
|
* The import writes a destination's cost unit and country from the export, on update as well as
|
|
* on insert, so it can only run once MyE&P reports both fields. Until it does, a run would blank
|
|
* them on the 2,206 destinations that carry a cost unit and the 2,259 that carry a country -
|
|
* silently, because nothing reads them back until an invoice is stamped. Most of these tests pin
|
|
* the refusal, and that it happens before any write.
|
|
*/
|
|
class MyEpImportCommandTest extends TestCase
|
|
{
|
|
public function testItRefusesToRunWhenNoRowReportsACostUnit(): void
|
|
{
|
|
$tester = $this->runImport([$this->row(costUnit: null, country: 'ch')]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('Kostenstellen', $tester->getDisplay());
|
|
$this->assertStringNotContainsString('Länderkennungen', $tester->getDisplay());
|
|
}
|
|
|
|
public function testItRefusesToRunWhenNoRowReportsACountryCode(): void
|
|
{
|
|
$tester = $this->runImport([$this->row(costUnit: '4711', country: null)]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('Länderkennungen', $tester->getDisplay());
|
|
}
|
|
|
|
public function testItNamesBothFieldsWhenNeitherIsReported(): void
|
|
{
|
|
$tester = $this->runImport([$this->row(costUnit: null, country: null)]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('Kostenstellen und Länderkennungen', $tester->getDisplay());
|
|
}
|
|
|
|
public function testAnEmptyStringDoesNotCountAsAReportedField(): void
|
|
{
|
|
$tester = $this->runImport([$this->row(costUnit: '', country: '')]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('Kostenstellen und Länderkennungen', $tester->getDisplay());
|
|
}
|
|
|
|
/**
|
|
* Most hotels legitimately have no kostenstelle upstream, so the guard asks whether the field
|
|
* is reported at all, not whether every row fills it.
|
|
*/
|
|
public function testOneRowCarryingAFieldIsEnoughToPassTheGuard(): void
|
|
{
|
|
$tester = $this->runImport(
|
|
[$this->row(costUnit: null, country: null), $this->row(costUnit: '4711', country: 'ch')],
|
|
$this->writingConnection(existingId: 7),
|
|
);
|
|
|
|
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
|
|
$this->assertStringNotContainsString('Kostenstellen', $tester->getDisplay());
|
|
}
|
|
|
|
/**
|
|
* A run that touches nothing and reports success reads as "nothing changed upstream" rather
|
|
* than as a failure to read the export at all.
|
|
*/
|
|
public function testItRefusesToRunOnAnEmptyExport(): void
|
|
{
|
|
$tester = $this->runImport([]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('keine Reisen', $tester->getDisplay());
|
|
}
|
|
|
|
public function testItRefusesToRunWhenTheExportCannotBeRead(): void
|
|
{
|
|
$client = $this->createStub(TravelExportClient::class);
|
|
$client->method('fetchAll')->willThrowException(new ApiException('gateway timeout'));
|
|
|
|
$tester = new CommandTester(new MyEpImportCommand(
|
|
$this->refusingConnection(),
|
|
$client,
|
|
new NullLogger(),
|
|
));
|
|
$tester->execute([]);
|
|
|
|
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
|
|
$this->assertStringContainsString('konnte nicht von MyE&P geladen werden', $tester->getDisplay());
|
|
}
|
|
|
|
public function testAKnownPairIsUpdatedRatherThanInserted(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchOne')->willReturn(42);
|
|
$connection->expects($this->once())->method('update')->with(
|
|
'destination',
|
|
$this->callback(function (array $payload): bool {
|
|
// the hotel name and code are the point: leaving them out of the update was what
|
|
// let a renamed hotel keep a stale name forever and drop out of house matching
|
|
$this->assertArrayHasKey('hotel', $payload);
|
|
$this->assertArrayHasKey('hotel_code', $payload);
|
|
$this->assertArrayNotHasKey('bus_pro_id', $payload);
|
|
|
|
return true;
|
|
}),
|
|
['id' => 42],
|
|
);
|
|
$connection->expects($this->never())->method('insert');
|
|
|
|
$tester = $this->runImport([$this->row(costUnit: '4711', country: 'ch')], $connection);
|
|
|
|
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
|
|
$this->assertStringContainsString('0 Destinationen angelegt, 1 aktualisiert', $tester->getDisplay());
|
|
}
|
|
|
|
public function testAnUnknownPairIsInserted(): void
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->method('fetchOne')->willReturn(false);
|
|
$connection->expects($this->once())->method('insert')->with(
|
|
'destination',
|
|
$this->callback(function (array $payload): bool {
|
|
$this->assertSame(12778, $payload['bus_pro_id']);
|
|
$this->assertSame(222928, $payload['hotel_bus_pro_id']);
|
|
$this->assertSame('ch', $payload['country']);
|
|
|
|
return true;
|
|
}),
|
|
);
|
|
$connection->expects($this->never())->method('update');
|
|
|
|
$tester = $this->runImport([$this->row(costUnit: '4711', country: 'ch')], $connection);
|
|
|
|
$this->assertStringContainsString('1 Destinationen angelegt, 0 aktualisiert', $tester->getDisplay());
|
|
}
|
|
|
|
/**
|
|
* Both counters are losses MyE&P made on its side and would otherwise not surface anywhere.
|
|
*/
|
|
public function testTheOperatorIsToldWhatMyEpDropped(): void
|
|
{
|
|
$tester = $this->runImport(
|
|
[$this->row(costUnit: '4711', country: 'ch')],
|
|
$this->writingConnection(existingId: 7),
|
|
skipped: 3,
|
|
pickupsWithoutMasterData: 21,
|
|
);
|
|
|
|
$display = $tester->getDisplay();
|
|
|
|
$this->assertStringContainsString('3 Reise-Hotel-Kombinationen verworfen', $display);
|
|
$this->assertStringContainsString('21 Zustiege ohne Stammdaten', $display);
|
|
}
|
|
|
|
public function testACleanExportSaysNothingAboutDroppedRows(): void
|
|
{
|
|
$tester = $this->runImport(
|
|
[$this->row(costUnit: '4711', country: 'ch')],
|
|
$this->writingConnection(existingId: 7),
|
|
);
|
|
|
|
$this->assertStringNotContainsString('verworfen', $tester->getDisplay());
|
|
}
|
|
|
|
/**
|
|
* @param DestinationRow[] $rows
|
|
*/
|
|
private function runImport(
|
|
array $rows,
|
|
?Connection $connection = null,
|
|
int $skipped = 0,
|
|
int $pickupsWithoutMasterData = 0,
|
|
): CommandTester {
|
|
$client = $this->createStub(TravelExportClient::class);
|
|
$client->method('fetchAll')->willReturn(
|
|
new TravelExport($rows, $skipped, $pickupsWithoutMasterData, '2026-09-29T03:20:00+02:00')
|
|
);
|
|
|
|
$tester = new CommandTester(new MyEpImportCommand(
|
|
$connection ?? $this->refusingConnection(),
|
|
$client,
|
|
new NullLogger(),
|
|
));
|
|
|
|
$tester->execute([]);
|
|
|
|
return $tester;
|
|
}
|
|
|
|
/**
|
|
* Any query at all means the guard let a write through, which is the whole failure the
|
|
* refusal tests cover, so the connection fails the test rather than the command.
|
|
*/
|
|
private function refusingConnection(): Connection&MockObject
|
|
{
|
|
$connection = $this->createMock(Connection::class);
|
|
$connection->expects($this->never())->method($this->anything());
|
|
|
|
return $connection;
|
|
}
|
|
|
|
private function writingConnection(int $existingId): Connection&Stub
|
|
{
|
|
$connection = $this->createStub(Connection::class);
|
|
$connection->method('fetchOne')->willReturn($existingId);
|
|
|
|
return $connection;
|
|
}
|
|
|
|
private function row(?string $costUnit, ?string $country): DestinationRow
|
|
{
|
|
return new DestinationRow(
|
|
12778,
|
|
'DWWSCH070227',
|
|
'Davos - Sportclub Weissfluh',
|
|
new \DateTimeImmutable('2027-02-07'),
|
|
new \DateTimeImmutable('2027-02-12'),
|
|
222928,
|
|
'DWWGRU2',
|
|
'Sportclub Weißfluh',
|
|
$costUnit,
|
|
$country,
|
|
[],
|
|
);
|
|
}
|
|
}
|