178 lines
5.5 KiB
PHP
178 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Service\Statistics;
|
|
|
|
use App\Entity\Assignment;
|
|
use App\Entity\Destination;
|
|
use App\Entity\User;
|
|
use App\Enum\StatisticsActorRole;
|
|
use App\Enum\StatisticsEventName;
|
|
use App\Service\Statistics\StatisticsRecorder;
|
|
use Doctrine\DBAL\Connection;
|
|
use Doctrine\DBAL\Exception as DbalException;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
|
|
class StatisticsRecorderTest extends TestCase
|
|
{
|
|
private Connection&MockObject $connection;
|
|
private Security&MockObject $security;
|
|
private LoggerInterface&MockObject $logger;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->connection = $this->createMock(Connection::class);
|
|
$this->security = $this->createMock(Security::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
}
|
|
|
|
public function testWritesTheEnumValueAndLeavesUnusedDimensionsNull(): void
|
|
{
|
|
$written = $this->capture();
|
|
|
|
$this->recorder()->record(StatisticsEventName::APPLICATION_CREATED, ['application_id' => 7]);
|
|
|
|
$this->assertSame('application.created', $written['data']['name']);
|
|
$this->assertSame('statistics_event', $written['table']);
|
|
$this->assertSame(7, $written['data']['application_id']);
|
|
$this->assertNull($written['data']['disposition_id']);
|
|
$this->assertNull($written['data']['hotel_code']);
|
|
}
|
|
|
|
/**
|
|
* The hotel metric is asked for by hotel, not by destination, and several destinations
|
|
* share one hotel under a SER prefix.
|
|
*/
|
|
public function testFreezesTheNormalizedHotelCodeOfTheAssignment(): void
|
|
{
|
|
$written = $this->capture();
|
|
|
|
$this->recorder()->recordForAssignment(
|
|
StatisticsEventName::ASSIGNMENT_JOB_PROFILE_CHANGED,
|
|
$this->assignmentInHotel('SERZIL'),
|
|
);
|
|
|
|
$this->assertSame('ZIL', $written['data']['hotel_code']);
|
|
}
|
|
|
|
public function testKeepsAnUnprefixedHotelCodeAsItIs(): void
|
|
{
|
|
$written = $this->capture();
|
|
|
|
$this->recorder()->recordForAssignment(
|
|
StatisticsEventName::ASSIGNMENT_JOB_PROFILE_CHANGED,
|
|
$this->assignmentInHotel('SBW'),
|
|
);
|
|
|
|
$this->assertSame('SBW', $written['data']['hotel_code']);
|
|
}
|
|
|
|
public function testAttributesAnAdministrativeUser(): void
|
|
{
|
|
$this->givenUser(['ROLE_ADMINISTRATIVE']);
|
|
|
|
$this->assertSame(StatisticsActorRole::ADMIN, $this->recorder()->resolveActorRole());
|
|
}
|
|
|
|
public function testAttributesATeamer(): void
|
|
{
|
|
$this->givenUser(['ROLE_TEAMER']);
|
|
|
|
$this->assertSame(StatisticsActorRole::TEAMER, $this->recorder()->resolveActorRole());
|
|
}
|
|
|
|
/**
|
|
* The backfill and the cron run without a token.
|
|
*/
|
|
public function testAttributesAnythingWithoutATokenToTheSystem(): void
|
|
{
|
|
$this->security->method('getUser')->willReturn(null);
|
|
|
|
$this->assertSame(StatisticsActorRole::SYSTEM, $this->recorder()->resolveActorRole());
|
|
}
|
|
|
|
/**
|
|
* Statistics are a side effect. Losing one must never cost the teamer their
|
|
* application or the admin their edit.
|
|
*/
|
|
public function testSwallowsAWriteFailureInsteadOfBreakingTheAction(): void
|
|
{
|
|
$this->connection
|
|
->method('insert')
|
|
->willThrowException(new DbalException('no such table'))
|
|
;
|
|
|
|
$this->logger->expects($this->once())->method('error');
|
|
|
|
$this->recorder()->record(StatisticsEventName::APPLICATION_CREATED);
|
|
}
|
|
|
|
/**
|
|
* A payload that will not encode is a lost event either way. What must not happen is
|
|
* losing it to a DBAL error about the column, with nothing in the log pointing at the
|
|
* payload that caused it.
|
|
*/
|
|
public function testReportsAPayloadThatCannotBeEncodedInsteadOfWritingGarbage(): void
|
|
{
|
|
$this->connection->expects($this->never())->method('insert');
|
|
$this->logger->expects($this->once())->method('error');
|
|
|
|
$this->recorder()->record(
|
|
StatisticsEventName::DISPOSITION_CALLED_OFF,
|
|
[],
|
|
['reason' => "\xB1\x31"],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hands back the row the recorder writes. An ArrayObject rather than an array, so the
|
|
* callback can fill it in after this method has already returned.
|
|
*
|
|
* @return \ArrayObject<string, mixed>
|
|
*/
|
|
private function capture(): \ArrayObject
|
|
{
|
|
$written = new \ArrayObject(['table' => '', 'data' => []]);
|
|
|
|
$this->connection
|
|
->method('insert')
|
|
->willReturnCallback(static function (string $table, array $data) use ($written): int {
|
|
$written['table'] = $table;
|
|
$written['data'] = $data;
|
|
|
|
return 1;
|
|
})
|
|
;
|
|
|
|
return $written;
|
|
}
|
|
|
|
private function recorder(): StatisticsRecorder
|
|
{
|
|
return new StatisticsRecorder($this->connection, $this->security, $this->logger);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $grantedRoles
|
|
*/
|
|
private function givenUser(array $grantedRoles): void
|
|
{
|
|
$this->security->method('getUser')->willReturn(new User());
|
|
$this->security
|
|
->method('isGranted')
|
|
->willReturnCallback(static fn (mixed $role): bool => in_array($role, $grantedRoles, true))
|
|
;
|
|
}
|
|
|
|
private function assignmentInHotel(string $hotelCode): Assignment
|
|
{
|
|
$destination = (new Destination())->setHotelCode($hotelCode);
|
|
|
|
return (new Assignment())->setDestination($destination);
|
|
}
|
|
}
|