Files
myep-team/tests/Service/Statistics/StatisticsRecorderTest.php
T

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\DatabaseRequired;
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 DatabaseRequired('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);
}
}