| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- <?php
- /*
- * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * See the LICENSE file in the project root for the full license text.
- */
- declare(strict_types=1);
- namespace App\Tests\Services;
- use App\Services\AuditLogger;
- use App\Tests\TestCase;
- final class AuditLoggerTest extends TestCase
- {
- public function testRealUpdateProducesExactlyOneRow(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $id = $logger->record(
- action: 'UPDATE',
- entityType: 'worker',
- entityId: 1,
- before: ['name' => 'Alice', 'is_active' => 1],
- after: ['name' => 'Alice Cooper', 'is_active' => 1],
- );
- $this->assertIsInt($id);
- $this->assertSame(1, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
- }
- public function testNoopUpdateWritesNoRow(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $snap = ['name' => 'Alice', 'is_active' => 1, 'default_rtb' => 0.1];
- $result = $logger->record(
- action: 'UPDATE',
- entityType: 'worker',
- entityId: 1,
- before: $snap,
- after: $snap,
- );
- $this->assertNull($result);
- $this->assertSame(0, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
- }
- public function testNoopUpdateIsKeyOrderInsensitive(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $before = ['name' => 'Alice', 'is_active' => 1];
- $after = ['is_active' => 1, 'name' => 'Alice']; // reordered keys
- $result = $logger->record('UPDATE', 'worker', 1, $before, $after);
- $this->assertNull($result);
- $this->assertSame(0, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
- }
- public function testCreateActionAlwaysWritesEvenWhenBeforeEqualsAfter(): void
- {
- // The no-op rule applies to UPDATE only. CREATE/DELETE always write.
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $logger->record(
- action: 'CREATE',
- entityType: 'worker',
- entityId: 1,
- before: null,
- after: ['name' => 'Alice'],
- );
- $this->assertSame(1, (int) $pdo->query('SELECT COUNT(*) FROM audit_log')->fetchColumn());
- }
- public function testDeleteCarriesBeforeJsonAndNullAfterJson(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $logger->record(
- action: 'DELETE',
- entityType: 'worker',
- entityId: 42,
- before: ['id' => 42, 'name' => 'Bob'],
- after: null,
- userId: 7,
- userEmail: 'admin@x',
- ipAddress: '1.2.3.4',
- userAgent: 'test-ua',
- );
- $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
- $this->assertSame('DELETE', $row['action']);
- $this->assertSame('worker', $row['entity_type']);
- $this->assertSame(42, (int) $row['entity_id']);
- $this->assertSame(7, (int) $row['user_id']);
- $this->assertSame('admin@x', $row['user_email']);
- $this->assertSame('1.2.3.4', $row['ip_address']);
- $this->assertSame('test-ua', $row['user_agent']);
- $this->assertIsString($row['before_json']);
- $this->assertSame('{"id":42,"name":"Bob"}', $row['before_json']);
- $this->assertNull($row['after_json']);
- }
- public function testLoginEventWritesWithNullBeforeAndAfter(): void
- {
- // LOGIN events are not mutations and carry no before/after.
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $logger->record(
- action: 'LOGIN',
- entityType: 'user',
- entityId: 1,
- before: null,
- after: null,
- );
- $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
- $this->assertSame('LOGIN', $row['action']);
- $this->assertNull($row['before_json']);
- $this->assertNull($row['after_json']);
- }
- public function testLoginFailedSupportsNullUser(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $logger->record(
- action: 'LOGIN_FAILED',
- entityType: 'user',
- entityId: null,
- before: null,
- after: ['reason' => 'bad_signature'],
- );
- $row = $pdo->query('SELECT * FROM audit_log LIMIT 1')->fetch();
- $this->assertSame('LOGIN_FAILED', $row['action']);
- $this->assertNull($row['entity_id']);
- $this->assertNull($row['user_id']);
- $this->assertSame('{"reason":"bad_signature"}', $row['after_json']);
- }
- public function testOccurredAtIsIsoUtc(): void
- {
- $pdo = $this->makeDb();
- $logger = new AuditLogger($pdo);
- $logger->record('CREATE', 'worker', 1, null, ['name' => 'Alice']);
- $ts = (string) $pdo->query('SELECT occurred_at FROM audit_log LIMIT 1')->fetchColumn();
- $this->assertMatchesRegularExpression(
- '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/',
- $ts,
- );
- }
- }
|