Skip to content

Commit 590d5f6

Browse files
feat: add unit test for formatters
1 parent a58c130 commit 590d5f6

4 files changed

Lines changed: 565 additions & 3 deletions

File tree

app/Audit/AuditLogFormatterFactory.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
<?php namespace App\Audit;
1+
<?php
2+
3+
namespace App\Audit;
4+
25
/**
36
* Copyright 2025 OpenStack Foundation
47
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,15 +23,19 @@
2023
use Doctrine\ORM\PersistentCollection;
2124
use Illuminate\Support\Facades\Log;
2225
use Doctrine\ORM\Mapping\ClassMetadata;
26+
2327
class AuditLogFormatterFactory implements IAuditLogFormatterFactory
2428
{
25-
2629
private array $config;
2730

2831
public function __construct()
2932
{
3033
// cache the config so we don't hit config() repeatedly
31-
$this->config = config('audit_log', []);
34+
try {
35+
$this->config = config('audit_log', []);
36+
} catch (\Exception $e) {
37+
$this->config = [];
38+
}
3239
}
3340

3441
public function make(AuditContext $ctx, $subject, string $event_type): ?IAuditLogFormatter
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
<?php
2+
3+
namespace Tests\OpenTelemetry\Formatters;
4+
5+
use App\Audit\AuditLogFormatterFactory;
6+
use App\Audit\AuditContext;
7+
use App\Audit\Interfaces\IAuditStrategy;
8+
use App\Audit\IAuditLogFormatter;
9+
use Tests\OpenTelemetry\Formatters\Support\FormatterTestHelper;
10+
use Tests\OpenTelemetry\Formatters\Support\AuditContextBuilder;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class AllFormattersIntegrationTest extends TestCase
14+
{
15+
private AuditContext $defaultContext;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
$this->defaultContext = AuditContextBuilder::default()->build();
21+
}
22+
23+
private function discoverMainFormatters(): array
24+
{
25+
return $this->discoverFormatters(__DIR__ . '/../../../app/Audit/ConcreteFormatters');
26+
}
27+
28+
private function discoverFormatters(string $directory): array
29+
{
30+
$formatters = [];
31+
32+
if (!is_dir($directory)) {
33+
return $formatters;
34+
}
35+
36+
$iterator = new \RecursiveIteratorIterator(
37+
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS)
38+
);
39+
40+
foreach ($iterator as $file) {
41+
if ($file->getExtension() !== 'php' || strpos($file->getPathname(), 'ChildEntityFormatters') !== false) {
42+
continue;
43+
}
44+
45+
$className = $this->buildClassName($file->getPathname(), $directory);
46+
47+
if (class_exists($className) && $this->isMainFormatter($className)) {
48+
$formatters[] = $className;
49+
}
50+
}
51+
52+
return array_values($formatters);
53+
}
54+
55+
/**
56+
* Build fully qualified class name from file path
57+
*/
58+
private function buildClassName(string $filePath, string $basePath): string
59+
{
60+
$relativePath = str_replace([$basePath . DIRECTORY_SEPARATOR, '.php'], '', $filePath);
61+
$classPath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
62+
return 'App\\Audit\\ConcreteFormatters\\' . $classPath;
63+
}
64+
65+
/**
66+
* Check if class is a valid main formatter
67+
*/
68+
private function isMainFormatter(string $className): bool
69+
{
70+
try {
71+
$reflection = new \ReflectionClass($className);
72+
73+
if ($reflection->isAbstract() || $reflection->isInterface()) {
74+
return false;
75+
}
76+
77+
$genericFormatters = [
78+
'EntityCreationAuditLogFormatter',
79+
'EntityDeletionAuditLogFormatter',
80+
'EntityUpdateAuditLogFormatter',
81+
'EntityCollectionUpdateAuditLogFormatter',
82+
];
83+
84+
return !in_array($reflection->getShortName(), $genericFormatters) &&
85+
$reflection->isSubclassOf('App\Audit\AbstractAuditLogFormatter');
86+
} catch (\ReflectionException $e) {
87+
return false;
88+
}
89+
}
90+
91+
public function testAllFormattersCanBeInstantiated(): void
92+
{
93+
foreach ($this->discoverMainFormatters() as $formatterClass) {
94+
try {
95+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
96+
$formatterClass,
97+
IAuditStrategy::EVENT_ENTITY_CREATION
98+
);
99+
100+
FormatterTestHelper::assertFormatterHasSetContextMethod($formatter);
101+
$formatter->setContext($this->defaultContext);
102+
$this->assertNotNull($formatter);
103+
} catch (\Exception $e) {
104+
$this->fail("Failed to validate {$formatterClass}: " . $e->getMessage());
105+
}
106+
}
107+
}
108+
109+
public function testAllFormatterConstructorParametersRequired(): void
110+
{
111+
$count = 0;
112+
foreach ($this->discoverMainFormatters() as $formatterClass) {
113+
try {
114+
FormatterTestHelper::assertFormatterHasValidConstructor($formatterClass);
115+
$count++;
116+
} catch (\Exception $e) {
117+
$this->fail("{$formatterClass}: " . $e->getMessage());
118+
}
119+
}
120+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
121+
}
122+
123+
public function testAllFormattersHandleAllEventTypes(): void
124+
{
125+
$eventTypes = [
126+
IAuditStrategy::EVENT_ENTITY_CREATION,
127+
IAuditStrategy::EVENT_ENTITY_UPDATE,
128+
IAuditStrategy::EVENT_ENTITY_DELETION,
129+
IAuditStrategy::EVENT_COLLECTION_UPDATE,
130+
];
131+
132+
foreach ($this->discoverMainFormatters() as $formatterClass) {
133+
foreach ($eventTypes as $eventType) {
134+
try {
135+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
136+
$formatterClass,
137+
$eventType
138+
);
139+
$formatter->setContext($this->defaultContext);
140+
$this->assertNotNull($formatter);
141+
} catch (\Exception $e) {
142+
$this->markTestIncomplete(
143+
"{$formatterClass} may not support all event types"
144+
);
145+
}
146+
}
147+
}
148+
}
149+
150+
public function testAllFormattersHandleNullSubjectGracefully(): void
151+
{
152+
$invalidSubject = new \stdClass();
153+
$count = 0;
154+
155+
foreach ($this->discoverMainFormatters() as $formatterClass) {
156+
try {
157+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
158+
$formatterClass,
159+
IAuditStrategy::EVENT_ENTITY_CREATION
160+
);
161+
$formatter->setContext($this->defaultContext);
162+
163+
FormatterTestHelper::assertFormatterHandlesInvalidSubjectGracefully(
164+
$formatter,
165+
$invalidSubject
166+
);
167+
$count++;
168+
} catch (\Exception $e) {
169+
$this->fail("{$formatterClass}: " . $e->getMessage());
170+
}
171+
}
172+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
173+
}
174+
175+
public function testFormattersHandleNullContextGracefully(): void
176+
{
177+
$invalidSubject = new \stdClass();
178+
$count = 0;
179+
180+
foreach ($this->discoverMainFormatters() as $formatterClass) {
181+
try {
182+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
183+
$formatterClass,
184+
IAuditStrategy::EVENT_ENTITY_CREATION
185+
);
186+
187+
FormatterTestHelper::assertFormatterHandlesInvalidSubjectGracefully(
188+
$formatter,
189+
$invalidSubject
190+
);
191+
$count++;
192+
} catch (\Exception $e) {
193+
$this->fail("{$formatterClass}: " . $e->getMessage());
194+
}
195+
}
196+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
197+
}
198+
199+
public function testFormattersHandleEmptyChangeSetGracefully(): void
200+
{
201+
$count = 0;
202+
foreach ($this->discoverMainFormatters() as $formatterClass) {
203+
try {
204+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
205+
$formatterClass,
206+
IAuditStrategy::EVENT_ENTITY_UPDATE
207+
);
208+
$formatter->setContext($this->defaultContext);
209+
210+
FormatterTestHelper::assertFormatterHandlesEmptyChangesetGracefully($formatter);
211+
$count++;
212+
} catch (\Exception $e) {
213+
$this->fail("{$formatterClass}: " . $e->getMessage());
214+
}
215+
}
216+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
217+
}
218+
219+
public function testAllFormattersImplementCorrectInterfaces(): void
220+
{
221+
$count = 0;
222+
foreach ($this->discoverMainFormatters() as $formatterClass) {
223+
try {
224+
FormatterTestHelper::assertFormatterExtendsAbstractFormatter($formatterClass);
225+
FormatterTestHelper::assertFormatterHasValidFormatMethod($formatterClass);
226+
$count++;
227+
} catch (\Exception $e) {
228+
$this->fail("{$formatterClass}: " . $e->getMessage());
229+
}
230+
}
231+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
232+
}
233+
234+
public function testAllFormattersHaveCorrectFormatMethodSignature(): void
235+
{
236+
$count = 0;
237+
foreach ($this->discoverMainFormatters() as $formatterClass) {
238+
try {
239+
FormatterTestHelper::assertFormatterHasValidFormatMethod($formatterClass);
240+
$count++;
241+
} catch (\Exception $e) {
242+
$this->fail("{$formatterClass}: " . $e->getMessage());
243+
}
244+
}
245+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
246+
}
247+
248+
249+
public function testAuditContextHasRequiredFields(): void
250+
{
251+
$context = $this->defaultContext;
252+
$this->assertNotNull($context->userId);
253+
$this->assertNotNull($context->userEmail);
254+
$this->assertNotNull($context->userFirstName);
255+
$this->assertNotNull($context->userLastName);
256+
$this->assertNotNull($context->uiApp);
257+
$this->assertNotNull($context->uiFlow);
258+
$this->assertNotNull($context->route);
259+
$this->assertNotNull($context->httpMethod);
260+
$this->assertNotNull($context->clientIp);
261+
$this->assertNotNull($context->userAgent);
262+
}
263+
264+
public function testAuditStrategyDefinesAllEventTypes(): void
265+
{
266+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_CREATION'));
267+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_UPDATE'));
268+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_DELETION'));
269+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_COLLECTION_UPDATE'));
270+
}
271+
272+
public function testFactoryInstantiatesEntityCreationFormatterWithEventType(): void
273+
{
274+
$factory = new AuditLogFormatterFactory();
275+
$subject = new \stdClass();
276+
$formatter = $factory->make($this->defaultContext, $subject, IAuditStrategy::EVENT_ENTITY_CREATION);
277+
278+
$this->assertTrue(
279+
$formatter === null || $formatter instanceof IAuditLogFormatter,
280+
'Factory must return null or IAuditLogFormatter instance'
281+
);
282+
}
283+
}

0 commit comments

Comments
 (0)