feat: collect assignment and disposition events for statistics
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user