Skip to content

Commit b52022f

Browse files
committed
chore: Add Unit Tests
1 parent b88e223 commit b52022f

File tree

3 files changed

+778
-0
lines changed

3 files changed

+778
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
<?php
2+
3+
namespace Tests\OpenTelemetry\Formatters;
4+
5+
/**
6+
* Copyright 2026 OpenStack Foundation
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
**/
17+
18+
use App\Audit\AbstractAuditLogFormatter;
19+
use App\Audit\AuditLogFormatterFactory;
20+
use App\Audit\AuditContext;
21+
use App\Audit\Interfaces\IAuditStrategy;
22+
use App\Audit\IAuditLogFormatter;
23+
use Illuminate\Support\Facades\App;
24+
use RecursiveDirectoryIterator;
25+
use RecursiveIteratorIterator;
26+
use Tests\OpenTelemetry\Formatters\Support\FormatterTestHelper;
27+
use Tests\OpenTelemetry\Formatters\Support\AuditContextBuilder;
28+
use PHPUnit\Framework\TestCase;
29+
30+
class AllFormattersIntegrationTest extends TestCase
31+
{
32+
private AuditContext $defaultContext;
33+
private const BASE_FORMATTERS_NAMESPACE = 'App\\Audit\\ConcreteFormatters\\';
34+
private string $BASE_FORMATTERS_DIR;
35+
private string $AbstractAuditLogFormatterClass;
36+
private const CHILD_ENTITY_DIR_NAME = 'ChildEntityFormatters';
37+
38+
public function __construct()
39+
{
40+
$this->BASE_FORMATTERS_DIR = App::path('Audit' . DIRECTORY_SEPARATOR . 'ConcreteFormatters');
41+
$this->AbstractAuditLogFormatterClass = AbstractAuditLogFormatter::class;
42+
}
43+
44+
protected function setUp(): void
45+
{
46+
parent::setUp();
47+
$this->defaultContext = AuditContextBuilder::default()->build();
48+
}
49+
50+
private function discoverFormatters(?string $directory = null): array
51+
{
52+
$directory = $directory ?? $this->BASE_FORMATTERS_DIR;
53+
$formatters = [];
54+
55+
if (!is_dir($directory)) {
56+
return $formatters;
57+
}
58+
59+
$iterator = new RecursiveIteratorIterator(
60+
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
61+
);
62+
63+
foreach ($iterator as $file) {
64+
if (
65+
$file->getExtension() !== 'php' or
66+
($file->isDir() and $file->getBasename() === self::CHILD_ENTITY_DIR_NAME)
67+
) {
68+
continue;
69+
}
70+
71+
$className = $this->buildClassName($file->getPathname(), $directory);
72+
73+
if (class_exists($className) && $this->isMainFormatter($className)) {
74+
$formatters[] = $className;
75+
}
76+
}
77+
78+
return array_values($formatters);
79+
}
80+
81+
82+
private function buildClassName(string $filePath, string $basePath): string
83+
{
84+
$relativePath = str_replace([$basePath . DIRECTORY_SEPARATOR, '.php'], '', $filePath);
85+
$classPath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
86+
return self::BASE_FORMATTERS_NAMESPACE . $classPath;
87+
}
88+
89+
private function isMainFormatter(string $className): bool
90+
{
91+
try {
92+
$reflection = new \ReflectionClass($className);
93+
94+
if ($reflection->isAbstract() || $reflection->isInterface()) {
95+
return false;
96+
}
97+
98+
$genericFormatters = [
99+
'EntityCreationAuditLogFormatter',
100+
'EntityDeletionAuditLogFormatter',
101+
'EntityUpdateAuditLogFormatter',
102+
'EntityCollectionUpdateAuditLogFormatter',
103+
];
104+
105+
return !in_array($reflection->getShortName(), $genericFormatters) &&
106+
$reflection->isSubclassOf(AbstractAuditLogFormatter::class);
107+
} catch (\ReflectionException $e) {
108+
return false;
109+
}
110+
}
111+
112+
public function testAllFormattersCanBeInstantiated(): void
113+
{
114+
foreach ($this->discoverFormatters() as $formatterClass) {
115+
try {
116+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
117+
$formatterClass,
118+
IAuditStrategy::EVENT_ENTITY_CREATION
119+
);
120+
121+
FormatterTestHelper::assertFormatterHasSetContextMethod($formatter);
122+
$formatter->setContext($this->defaultContext);
123+
$this->assertNotNull($formatter);
124+
} catch (\Exception $e) {
125+
$this->fail("Failed to validate {$formatterClass}: " . $e->getMessage());
126+
}
127+
}
128+
}
129+
130+
public function testAllFormatterConstructorParametersRequired(): void
131+
{
132+
$errors = [];
133+
$count = 0;
134+
135+
foreach ($this->discoverFormatters() as $formatterClass) {
136+
try {
137+
FormatterTestHelper::assertFormatterHasValidConstructor($formatterClass);
138+
$count++;
139+
} catch (\Exception $e) {
140+
$errors[] = "{$formatterClass}: " . $e->getMessage();
141+
}
142+
}
143+
144+
$this->assertEmpty($errors, implode("\n", $errors));
145+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
146+
}
147+
148+
public function testAllFormattersHandleAllEventTypes(): void
149+
{
150+
$eventTypes = [
151+
IAuditStrategy::EVENT_ENTITY_CREATION,
152+
IAuditStrategy::EVENT_ENTITY_UPDATE,
153+
IAuditStrategy::EVENT_ENTITY_DELETION,
154+
IAuditStrategy::EVENT_COLLECTION_UPDATE,
155+
];
156+
157+
$errors = [];
158+
$unsupported = [];
159+
160+
foreach ($this->discoverFormatters() as $formatterClass) {
161+
foreach ($eventTypes as $eventType) {
162+
try {
163+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
164+
$formatterClass,
165+
$eventType
166+
);
167+
$formatter->setContext($this->defaultContext);
168+
$this->assertNotNull($formatter);
169+
} catch (\Exception $e) {
170+
if (strpos($e->getMessage(), 'event type') !== false) {
171+
$unsupported[] = "{$formatterClass} does not support {$eventType}";
172+
} else {
173+
$errors[] = "{$formatterClass} with {$eventType}: " . $e->getMessage();
174+
}
175+
}
176+
}
177+
}
178+
179+
$this->assertEmpty($errors, "Event type handling failed:\n" . implode("\n", $errors));
180+
}
181+
182+
public function testAllFormattersHandleInvalidSubjectGracefully(): void
183+
{
184+
$errors = [];
185+
$count = 0;
186+
187+
foreach ($this->discoverFormatters() as $formatterClass) {
188+
try {
189+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
190+
$formatterClass,
191+
IAuditStrategy::EVENT_ENTITY_CREATION
192+
);
193+
$formatter->setContext($this->defaultContext);
194+
195+
FormatterTestHelper::assertFormatterHandlesInvalidSubjectGracefully($formatter, new \stdClass());
196+
$count++;
197+
} catch (\Exception $e) {
198+
$errors[] = "{$formatterClass}: " . $e->getMessage();
199+
}
200+
}
201+
202+
$this->assertEmpty($errors, implode("\n", $errors));
203+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
204+
}
205+
206+
public function testAllFormattersHandleMissingContextGracefully(): void
207+
{
208+
$errors = [];
209+
$count = 0;
210+
211+
foreach ($this->discoverFormatters() as $formatterClass) {
212+
try {
213+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
214+
$formatterClass,
215+
IAuditStrategy::EVENT_ENTITY_CREATION
216+
);
217+
218+
$result = $formatter->format(new \stdClass(), []);
219+
220+
$this->assertNull(
221+
$result,
222+
"{$formatterClass}::format() must return null when context not set, got " .
223+
(is_string($result) ? "'{$result}'" : gettype($result))
224+
);
225+
$count++;
226+
} catch (\Exception $e) {
227+
$errors[] = "{$formatterClass} threw exception without context: " . $e->getMessage();
228+
}
229+
}
230+
231+
$this->assertEmpty($errors, implode("\n", $errors));
232+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
233+
}
234+
235+
public function testFormattersHandleEmptyChangeSetGracefully(): void
236+
{
237+
$errors = [];
238+
$count = 0;
239+
240+
foreach ($this->discoverFormatters() as $formatterClass) {
241+
try {
242+
$formatter = FormatterTestHelper::assertFormatterCanBeInstantiated(
243+
$formatterClass,
244+
IAuditStrategy::EVENT_ENTITY_UPDATE
245+
);
246+
$formatter->setContext($this->defaultContext);
247+
248+
FormatterTestHelper::assertFormatterHandlesEmptyChangesetGracefully($formatter);
249+
$count++;
250+
} catch (\Exception $e) {
251+
$errors[] = "{$formatterClass}: " . $e->getMessage();
252+
}
253+
}
254+
255+
$this->assertEmpty($errors, implode("\n", $errors));
256+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
257+
}
258+
259+
public function testAllFormattersImplementCorrectInterfaces(): void
260+
{
261+
$errors = [];
262+
$count = 0;
263+
264+
foreach ($this->discoverFormatters() as $formatterClass) {
265+
try {
266+
FormatterTestHelper::assertFormatterExtendsAbstractFormatter($formatterClass);
267+
FormatterTestHelper::assertFormatterHasValidFormatMethod($formatterClass);
268+
$count++;
269+
} catch (\Exception $e) {
270+
$errors[] = "{$formatterClass}: " . $e->getMessage();
271+
}
272+
}
273+
274+
$this->assertEmpty($errors, implode("\n", $errors));
275+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
276+
}
277+
278+
public function testAllFormattersHaveCorrectFormatMethodSignature(): void
279+
{
280+
$errors = [];
281+
$count = 0;
282+
283+
foreach ($this->discoverFormatters() as $formatterClass) {
284+
try {
285+
FormatterTestHelper::assertFormatterHasValidFormatMethod($formatterClass);
286+
$count++;
287+
} catch (\Exception $e) {
288+
$errors[] = "{$formatterClass}: " . $e->getMessage();
289+
}
290+
}
291+
292+
$this->assertEmpty($errors, implode("\n", $errors));
293+
$this->assertGreaterThan(0, $count, 'At least one formatter should be validated');
294+
}
295+
296+
297+
public function testAuditContextHasRequiredFields(): void
298+
{
299+
$context = $this->defaultContext;
300+
301+
$this->assertIsInt($context->userId);
302+
$this->assertGreaterThan(0, $context->userId);
303+
304+
$this->assertIsString($context->userEmail);
305+
$this->assertNotEmpty($context->userEmail);
306+
$this->assertNotFalse(
307+
filter_var($context->userEmail, FILTER_VALIDATE_EMAIL),
308+
"User email '{$context->userEmail}' is not valid"
309+
);
310+
311+
$this->assertIsString($context->userFirstName);
312+
$this->assertNotEmpty($context->userFirstName);
313+
314+
$this->assertIsString($context->userLastName);
315+
$this->assertNotEmpty($context->userLastName);
316+
317+
$this->assertIsString($context->uiApp);
318+
$this->assertNotEmpty($context->uiApp);
319+
320+
$this->assertIsString($context->uiFlow);
321+
$this->assertNotEmpty($context->uiFlow);
322+
323+
$this->assertIsString($context->route);
324+
$this->assertNotEmpty($context->route);
325+
326+
$this->assertIsString($context->httpMethod);
327+
$this->assertNotEmpty($context->httpMethod);
328+
329+
$this->assertIsString($context->clientIp);
330+
$this->assertNotEmpty($context->clientIp);
331+
$this->assertNotFalse(
332+
filter_var($context->clientIp, FILTER_VALIDATE_IP),
333+
"Client IP '{$context->clientIp}' is not valid"
334+
);
335+
336+
$this->assertIsString($context->userAgent);
337+
$this->assertNotEmpty($context->userAgent);
338+
}
339+
340+
public function testAuditStrategyDefinesAllEventTypes(): void
341+
{
342+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_CREATION'));
343+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_UPDATE'));
344+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_ENTITY_DELETION'));
345+
$this->assertTrue(defined('App\Audit\Interfaces\IAuditStrategy::EVENT_COLLECTION_UPDATE'));
346+
}
347+
348+
public function testFactoryInstantiatesCorrectFormatterForSubject(): void
349+
{
350+
$factory = new AuditLogFormatterFactory();
351+
352+
$unknownSubject = new \stdClass();
353+
$formatter = $factory->make($this->defaultContext, $unknownSubject, IAuditStrategy::EVENT_ENTITY_CREATION);
354+
355+
$this->assertTrue(
356+
$formatter === null || $formatter instanceof IAuditLogFormatter,
357+
'Factory must return null or IAuditLogFormatter for unknown subject type'
358+
);
359+
360+
$validSubject = new class {
361+
public function __toString()
362+
{
363+
return 'MockEntity';
364+
}
365+
};
366+
367+
$formatter = $factory->make($this->defaultContext, $validSubject, IAuditStrategy::EVENT_ENTITY_CREATION);
368+
369+
if ($formatter !== null) {
370+
$this->assertInstanceOf(
371+
IAuditLogFormatter::class,
372+
$formatter,
373+
'Factory must return IAuditLogFormatter instance for valid subject'
374+
);
375+
376+
$this->assertNotNull($formatter, 'Returned formatter must not be null');
377+
}
378+
}
379+
}

0 commit comments

Comments
 (0)