Files
myep/tests/Logger/DatabaseHandlerTest.php
T

154 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Tests\Logger;
use App\Entity\LogEntry;
use App\Logger\DatabaseHandler;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
use Doctrine\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\Persistence\ObjectManager;
use Monolog\Level;
use Monolog\LogRecord;
use PHPUnit\Framework\TestCase;
/**
* The handler writes through the DBAL connection instead of the EntityManager, so the column names
* are spelled out by hand here and can drift away from the LogEntry mapping. These tests pin them
* to the mapping, and pin the handler to the connection.
*/
class DatabaseHandlerTest extends TestCase
{
public function testTheHandlerStaysOutOfTheUnitOfWork(): void
{
// A log record can be emitted halfway through somebody else's unit of work. Flushing an
// EntityManager here would write out whatever that unit of work has pending, which is how
// a half-built User once reached the database with a NULL password.
$parameters = (new \ReflectionMethod(DatabaseHandler::class, '__construct'))->getParameters();
foreach ($parameters as $parameter) {
$type = $parameter->getType();
self::assertInstanceOf(\ReflectionNamedType::class, $type);
self::assertNotSame(ObjectManager::class, $type->getName());
self::assertFalse(
is_a($type->getName(), ObjectManager::class, true),
'the handler must not be able to flush anybody elses entities',
);
}
}
public function testTheWrittenColumnsMatchTheLogEntryMapping(): void
{
$handler = new DatabaseHandler($this->connection($table, $data, $types));
$handler->handle($this->record('auth', 'Login', ['email' => '[email protected]']));
$metadata = $this->logEntryMetadata();
self::assertSame($metadata->getTableName(), $table);
self::assertSame([], array_diff(array_keys($data), $metadata->getColumnNames()));
// Everything the table requires has to be supplied: no default fills these in, and the id
// is the only column the database generates itself.
$required = array_diff($metadata->getColumnNames(), ['id']);
foreach ($required as $column) {
self::assertArrayHasKey($column, $data);
}
self::assertSame([], array_diff(array_keys($types), array_keys($data)));
}
public function testTheRecordIsWrittenAsIs(): void
{
$handler = new DatabaseHandler($this->connection($table, $data, $types));
$handler->handle($this->record('bpn', 'Request failed', ['request_id' => 'r-1'], ['error_code' => '853']));
self::assertSame('bpn', $data['channel']);
self::assertSame('Request failed', $data['message']);
self::assertSame(['request_id' => 'r-1'], $data['context']);
self::assertSame(['error_code' => '853'], $data['extra']);
self::assertSame('853', $data['error_code']);
self::assertInstanceOf(\DateTimeImmutable::class, $data['created_at']);
}
public function testPlaceholdersAreReplacedFromTheContext(): void
{
$handler = new DatabaseHandler($this->connection($table, $data, $types));
$handler->handle($this->record('core', 'Booking {id} confirmed', ['id' => '4711']));
self::assertSame('Booking 4711 confirmed', $data['message']);
}
public function testAnErrorCodeIsOptional(): void
{
$handler = new DatabaseHandler($this->connection($table, $data, $types));
$handler->handle($this->record('core', 'Login'));
self::assertNull($data['error_code']);
}
public function testDebugRecordsAreNotWritten(): void
{
$connection = $this->createMock(Connection::class);
$connection->expects(self::never())->method('insert');
$handler = new DatabaseHandler($connection);
$handler->handle($this->record('core', 'Noise', level: Level::Debug));
}
/**
* @param array<string, mixed>|null $data
* @param array<string, mixed>|null $types
*/
private function connection(?string &$table, ?array &$data, ?array &$types): Connection
{
$connection = $this->createMock(Connection::class);
$connection
->method('insert')
->willReturnCallback(
static function (string $insertTable, array $insertData, array $insertTypes) use (&$table, &$data, &$types): int {
$table = $insertTable;
$data = $insertData;
$types = $insertTypes;
return 1;
}
)
;
return $connection;
}
/**
* @param array<string, mixed> $context
* @param array<string, mixed> $extra
*/
private function record(string $channel, string $message, array $context = [], array $extra = [], Level $level = Level::Info): LogRecord
{
return new LogRecord(new \DateTimeImmutable(), $channel, $level, $message, $context, $extra);
}
/**
* @return ClassMetadata<LogEntry>
*/
private function logEntryMetadata(): ClassMetadata
{
/** @var ClassMetadata<LogEntry> $metadata */
$metadata = new ClassMetadata(LogEntry::class, new UnderscoreNamingStrategy(CASE_LOWER, true));
$metadata->initializeReflection(new RuntimeReflectionService());
(new AttributeDriver([dirname(__DIR__, 2).'/src/Entity']))->loadMetadataForClass(LogEntry::class, $metadata);
return $metadata;
}
}