diff --git a/composer.json b/composer.json index 200ae39..882dff4 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,15 @@ ], "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "laminas/laminas-filter": "^2.37", "laminas/laminas-servicemanager": "^3.22", "laminas/laminas-validator": "^2.64" }, "require-dev": { "phpunit/phpunit": "^10.2", "laminas/laminas-coding-standard": "^2.5", - "vimeo/psalm": "^5.13" + "vimeo/psalm": "^5.13", + "ext-curl": "*" }, "autoload": { "psr-4": { diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/log/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/src/Filter/Priority.php b/src/Filter/Priority.php index 26550ab..b78c214 100644 --- a/src/Filter/Priority.php +++ b/src/Filter/Priority.php @@ -7,7 +7,6 @@ use Dot\Log\Exception\InvalidArgumentException; use Traversable; -use function ctype_digit; use function gettype; use function is_array; use function is_int; @@ -34,14 +33,14 @@ public function __construct(iterable|int $priority, ?string $operator = null) $operator = $priority['operator'] ?? null; $priority = $priority['priority'] ?? null; } - if (! is_int($priority) && ! ctype_digit($priority)) { + if (! is_int($priority)) { throw new InvalidArgumentException(sprintf( 'Priority must be a number, received "%s"', gettype($priority) )); } - $this->priority = (int) $priority; + $this->priority = $priority; $this->operator = $operator ?? '<='; } diff --git a/src/Manager/FilterPluginManager.php b/src/Manager/FilterPluginManager.php index 27c90fd..ef3dae8 100644 --- a/src/Manager/FilterPluginManager.php +++ b/src/Manager/FilterPluginManager.php @@ -12,6 +12,7 @@ use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\Factory\InvokableFactory; +use Psr\Container\ContainerInterface; use function gettype; use function is_object; @@ -50,6 +51,11 @@ class FilterPluginManager extends AbstractPluginManager */ protected $sharedByDefault = false; + public function __construct(ContainerInterface $container, array $config = []) + { + parent::__construct($container, $config); + } + /** * Validate the plugin is of the expected type. * diff --git a/src/Manager/FormatterPluginManager.php b/src/Manager/FormatterPluginManager.php index f94a795..1dc8871 100644 --- a/src/Manager/FormatterPluginManager.php +++ b/src/Manager/FormatterPluginManager.php @@ -9,6 +9,7 @@ use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\Factory\InvokableFactory; +use Psr\Container\ContainerInterface; use function gettype; use function is_object; @@ -40,6 +41,11 @@ class FormatterPluginManager extends AbstractPluginManager */ protected $sharedByDefault = false; + public function __construct(ContainerInterface $container, array $config = []) + { + parent::__construct($container, $config); + } + /** * Validate the plugin is of the expected type. * diff --git a/src/Manager/ProcessorPluginManager.php b/src/Manager/ProcessorPluginManager.php index 0eab386..e0e9d6b 100644 --- a/src/Manager/ProcessorPluginManager.php +++ b/src/Manager/ProcessorPluginManager.php @@ -12,6 +12,7 @@ use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\Factory\InvokableFactory; +use Psr\Container\ContainerInterface; use function gettype; use function is_object; @@ -49,6 +50,11 @@ class ProcessorPluginManager extends AbstractPluginManager */ protected $sharedByDefault = false; + public function __construct(ContainerInterface $container, array $config = []) + { + parent::__construct($container, $config); + } + /** * Validate the plugin is of the expected type. * diff --git a/src/Manager/WriterPluginManager.php b/src/Manager/WriterPluginManager.php index adeb236..c699820 100644 --- a/src/Manager/WriterPluginManager.php +++ b/src/Manager/WriterPluginManager.php @@ -10,6 +10,7 @@ use Dot\Log\Writer\WriterInterface; use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; +use Psr\Container\ContainerInterface; use function gettype; use function is_object; @@ -48,6 +49,11 @@ class WriterPluginManager extends AbstractPluginManager */ protected $sharedByDefault = false; + public function __construct(ContainerInterface $container, array $config = []) + { + parent::__construct($container, $config); + } + /** * Validate the plugin is of the expected type. * diff --git a/src/Processor/RequestId.php b/src/Processor/RequestId.php index d5a0701..e603397 100644 --- a/src/Processor/RequestId.php +++ b/src/Processor/RequestId.php @@ -8,7 +8,7 @@ class RequestId implements ProcessorInterface { - protected string $identifier; + protected ?string $identifier = null; /** * Adds an identifier for the request to the log, unless one has already been set. diff --git a/src/Writer/Stream.php b/src/Writer/Stream.php index f5c5cb4..2b29fa2 100644 --- a/src/Writer/Stream.php +++ b/src/Writer/Stream.php @@ -68,7 +68,7 @@ public function __construct( if (! is_string($streamOrUrl) && ! is_resource($streamOrUrl)) { throw new InvalidArgumentException(sprintf( - 'Resource is not a stream nor a string; received "%s', + 'Resource is not a stream nor a string; received "%s"', gettype($streamOrUrl) )); } diff --git a/test/Factory/WriterFactoryTest.php b/test/Factory/WriterFactoryTest.php new file mode 100644 index 0000000..d769f0f --- /dev/null +++ b/test/Factory/WriterFactoryTest.php @@ -0,0 +1,47 @@ +container = $this->createMock(ContainerInterface::class); + $this->subject = new WriterFactory(); + } + + public function testWillInstantiate(): void + { + $factory = (new WriterFactory())( + $this->container, + Stream::class, + ['stream' => __DIR__ . '/../../log/dk.log'] + ); + + $this->assertInstanceOf(AbstractWriter::class, $factory); + } + + public function testSetCreationOptions(): void + { + $input = []; + + $this->assertNull($this->subject->setCreationOptions($input)); + } +} diff --git a/test/Filter/PriorityTest.php b/test/Filter/PriorityTest.php new file mode 100644 index 0000000..c80417d --- /dev/null +++ b/test/Filter/PriorityTest.php @@ -0,0 +1,60 @@ +subject = new Priority(47); + } + + public function testWillInstantiateWithInt(): void + { + $this->assertInstanceOf(Priority::class, $this->subject); + } + + public function testWillInstantiateWithArray(): void + { + $input = ['priority' => 47]; + + $result = new Priority($input); + + $this->assertInstanceOf(Priority::class, $result); + } + + public function testWillNotInstantiateWithEmptyArray(): void + { + $input = []; + + $this->expectExceptionMessage('Priority must be a number, received "NULL"'); + $this->expectException(InvalidArgumentException::class); + new Priority($input); + } + + public function testFilterWillAcceptMessage(): void + { + $input = ['priority' => 47]; + + $result = $this->subject->filter($input); + + $this->assertTrue($result); + } + + public function testFilterWillNotAcceptMessage(): void + { + $input = ['priority' => 244]; + + $result = $this->subject->filter($input); + + $this->assertFalse($result); + } +} diff --git a/test/Filter/RegexTest.php b/test/Filter/RegexTest.php new file mode 100644 index 0000000..83a76d0 --- /dev/null +++ b/test/Filter/RegexTest.php @@ -0,0 +1,58 @@ +subject = new Regex(['regex' => '/(a)(b)*(c)/']); + } + + public function testWillInstantiate(): void + { + $this->assertInstanceOf(Regex::class, $this->subject); + } + + /** + * @throws ErrorException + */ + public function testWillNotInstantiateWithEmptyArray(): void + { + $this->expectExceptionMessage( + 'preg_match(): Argument #1 ($pattern) must be of type string, null given' + ); + $this->expectException(TypeError::class); + new Regex([]); + } + + public function testFilterWillAcceptMessage(): void + { + $input = ['message' => 'ac']; + + $result = $this->subject->filter($input); + + $this->assertTrue($result); + } + + public function testFilterWillNotAcceptMessage(): void + { + $input = ['message' => 'What a wonderful day to write tests']; + + $result = $this->subject->filter($input); + + $this->assertFalse($result); + } +} diff --git a/test/Filter/SuppressFilterTest.php b/test/Filter/SuppressFilterTest.php new file mode 100644 index 0000000..b056262 --- /dev/null +++ b/test/Filter/SuppressFilterTest.php @@ -0,0 +1,45 @@ +subject = new SuppressFilter(); + } + + public function testWillInstantiate(): void + { + $this->assertInstanceOf(SuppressFilter::class, $this->subject); + } + + public function testWillNotInstantiate(): void + { + $this->expectExceptionMessage( + 'Suppress must be a boolean; received "string"' + ); + $this->expectException(InvalidArgumentException::class); + new SuppressFilter(['suppress' => 'oups']); + } + + public function testFilter(): void + { + $result = $this->subject->filter([]); + + $this->assertIsBool($result); + } + + public function testSuppress(): void + { + $this->assertNull($this->subject->suppress(true)); + } +} diff --git a/test/Filter/ValidatorTest.php b/test/Filter/ValidatorTest.php new file mode 100644 index 0000000..8a29a9a --- /dev/null +++ b/test/Filter/ValidatorTest.php @@ -0,0 +1,52 @@ +subject = new Validator(['validator' => new NotEmpty()]); + } + + public function testWillInstantiate(): void + { + $this->assertInstanceOf(Validator::class, $this->subject); + } + + public function testWillNotInstantiateWithEmptyArray(): void + { + $this->expectExceptionMessage( + 'Parameter of type NULL is invalid; must implement Laminas\Validator\ValidatorInterface' + ); + $this->expectException(InvalidArgumentException::class); + new Validator([]); + } + + public function testFilterWillAcceptMessage(): void + { + $input = ['message' => 'What a wonderful day to write tests']; + + $result = $this->subject->filter($input); + + $this->assertTrue($result); + } + + public function testFilterWillNotAcceptMessage(): void + { + $input = ['message' => '']; + + $result = $this->subject->filter($input); + + $this->assertFalse($result); + } +} diff --git a/test/Formatter/BaseTest.php b/test/Formatter/BaseTest.php new file mode 100644 index 0000000..216c7c6 --- /dev/null +++ b/test/Formatter/BaseTest.php @@ -0,0 +1,43 @@ +subject = new Base(); + } + + public function testWillInstantiate(): void + { + $this->assertInstanceOf(Base::class, $this->subject); + } + + public function testFormat(): void + { + $input = ['value']; + $result = $this->subject->format($input); + $this->assertSame($input, $result); + } + + public function testGetDatetimeFormat(): void + { + $result = $this->subject->getDateTimeFormat(); + $this->assertSame(FormatterInterface::DEFAULT_DATETIME_FORMAT, $result); + } + + public function testSetDatetimeFormat(): void + { + $result = $this->subject->setDateTimeFormat('Y-m-d H:i:s'); + $this->assertInstanceOf(Base::class, $result); + } +} diff --git a/test/Formatter/JsonTest.php b/test/Formatter/JsonTest.php new file mode 100644 index 0000000..7926c5e --- /dev/null +++ b/test/Formatter/JsonTest.php @@ -0,0 +1,39 @@ +subject = new Json(); + } + + public function testFormat(): void + { + $input = ['awesome array', 'timestamp' => new DateTime()]; + $result = $this->subject->format($input); + $this->assertIsString($result); + } + + public function testGetDatetimeFormat(): void + { + $result = $this->subject->getDateTimeFormat(); + $this->assertSame(FormatterInterface::DEFAULT_DATETIME_FORMAT, $result); + } + + public function testSetDatetimeFormat(): void + { + $result = $this->subject->setDateTimeFormat('Y-m-d H:i:s'); + $this->assertInstanceOf(Json::class, $result); + } +} diff --git a/test/Formatter/SimpleTest.php b/test/Formatter/SimpleTest.php new file mode 100644 index 0000000..7cee499 --- /dev/null +++ b/test/Formatter/SimpleTest.php @@ -0,0 +1,39 @@ +subject = new Simple(Simple::DEFAULT_FORMAT); + } + + public function testWillInstantiate(): void + { + $this->assertInstanceOf(Simple::class, $this->subject); + } + + public function testWillNotInstantiate(): void + { + $this->expectExceptionMessage('Format must be a string'); + $this->expectException(InvalidArgumentException::class); + new Simple(['format' => new DateTime()]); + } + + public function testFormat(): void + { + $input = ['message' => 'Test Message', 'priorityName' => 'Critical']; + $result = $this->subject->format($input); + $this->assertIsString($result); + } +} diff --git a/test/LoggerServiceFactoryTest.php b/test/LoggerServiceFactoryTest.php new file mode 100644 index 0000000..294417f --- /dev/null +++ b/test/LoggerServiceFactoryTest.php @@ -0,0 +1,240 @@ +serviceManager = new ServiceManager(); + $config = new Config([ + 'aliases' => [ + 'Dot\Log' => Logger::class, + ], + 'factories' => [ + Logger::class => LoggerServiceFactory::class, + ], + 'services' => [ + 'config' => [ + 'log' => [], + ], + ], + ]); + $config->configureServiceManager($this->serviceManager); + } + + public static function providerValidLoggerService(): array + { + return [ + [Logger::class], + ['Dot\Log'], + ]; + } + + public static function providerInvalidLoggerService(): array + { + return [ + ['log'], + ['Logger\Application\Frontend'], + ['writers'], + ]; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @dataProvider providerValidLoggerService + */ + public function testValidLoggerService(string $service): void + { + $actual = $this->serviceManager->get($service); + self::assertInstanceOf(Logger::class, $actual); + } + + /** + * @dataProvider providerInvalidLoggerService + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testInvalidLoggerService(string $service): void + { + $this->expectException(ServiceNotFoundException::class); + $this->serviceManager->get($service); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testWillInjectWriterPluginManagerIfAvailable(): void + { + $writers = new WriterPluginManager(new ServiceManager()); + $mockWriter = $this->createMock(WriterInterface::class); + $writers->setService('CustomWriter', $mockWriter); + + $config = new Config([ + 'factories' => [ + Logger::class => LoggerServiceFactory::class, + ], + 'services' => [ + 'LogWriterManager' => $writers, + 'config' => [ + 'log' => [ + 'writers' => [['name' => 'CustomWriter', 'priority' => 1]], + ], + ], + ], + ]); + $services = new ServiceManager(); + $config->configureServiceManager($services); + + $log = $services->get(Logger::class); + $logWriters = $log->getWriters(); + self::assertEquals(1, count($logWriters)); + $writer = $logWriters->current(); + self::assertSame($mockWriter, $writer); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws Exception + */ + public function testWillInjectProcessorPluginManagerIfAvailable(): void + { + $processors = new ProcessorPluginManager(new ServiceManager()); + $mockProcessor = $this->createMock(ProcessorInterface::class); + $processors->setService('CustomProcessor', $mockProcessor); + + $config = new Config([ + 'factories' => [ + Logger::class => LoggerServiceFactory::class, + ], + 'services' => [ + 'LogProcessorManager' => $processors, + 'config' => [ + 'log' => [ + 'writers' => [['name' => Noop::class, 'priority' => 1]], + 'processors' => [['name' => 'CustomProcessor', 'priority' => 1]], + ], + ], + ], + ]); + $services = new ServiceManager(); + $config->configureServiceManager($services); + + $log = $services->get(Logger::class); + $logProcessors = $log->getProcessors(); + self::assertEquals(1, count($logProcessors)); + $processor = $logProcessors->current(); + self::assertSame($mockProcessor, $processor); + } + + /** + * @dataProvider dataWritersValues() + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWritersValue(mixed $writers, int $count): void + { + $config = new Config([ + 'factories' => [ + Logger::class => LoggerServiceFactory::class, + ], + 'services' => [ + 'config' => [ + 'log' => [ + 'writers' => $writers, + ], + ], + ], + ]); + $services = new ServiceManager(); + $config->configureServiceManager($services); + + /** @var Logger $log */ + $log = $services->get(Logger::class); + self::assertCount($count, $log->getWriters()); + } + + public static function dataWritersValues(): array + { + return [ + 'null' => [null, 0], + 'string' => ['writers config', 0], + 'number' => [1e3, 0], + 'object' => [new stdClass(), 0], + 'empty iterable' => [new ArrayObject(), 0], + 'iterable' => [new ArrayObject([['name' => Noop::class, 'priority' => 1]]), 1], + ]; + } + + /** + * @dataProvider dataInvalidWriterConfig() + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testInvalidWriterConfig(mixed $value, string $type): void + { + $config = new Config([ + 'factories' => [ + Logger::class => LoggerServiceFactory::class, + ], + 'services' => [ + 'config' => [ + 'log' => [ + 'writers' => [ + $value, + ], + ], + ], + ], + ]); + $services = new ServiceManager(); + $config->configureServiceManager($services); + + self::expectException(ServiceNotCreatedException::class); + self::expectExceptionMessage( + 'config log.writers[] must contain array or ArrayAccess, ' . $type . ' provided' + ); + + $services->get(Logger::class); + } + + public static function dataInvalidWriterConfig(): array + { + return [ + 'string' => ['invalid config', 'string'], + 'object' => [new stdClass(), 'stdClass'], + ]; + } +} diff --git a/test/LoggerTest.php b/test/LoggerTest.php new file mode 100644 index 0000000..f81fa43 --- /dev/null +++ b/test/LoggerTest.php @@ -0,0 +1,339 @@ +subject = new Logger(); + } + + public function testUsesWriterPluginManagerByDefault(): void + { + $this->assertInstanceOf(WriterPluginManager::class, $this->subject->getWriterPluginManager()); + } + + public function testPassingShortNameToPluginReturnsWriterByThatName(): void + { + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage( + 'A plugin by the name "mock" was not found in the plugin manager Dot\Log\Manager\WriterPluginManager' + ); + $this->subject->writerPlugin('mock'); + } + + public function testEmptyWriter(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No log writer specified'); + $this->subject->log(Logger::INFO, 'test'); + } + + public function testSetWriters(): void + { + $writer = $this->subject->writerPlugin('null'); + $writers = new SplPriorityQueue(); + $writers->insert($writer, 1); + $this->subject->setWriters($writers); + + $writers = $this->subject->getWriters(); + $this->assertInstanceOf(SplPriorityQueue::class, $writers); + $writer = $writers->extract(); + $this->assertInstanceOf(Noop::class, $writer); + } + + public function testAddWriterWithPriority(): void + { + $writer = $this->subject->writerPlugin('null'); + $this->subject->addWriter($writer, 3); + $writers = $this->subject->getWriters(); + + $this->assertInstanceOf(SplPriorityQueue::class, $writers); + $writer = $writers->extract(); + $this->assertInstanceOf(Noop::class, $writer); + } + + public function testAddWithSamePriority(): void + { + $writer1 = $this->subject->writerPlugin('null'); + $this->subject->addWriter($writer1, 1); + $writer2 = $this->subject->writerPlugin('null'); + $this->subject->addWriter($writer2, 1); + $writers = $this->subject->getWriters(); + + $this->assertInstanceOf(SplPriorityQueue::class, $writers); + $writer = $writers->extract(); + $this->assertInstanceOf(Noop::class, $writer); + $writer = $writers->extract(); + $this->assertInstanceOf(Noop::class, $writer); + } + + public function testLogging(): void + { + $writer = new Mock(); + $this->subject->addWriter($writer); + $this->subject->log(Logger::INFO, 'tottakai'); + + $this->assertEquals(count($writer->events), 1); + $this->assertStringContainsString('tottakai', $writer->events[0]['message']); + } + + public function testLoggingArray(): void + { + $writer = new Mock(); + $this->subject->addWriter($writer); + $this->subject->log(Logger::INFO, ['test']); + + $this->assertEquals(count($writer->events), 1); + $this->assertStringContainsString('test', $writer->events[0]['message']); + } + + public function testAddFilter(): void + { + $writer = new Mock(); + $filter = new MockFilter(); + $writer->addFilter($filter); + $this->subject->addWriter($writer); + $this->subject->log(Logger::INFO, ['test']); + + $this->assertEquals(count($filter->events), 1); + $this->assertStringContainsString('test', $filter->events[0]['message']); + } + + public static function provideTestFilters(): array + { + $data = [ + ['priority', ['priority' => Logger::INFO]], + ['regex', ['regex' => '/[0-9]+/']], + ]; + + // Conditionally enabled until laminas-validator is forwards-compatible + // with laminas-servicemanager v3. + if (class_exists(Digits::class)) { + $data[] = ['validator', ['validator' => new Digits()]]; + } + + return $data; + } + + /** + * @dataProvider provideTestFilters + */ + public function testAddFilterByNameWithParams(string $filter, array $options): void + { + $writer = new Mock(); + $writer->addFilter($filter, $options); + $this->subject->addWriter($writer); + + $this->subject->log(Logger::INFO, '123'); + $this->assertEquals(count($writer->events), 1); + $this->assertStringContainsString('123', $writer->events[0]['message']); + } + + public static function provideAttributes(): array + { + return [ + [[]], + [['user' => 'foo', 'ip' => '127.0.0.1']], + [new ArrayObject(['id' => 42])], + ]; + } + + /** + * @dataProvider provideAttributes + */ + public function testLoggingCustomAttributesForUserContext(array|ArrayObject $extra): void + { + $writer = new Mock(); + $this->subject->addWriter($writer); + $this->subject->log(Logger::ERR, 'tottakai', $extra); + + $this->assertEquals(count($writer->events), 1); + $this->assertIsArray($writer->events[0]['extra']); + $this->assertEquals(count($writer->events[0]['extra']), count($extra)); + } + + public function testRegisterErrorHandler(): void + { + $writer = new Mock(); + $this->subject->addWriter($writer); + + $previous = Logger::registerErrorHandler($this->subject); + $this->assertNotNull($previous); + $this->assertNotFalse($previous); + + // check for single error handler instance + $this->assertFalse(Logger::registerErrorHandler($this->subject)); + + // generate a warning + echo $test; // $test is not defined + + Logger::unregisterErrorHandler(); + + if (PHP_VERSION_ID < 80000) { + $this->assertEquals('Undefined variable: test', $writer->events[0]['message']); + } else { + $this->assertEquals('Undefined variable $test', $writer->events[0]['message']); + } + } + + public function testOptionsWithMock(): void + { + $options = [ + 'writers' => [ + 'first_writer' => [ + 'name' => 'null', + 'priority' => 1, + ], + ], + ]; + $logger = new Logger($options); + + $writers = $logger->getWriters()->toArray(); + $this->assertCount(1, $writers); + $this->assertInstanceOf(Noop::class, $writers[0]); + } + + public function testOptionsWithWriterOptions(): void + { + $options = [ + 'writers' => [ + [ + 'name' => 'stream', + 'options' => [ + 'stream' => 'php://output', + 'log_separator' => 'foo', + ], + 'priority' => 1, + ], + ], + ]; + $logger = new Logger($options); + + $writers = $logger->getWriters()->toArray(); + $this->assertCount(1, $writers); + $this->assertInstanceOf(Stream::class, $writers[0]); + $this->assertEquals('foo', $writers[0]->getLogSeparator()); + } + + public function testOptionsWithMockAndProcessor(): void + { + $options = [ + 'writers' => [ + 'first_writer' => [ + 'name' => 'null', + 'priority' => 1, + ], + ], + 'processors' => [ + 'first_processor' => [ + 'name' => 'requestid', + 'priority' => 1, + ], + ], + ]; + $logger = new Logger($options); + $processors = $logger->getProcessors()->toArray(); + $this->assertCount(1, $processors); + $this->assertInstanceOf(RequestId::class, $processors[0]); + } + + public function testAddProcessor(): void + { + $processor = new Backtrace(); + $this->subject->addProcessor($processor); + + $processors = $this->subject->getProcessors()->toArray(); + $this->assertEquals($processor, $processors[0]); + } + + public function testAddProcessorByName(): void + { + $this->subject->addProcessor('backtrace'); + + $processors = $this->subject->getProcessors()->toArray(); + $this->assertInstanceOf(Backtrace::class, $processors[0]); + + $writer = new Mock(); + $this->subject->addWriter($writer); + $this->subject->log(Logger::ERR, 'foo'); + } + + public function testExceptionHandler(): void + { + $writer = new Mock(); + $this->subject->addWriter($writer); + + $this->assertTrue(Logger::registerExceptionHandler($this->subject)); + + // check for single error handler instance + $this->assertFalse(Logger::registerExceptionHandler($this->subject)); + + // get the internal exception handler + $exceptionHandler = set_exception_handler(function ($e) { + }); + set_exception_handler($exceptionHandler); + + // reset the exception handler + Logger::unregisterExceptionHandler(); + + // call the exception handler + $exceptionHandler(new Exception('error', 200, new Exception('previos', 100))); + $exceptionHandler(new ErrorException('user notice', 1000, E_USER_NOTICE, __FILE__, __LINE__)); + + // check logged messages + $expectedEvents = [ + ['priority' => Logger::ERR, 'message' => 'previos', 'file' => __FILE__], + ['priority' => Logger::ERR, 'message' => 'error', 'file' => __FILE__], + ['priority' => Logger::NOTICE, 'message' => 'user notice', 'file' => __FILE__], + ]; + for ($i = 0; $i < count($expectedEvents); $i++) { + $expectedEvent = $expectedEvents[$i]; + $event = $writer->events[$i]; + + $this->assertEquals($expectedEvent['priority'], $event['priority'], 'Unexpected priority'); + $this->assertEquals($expectedEvent['message'], $event['message'], 'Unexpected message'); + $this->assertEquals($expectedEvent['file'], $event['extra']['file'], 'Unexpected file'); + } + } + + /** + * @group Laminas-7238 + */ + public function testCatchExceptionNotValidPriority(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$priority must be an integer >= 0 and < 8; received -1'); + $writer = new Mock(); + $this->subject->addWriter($writer); + $this->subject->log(-1, 'Foo'); + } +} diff --git a/test/Manager/FilterPluginManagerTest.php b/test/Manager/FilterPluginManagerTest.php new file mode 100644 index 0000000..ffd09f5 --- /dev/null +++ b/test/Manager/FilterPluginManagerTest.php @@ -0,0 +1,42 @@ +createMock(ContainerInterface::class); + $this->subject = new FilterPluginManager($container); + } + + public function testValidate(): void + { + $this->assertNull($this->subject->validate(new Priority(47))); + } + + public function testWillNotValidate(): void + { + $this->expectExceptionMessage( + 'Dot\Log\Manager\FilterPluginManager can only create instances of' + . ' Dot\Log\Filter\FilterInterface; Dot\Log\Formatter\Json is invalid' + ); + $this->expectException(InvalidServiceException::class); + $this->subject->validate(new Json()); + } +} diff --git a/test/Manager/FormatterPluginManagerTest.php b/test/Manager/FormatterPluginManagerTest.php new file mode 100644 index 0000000..376219e --- /dev/null +++ b/test/Manager/FormatterPluginManagerTest.php @@ -0,0 +1,42 @@ +createMock(ContainerInterface::class); + $this->subject = new FormatterPluginManager($container); + } + + public function testValidate(): void + { + $this->assertNull($this->subject->validate(new Json())); + } + + public function testWillNotValidate(): void + { + $this->expectExceptionMessage( + 'Dot\Log\Manager\FormatterPluginManager can only create instances of' + . ' Dot\Log\Formatter\FormatterInterface; Dot\Log\Filter\Priority is invalid' + ); + $this->expectException(InvalidServiceException::class); + $this->subject->validate(new Priority(47)); + } +} diff --git a/test/Manager/ProcessorPluginManagerTest.php b/test/Manager/ProcessorPluginManagerTest.php new file mode 100644 index 0000000..bf9b6c8 --- /dev/null +++ b/test/Manager/ProcessorPluginManagerTest.php @@ -0,0 +1,42 @@ +createMock(ContainerInterface::class); + $this->subject = new ProcessorPluginManager($container); + } + + public function testValidate(): void + { + $this->assertNull($this->subject->validate(new Backtrace())); + } + + public function testWillNotValidate(): void + { + $this->expectExceptionMessage( + 'Dot\Log\Manager\ProcessorPluginManager can only create instances of ' + . 'Dot\Log\Processor\ProcessorInterface; Dot\Log\Formatter\Json is invalid' + ); + $this->expectException(InvalidServiceException::class); + $this->subject->validate(new Json()); + } +} diff --git a/test/Manager/WriterPluginManagerTest.php b/test/Manager/WriterPluginManagerTest.php new file mode 100644 index 0000000..7645078 --- /dev/null +++ b/test/Manager/WriterPluginManagerTest.php @@ -0,0 +1,42 @@ +createMock(ContainerInterface::class); + $this->subject = new WriterPluginManager($container); + } + + public function testValidate(): void + { + $this->assertNull($this->subject->validate(new Noop())); + } + + public function testWillNotValidate(): void + { + $this->expectExceptionMessage( + 'Dot\Log\Manager\WriterPluginManager can only create instances of' + . ' Dot\Log\Writer\WriterInterface; Dot\Log\Formatter\Json is invalid' + ); + $this->expectException(InvalidServiceException::class); + $this->subject->validate(new Json()); + } +} diff --git a/test/Mock.php b/test/Mock.php new file mode 100644 index 0000000..c603dd6 --- /dev/null +++ b/test/Mock.php @@ -0,0 +1,42 @@ +events[] = $event; + } + + /** + * Record shutdown + * + * @return void + */ + public function shutdown() + { + $this->shutdown = true; + } +} diff --git a/test/MockFilter.php b/test/MockFilter.php new file mode 100644 index 0000000..a109635 --- /dev/null +++ b/test/MockFilter.php @@ -0,0 +1,26 @@ +events[] = $event; + return true; + } +} diff --git a/test/Processor/BackTraceTest.php b/test/Processor/BackTraceTest.php new file mode 100644 index 0000000..2970229 --- /dev/null +++ b/test/Processor/BackTraceTest.php @@ -0,0 +1,35 @@ +subject = new Backtrace(); + } + + public function testWillInstantiate() + { + $this->assertInstanceOf(Backtrace::class, $this->subject); + } + + public function testProcess() + { + $result = $this->subject->process([]); + $this->assertArrayHasKey('extra', $result); + } + + public function testGetIgnoredNamespaces() + { + $result = $this->subject->getIgnoredNamespaces(); + $this->assertIsArray($result); + } +} diff --git a/test/Processor/PsrPlaceholderTest.php b/test/Processor/PsrPlaceholderTest.php new file mode 100644 index 0000000..e0b569e --- /dev/null +++ b/test/Processor/PsrPlaceholderTest.php @@ -0,0 +1,41 @@ +subject = new PsrPlaceholder(); + } + + public function testProcessMessageWithNoCurlyBrackets(): void + { + $input = ["message" => "this is a message"]; + + $result = $this->subject->process($input); + $this->assertSame($input, $result); + } + + public function testProcess(): void + { + $input = [ + "message" => '{nullvalue} of {objectvalue} is in fact a {stringvalue}', + "extra" => [ + "nullvalue" => null, + "objectvalue" => $this->subject, + "stringvalue" => "this is a message", + ], + ]; + + $result = $this->subject->process($input); + $this->assertNotSame($input['message'], $result['message']); + } +} diff --git a/test/Processor/ReferenceIdTest.php b/test/Processor/ReferenceIdTest.php new file mode 100644 index 0000000..ee93150 --- /dev/null +++ b/test/Processor/ReferenceIdTest.php @@ -0,0 +1,51 @@ +subject = new ReferenceId(); + } + + public function testProcessWithReferenceId(): void + { + $input = [ + "extra" => [ + "referenceId" => "something", + ], + ]; + + $result = $this->subject->process($input); + $this->assertSame($input, $result); + } + + public function testProcess(): void + { + $input = []; + + $result = $this->subject->process($input); + $this->assertArrayHasKey("extra", $result); + } + + public function testGetReferenceId(): void + { + $result = $this->subject->getReferenceId(); + $this->assertIsString($result); + } + + public function testSetReferenceId(): void + { + $input = "something"; + $result = $this->subject->setReferenceId($input); + $this->assertSame($input, $result->getReferenceId()); + } +} diff --git a/test/Processor/RequestIdTest.php b/test/Processor/RequestIdTest.php new file mode 100644 index 0000000..e0b911a --- /dev/null +++ b/test/Processor/RequestIdTest.php @@ -0,0 +1,38 @@ +subject = new RequestId(); + } + + public function testProcessWithRequestId(): void + { + $input = [ + "extra" => [ + "requestId" => "something", + ], + ]; + + $result = $this->subject->process($input); + $this->assertSame($input, $result); + } + + public function testProcess(): void + { + $input = []; + + $result = $this->subject->process($input); + $this->assertArrayHasKey("extra", $result); + } +} diff --git a/test/Writer/StreamTest.php b/test/Writer/StreamTest.php new file mode 100644 index 0000000..8aa8e90 --- /dev/null +++ b/test/Writer/StreamTest.php @@ -0,0 +1,97 @@ + __DIR__ . '/../../log/error-log-{Y}-{m}-{d}.log', + 'filters' => [ + 'allMessages' => [ + 'name' => 'priority', + 'options' => [ + 'operator' => '>=', + 'priority' => Logger::EMERG, + ], + ], + ], + 'formatter' => [ + 'name' => Json::class, + ], + ]; + + /** + * @throws ErrorException + */ + protected function setUp(): void + { + $this->subject = new Stream($this->options); + } + + /** + * @throws ErrorException + */ + public function testWillNotInstantiateWithInt(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Resource is not a stream nor a string; received "integer"'); + new Stream(47); + } + + /** + * @throws ErrorException + */ + public function testWillNotInstantiateWithWrongResource(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Resource is not a stream nor a string; received "object"'); + new Stream(['stream' => curl_init('url')]); + } + + /** + * @throws ErrorException + */ + public function testWillNotInstantiateWithWrongMode(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Mode must be "a" on existing streams; received "r"'); + new Stream( + [ + 'stream' => fopen(__DIR__ . '/../../log/error-log-{Y}-{m}-{d}.log', 'r'), + 'mode' => 'r', + ] + ); + } + + public function testSetLogSeparator(): void + { + $input = "separator"; + + $result = $this->subject->setLogSeparator($input); + $this->assertSame($this->subject, $result); + } + + public function testGetLogSeparator(): void + { + $this->assertIsString($this->subject->getLogSeparator()); + } + + public function testShutDown() + { + $this->assertNull($this->subject->shutdown()); + } +}