Skip to content

Commit b1ae8a4

Browse files
committed
chore: Add unit test suit for Formatters
1 parent 6831fe5 commit b1ae8a4

11 files changed

Lines changed: 282 additions & 61 deletions

app/Audit/AbstractAuditLogFormatter.php

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace App\Audit;
44

5+
use App\Audit\Utils\DateFormatter;
6+
57
/**
68
* Copyright 2025 OpenStack Foundation
79
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +19,13 @@
1719

1820
abstract class AbstractAuditLogFormatter implements IAuditLogFormatter
1921
{
20-
protected AuditContext $ctx;
22+
protected ?AuditContext $ctx = null;
23+
protected string $event_type;
24+
25+
public function __construct(string $event_type)
26+
{
27+
$this->event_type = $event_type;
28+
}
2129

2230
final public function setContext(AuditContext $ctx): void
2331
{
@@ -26,6 +34,9 @@ final public function setContext(AuditContext $ctx): void
2634

2735
protected function getUserInfo(): string
2836
{
37+
if (app()->runningInConsole()) {
38+
return 'Worker Job';
39+
}
2940
if (!$this->ctx) {
3041
return 'Unknown (unknown)';
3142
}
@@ -41,5 +52,82 @@ protected function getUserInfo(): string
4152
return sprintf("%s (%s)", $user_name, $user_id);
4253
}
4354

44-
abstract public function format($subject, array $change_set): ?string;
55+
protected function formatAuditDate($date, string $format = 'Y-m-d H:i:s'): string
56+
{
57+
return DateFormatter::format($date, $format);
58+
}
59+
60+
protected function getIgnoredFields(): array
61+
{
62+
return [
63+
'last_created',
64+
'last_updated',
65+
'last_edited',
66+
'created_by',
67+
'updated_by'
68+
];
69+
}
70+
71+
protected function formatChangeValue($value): string
72+
{
73+
if (is_bool($value)) {
74+
return $value ? 'true' : 'false';
75+
}
76+
if (is_null($value)) {
77+
return 'null';
78+
}
79+
if ($value instanceof \DateTimeInterface) {
80+
return $value->format('Y-m-d H:i:s');
81+
}
82+
if ($value instanceof \Doctrine\Common\Collections\Collection) {
83+
$count = $value->count();
84+
return sprintf('Collection[%d items]', $count);
85+
}
86+
if (is_object($value)) {
87+
$className = get_class($value);
88+
return sprintf('%s', $className);
89+
}
90+
if (is_array($value)) {
91+
return sprintf('Array[%d items]', count($value));
92+
}
93+
return (string) $value;
94+
}
95+
96+
97+
protected function buildChangeDetails(array $change_set): string
98+
{
99+
$changed_fields = [];
100+
$ignored_fields = $this->getIgnoredFields();
101+
102+
foreach ($change_set as $prop_name => $change_values) {
103+
if (in_array($prop_name, $ignored_fields)) {
104+
continue;
105+
}
106+
107+
$old_value = $change_values[0] ?? null;
108+
$new_value = $change_values[1] ?? null;
109+
110+
$formatted_change = $this->formatFieldChange($prop_name, $old_value, $new_value);
111+
if ($formatted_change !== null) {
112+
$changed_fields[] = $formatted_change;
113+
}
114+
}
115+
116+
if (empty($changed_fields)) {
117+
return 'properties without changes registered';
118+
}
119+
120+
$fields_summary = count($changed_fields) . ' field(s) modified: ';
121+
return $fields_summary . implode(' | ', $changed_fields);
122+
}
123+
124+
protected function formatFieldChange(string $prop_name, $old_value, $new_value): ?string
125+
{
126+
$old_display = $this->formatChangeValue($old_value);
127+
$new_display = $this->formatChangeValue($new_value);
128+
129+
return sprintf("Property \"%s\" has changed from \"%s\" to \"%s\"", $prop_name, $old_display, $new_display);
130+
}
131+
132+
abstract public function format(mixed $subject, array $change_set): ?string;
45133
}

app/Audit/AuditLogFormatterFactory.php

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<?php namespace App\Audit;
1+
<?php
2+
namespace App\Audit;
23
/**
34
* Copyright 2025 OpenStack Foundation
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -27,13 +28,10 @@ class AuditLogFormatterFactory implements IAuditLogFormatterFactory
2728

2829
public function __construct()
2930
{
30-
try
31-
{
32-
Log::debug("AuditLogFormatterFactory::construct loading audit_log config");
31+
try {
3332
$this->config = config('audit_log', []);
34-
}
35-
catch(\Exception $ex){
36-
Log::error('Failed to load audit_log configuration', ['exception' => $ex]);
33+
} catch (\Exception $ex) {
34+
$this->config = [];
3735
}
3836
}
3937

@@ -64,8 +62,7 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
6462
$targetEntity = $type;
6563
}
6664
Log::debug("AuditLogFormatterFactory::make getTypeClass targetEntity {$targetEntity}");
67-
}
68-
elseif (method_exists($subject, 'getMapping')) {
65+
} elseif (method_exists($subject, 'getMapping')) {
6966
$mapping = $subject->getMapping();
7067
$targetEntity = $mapping['targetEntity'] ?? null;
7168
Log::debug("AuditLogFormatterFactory::make getMapping targetEntity {$targetEntity}");
@@ -78,7 +75,8 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
7875
$prop->setAccessible(true);
7976
$mapping = $prop->getValue($subject);
8077
$targetEntity = $mapping['targetEntity'] ?? null;
81-
if ($targetEntity) break;
78+
if ($targetEntity)
79+
break;
8280
}
8381
}
8482
}
@@ -108,26 +106,27 @@ public function make(AuditContext $ctx, $subject, $eventType): ?IAuditLogFormatt
108106
break;
109107
case IAuditStrategy::EVENT_ENTITY_CREATION:
110108
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
111-
if(is_null($formatter)) {
112-
$formatter = new EntityCreationAuditLogFormatter();
109+
if (is_null($formatter)) {
110+
$formatter = new EntityCreationAuditLogFormatter($eventType);
113111
}
114112
break;
115113
case IAuditStrategy::EVENT_ENTITY_DELETION:
116114
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
117-
if(is_null($formatter)) {
115+
if (is_null($formatter)) {
118116
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
119117
$formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter);
120118
}
121119
break;
122120
case IAuditStrategy::EVENT_ENTITY_UPDATE:
123121
$formatter = $this->getFormatterByContext($subject, $eventType, $ctx);
124-
if(is_null($formatter)) {
122+
if (is_null($formatter)) {
125123
$child_entity_formatter = ChildEntityFormatterFactory::build($subject);
126124
$formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter);
127125
}
128126
break;
129127
}
130-
if ($formatter === null) return null;
128+
if ($formatter === null)
129+
return null;
131130
$formatter->setContext($ctx);
132131
return $formatter;
133132
}
@@ -173,4 +172,4 @@ private function routeMatches(string $route, string $actual_route): bool
173172
{
174173
return strcmp($actual_route, $route) === 0;
175174
}
176-
}
175+
}

app/Audit/ConcreteFormatters/EntityCollectionUpdateAuditLogFormatter.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Audit\ConcreteFormatters\ChildEntityFormatters\IChildEntityAuditLogFormatter;
66
use App\Audit\AbstractAuditLogFormatter;
7+
use App\Audit\Interfaces\IAuditStrategy;
78
use Illuminate\Support\Facades\Log;
89
use ReflectionException;
910

@@ -34,13 +35,14 @@ class EntityCollectionUpdateAuditLogFormatter extends AbstractAuditLogFormatter
3435

3536
public function __construct(?IChildEntityAuditLogFormatter $child_entity_formatter)
3637
{
38+
parent::__construct(IAuditStrategy::EVENT_COLLECTION_UPDATE);
3739
$this->child_entity_formatter = $child_entity_formatter;
3840
}
3941

4042
/**
4143
* @inheritDoc
4244
*/
43-
public function format($subject, array $change_set): ?string
45+
public function format($subject, $change_set): ?string
4446
{
4547
try {
4648
if ($this->child_entity_formatter != null) {

app/Audit/ConcreteFormatters/EntityCreationAuditLogFormatter.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Audit\ConcreteFormatters;
44

55
use App\Audit\AbstractAuditLogFormatter;
6+
use App\Audit\Interfaces\IAuditStrategy;
67
use ReflectionClass;
78

89
/**
@@ -25,6 +26,11 @@
2526
*/
2627
class EntityCreationAuditLogFormatter extends AbstractAuditLogFormatter
2728
{
29+
public function __construct()
30+
{
31+
parent::__construct(IAuditStrategy::EVENT_ENTITY_CREATION);
32+
}
33+
2834
protected function getCreationIgnoredEntities(): array
2935
{
3036
return [
@@ -36,10 +42,14 @@ protected function getCreationIgnoredEntities(): array
3642
*/
3743
public function format($subject, $change_set): ?string
3844
{
39-
$class_name = class_basename(is_string($subject) ? $subject : get_class($subject));
45+
$class_name = (new ReflectionClass($subject))->getShortName();
4046
$ignored_entities = $this->getCreationIgnoredEntities();
4147
if (in_array($class_name, $ignored_entities))
4248
return null;
43-
return "{$class_name} created";
49+
50+
$entity_id = method_exists($subject, 'getId') ? $subject->getId() : 'N/A';
51+
$user_info = $this->getUserInfo();
52+
53+
return "{$class_name} (ID: {$entity_id}) created by {$user_info}";
4454
}
4555
}

app/Audit/ConcreteFormatters/EntityDeletionAuditLogFormatter.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace App\Audit\ConcreteFormatters;
44

5+
use App\Audit\Interfaces\IAuditStrategy;
6+
57
/**
68
* Copyright 2022 OpenStack Foundation
79
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,8 +33,9 @@ class EntityDeletionAuditLogFormatter extends AbstractAuditLogFormatter
3133
*/
3234
private $child_entity_formatter;
3335

34-
public function __construct(?IChildEntityAuditLogFormatter $child_entity_formatter)
36+
public function __construct(?IChildEntityAuditLogFormatter $child_entity_formatter = null)
3537
{
38+
parent::__construct(IAuditStrategy::EVENT_ENTITY_DELETION);
3639
$this->child_entity_formatter = $child_entity_formatter;
3740
}
3841

@@ -47,7 +50,7 @@ protected function getCreationIgnoredEntities(): array
4750
*/
4851
public function format($subject, $change_set): ?string
4952
{
50-
$class_name = class_basename(is_string($subject) ? $subject : get_class($subject));
53+
$class_name = (new ReflectionClass($subject))->getShortName();
5154
$ignored_entities = $this->getCreationIgnoredEntities();
5255
if (in_array($class_name, $ignored_entities))
5356
return null;
@@ -57,6 +60,8 @@ public function format($subject, $change_set): ?string
5760
->format($subject, IChildEntityAuditLogFormatter::CHILD_ENTITY_DELETION);
5861
}
5962

60-
return "{$class_name} deleted";
63+
$entity_id = method_exists($subject, 'getId') ? $subject->getId() : 'unknown';
64+
$user_info = $this->getUserInfo();
65+
return "{$class_name} (ID: {$entity_id}) deleted by {$user_info}";
6166
}
6267
}

app/Audit/ConcreteFormatters/EntityUpdateAuditLogFormatter.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22
namespace App\Audit\ConcreteFormatters;
3+
4+
use App\Audit\Interfaces\IAuditStrategy;
35
/**
46
* Copyright 2022 OpenStack Foundation
57
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,12 +40,13 @@ class EntityUpdateAuditLogFormatter extends AbstractAuditLogFormatter
3840
*/
3941
private $child_entity_formatter;
4042

41-
public function __construct(?IChildEntityAuditLogFormatter $child_entity_formatter)
43+
public function __construct(?IChildEntityAuditLogFormatter $child_entity_formatter = null)
4244
{
45+
parent::__construct(IAuditStrategy::EVENT_ENTITY_UPDATE);
4346
$this->child_entity_formatter = $child_entity_formatter;
4447
}
4548

46-
protected function getIgnoredFields()
49+
protected function getIgnoredFields(): array
4750
{
4851
return [
4952
'last_created',
@@ -87,7 +90,7 @@ private static function formatEntity(string $parent_class, string $prop_name, ?I
8790
public function format($subject, $change_set): ?string
8891
{
8992
$res = [];
90-
$class_name = class_basename(is_string($subject) ? $subject : get_class($subject));
93+
$class_name = (new ReflectionClass($subject))->getShortName();
9194
$ignored_fields = $this->getIgnoredFields();
9295

9396
foreach (array_keys($change_set) as $prop_name) {
@@ -144,8 +147,8 @@ public function format($subject, $change_set): ?string
144147
}
145148

146149
if ($old_value instanceof DateTime || $new_value instanceof DateTime) {
147-
$old_value = $old_value != null ? $old_value->format('Y-m-d H:i:s') : "";
148-
$new_value = $new_value != null ? $new_value->format('Y-m-d H:i:s') : "";
150+
$old_value = $old_value != null ? $this->formatAuditDate($old_value) : "";
151+
$new_value = $new_value != null ? $this->formatAuditDate($new_value) : "";
149152
} else if (is_bool($old_value) || is_bool($new_value)) {
150153
$old_value = $old_value ? 'true' : 'false';
151154
$new_value = $new_value ? 'true' : 'false';
@@ -162,6 +165,10 @@ public function format($subject, $change_set): ?string
162165
if (count($res) == 0)
163166
return null;
164167

165-
return join("|", $res);
168+
$entity_id = method_exists($subject, 'getId') ? $subject->getId() : 'N/A';
169+
$user_info = $this->getUserInfo();
170+
$message = join("|", $res);
171+
172+
return "{$class_name} (ID: {$entity_id}) updated by {$user_info}: {$message}";
166173
}
167174
}

0 commit comments

Comments
 (0)