feat(security): add audit log listeners and tests for client secret and status events

This commit is contained in:
Jérémy Munsch 2026-08-21 11:09:25 +02:00
parent 0d2b0124ee
commit dc83df4105
5 changed files with 277 additions and 0 deletions

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Infra\Listeners;
use Application\Security\Events\ClientSecretGenerated;
use Application\Security\Events\ClientSecretRegenerated;
use Psr\Log\LoggerInterface;
final readonly class LogClientSecretRotated
{
public function __construct(
private LoggerInterface $logger,
) {
}
public function handle(ClientSecretGenerated | ClientSecretRegenerated $event): void
{
$this->logger->info(
sprintf(
'Client secret rotated for client "%s" (ID %s)',
$event->clientIdentifier->value,
$event->clientId->value
),
[
'client_id' => $event->clientId->value,
'client_identifier' => $event->clientIdentifier->value,
'rotated_at' => $event->generatedAt->format('Y-m-d H:i:s'),
]
);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Infra\Listeners;
use Application\Security\Events\ClientStatusToggled;
use Psr\Log\LoggerInterface;
final readonly class LogClientStatusChanged
{
public function __construct(
private LoggerInterface $logger,
) {
}
public function handle(ClientStatusToggled $event): void
{
$this->logger->info(
sprintf(
'Client status changed for client "%s" (ID %s)',
$event->clientIdentifier->value,
$event->clientId->value
),
[
'client_id' => $event->clientId->value,
'client_identifier' => $event->clientIdentifier->value,
'is_active' => $event->isActive,
'toggled_at' => $event->toggledAt->format('Y-m-d H:i:s'),
]
);
}
}

View file

@ -4,10 +4,15 @@ declare(strict_types=1);
namespace Infra\Providers;
use Application\Security\Events\ClientSecretGenerated;
use Application\Security\Events\ClientSecretRegenerated;
use Application\Security\Events\ClientStatusToggled;
use Domain\Events\BrandCreated;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Infra\Listeners\LogBrandCreated;
use Infra\Listeners\LogClientSecretRotated;
use Infra\Listeners\LogClientStatusChanged;
class EventServiceProvider extends ServiceProvider
{
@ -17,5 +22,20 @@ class EventServiceProvider extends ServiceProvider
BrandCreated::class,
LogBrandCreated::class,
);
Event::listen(
ClientSecretGenerated::class,
LogClientSecretRotated::class,
);
Event::listen(
ClientSecretRegenerated::class,
LogClientSecretRotated::class,
);
Event::listen(
ClientStatusToggled::class,
LogClientStatusChanged::class,
);
}
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Listeners;
use Application\Security\Events\ClientSecretGenerated;
use Application\Security\Events\ClientSecretRegenerated;
use Application\Security\Models\ValueObjects\ClientIdentifier;
use Application\Security\Models\ValueObjects\Entities\ClientId;
use Infra\Listeners\LogClientSecretRotated;
use Mockery;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
covers(LogClientSecretRotated::class);
describe('LogClientSecretRotated Listener Unit Tests', function (): void {
it('logs informative message when client secret is generated', function (): void {
// GIVEN
$clientId = new ClientId(Uuid::uuid7());
$clientIdentifier = new ClientIdentifier('arcadia_id_11111111111111111111111111111111');
$generatedAt = new \DateTimeImmutable('2026-08-21 10:00:00');
$event = new ClientSecretGenerated(
clientId: $clientId,
clientIdentifier: $clientIdentifier,
generatedAt: $generatedAt,
);
/** @var LoggerInterface&Mockery\MockInterface $loggerMock */
$loggerMock = Mockery::mock(LoggerInterface::class);
$loggerMock->shouldReceive('info')
->once()
->with(
sprintf(
'Client secret rotated for client "%s" (ID %s)',
$clientIdentifier->value,
$clientId->value
),
[
'client_id' => $clientId->value,
'client_identifier' => $clientIdentifier->value,
'rotated_at' => '2026-08-21 10:00:00',
]
);
$listener = new LogClientSecretRotated($loggerMock);
// WHEN
$listener->handle($event);
// THEN
expect(true)->toBeTrue();
});
it('logs informative message when client secret is regenerated', function (): void {
// GIVEN
$clientId = new ClientId(Uuid::uuid7());
$clientIdentifier = new ClientIdentifier('arcadia_id_22222222222222222222222222222222');
$generatedAt = new \DateTimeImmutable('2026-08-21 11:00:00');
$event = new ClientSecretRegenerated(
clientId: $clientId,
clientIdentifier: $clientIdentifier,
generatedAt: $generatedAt,
);
/** @var LoggerInterface&Mockery\MockInterface $loggerMock */
$loggerMock = Mockery::mock(LoggerInterface::class);
$loggerMock->shouldReceive('info')
->once()
->with(
sprintf(
'Client secret rotated for client "%s" (ID %s)',
$clientIdentifier->value,
$clientId->value
),
[
'client_id' => $clientId->value,
'client_identifier' => $clientIdentifier->value,
'rotated_at' => '2026-08-21 11:00:00',
]
);
$listener = new LogClientSecretRotated($loggerMock);
// WHEN
$listener->handle($event);
// THEN
expect(true)->toBeTrue();
});
});

View file

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Infra\Listeners;
use Application\Security\Events\ClientStatusToggled;
use Application\Security\Models\ValueObjects\ClientIdentifier;
use Application\Security\Models\ValueObjects\Entities\ClientId;
use Infra\Listeners\LogClientStatusChanged;
use Mockery;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
covers(LogClientStatusChanged::class);
describe('LogClientStatusChanged Listener Unit Tests', function (): void {
it('logs informative message when client status is changed to active', function (): void {
// GIVEN
$clientId = new ClientId(Uuid::uuid7());
$clientIdentifier = new ClientIdentifier('arcadia_id_33333333333333333333333333333333');
$toggledAt = new \DateTimeImmutable('2026-08-21 12:00:00');
$event = new ClientStatusToggled(
clientId: $clientId,
clientIdentifier: $clientIdentifier,
isActive: true,
toggledAt: $toggledAt,
);
/** @var LoggerInterface&Mockery\MockInterface $loggerMock */
$loggerMock = Mockery::mock(LoggerInterface::class);
$loggerMock->shouldReceive('info')
->once()
->with(
sprintf(
'Client status changed for client "%s" (ID %s)',
$clientIdentifier->value,
$clientId->value
),
[
'client_id' => $clientId->value,
'client_identifier' => $clientIdentifier->value,
'is_active' => true,
'toggled_at' => '2026-08-21 12:00:00',
]
);
$listener = new LogClientStatusChanged($loggerMock);
// WHEN
$listener->handle($event);
// THEN
expect(true)->toBeTrue();
});
it('logs informative message when client status is changed to inactive', function (): void {
// GIVEN
$clientId = new ClientId(Uuid::uuid7());
$clientIdentifier = new ClientIdentifier('arcadia_id_44444444444444444444444444444444');
$toggledAt = new \DateTimeImmutable('2026-08-21 12:30:00');
$event = new ClientStatusToggled(
clientId: $clientId,
clientIdentifier: $clientIdentifier,
isActive: false,
toggledAt: $toggledAt,
);
/** @var LoggerInterface&Mockery\MockInterface $loggerMock */
$loggerMock = Mockery::mock(LoggerInterface::class);
$loggerMock->shouldReceive('info')
->once()
->with(
sprintf(
'Client status changed for client "%s" (ID %s)',
$clientIdentifier->value,
$clientId->value
),
[
'client_id' => $clientId->value,
'client_identifier' => $clientIdentifier->value,
'is_active' => false,
'toggled_at' => '2026-08-21 12:30:00',
]
);
$listener = new LogClientStatusChanged($loggerMock);
// WHEN
$listener->handle($event);
// THEN
expect(true)->toBeTrue();
});
});