Files
myep-team/tests/Command/MyEpImportCommandTest.php
T

240 lines
8.7 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\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->createMock(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->createMock(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&MockObject
{
$connection = $this->createMock(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,
[],
);
}
}