* 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\Repositories; use App\Repositories\AuditRepository; use App\Services\AuditLogger; use App\Tests\TestCase; use InvalidArgumentException; /** * R01-N11: AuditRepository::distinctColumn interpolates its argument into * SQL. The method is private and both internal callers pass literals, but * the runtime whitelist guard makes the contract explicit so a future * refactor can't open an injection vector. */ final class AuditRepositoryTest extends TestCase { public function testDistinctActionsReturnsSortedUniqueValues(): void { $pdo = $this->makeDb(); $logger = new AuditLogger($pdo); $logger->record('UPDATE', 'worker', 1, ['n' => 1], ['n' => 2]); $logger->record('CREATE', 'worker', 2, null, ['n' => 1]); $logger->record('UPDATE', 'sprint', 3, ['n' => 1], ['n' => 2]); $repo = new AuditRepository($pdo); $this->assertSame(['CREATE', 'UPDATE'], $repo->distinctActions()); } public function testDistinctEntityTypesReturnsSortedUniqueValues(): void { $pdo = $this->makeDb(); $logger = new AuditLogger($pdo); $logger->record('UPDATE', 'worker', 1, ['n' => 1], ['n' => 2]); $logger->record('UPDATE', 'sprint', 2, ['n' => 1], ['n' => 2]); $logger->record('UPDATE', 'worker', 3, ['n' => 1], ['n' => 2]); $repo = new AuditRepository($pdo); $this->assertSame(['sprint', 'worker'], $repo->distinctEntityTypes()); } public function testDistinctColumnRejectsUnknownColumnViaReflection(): void { $pdo = $this->makeDb(); $repo = new AuditRepository($pdo); $rm = new \ReflectionMethod($repo, 'distinctColumn'); $rm->setAccessible(true); $this->expectException(InvalidArgumentException::class); $rm->invoke($repo, 'user_email'); } public function testDistinctColumnRejectsInjectionAttemptViaReflection(): void { $pdo = $this->makeDb(); $repo = new AuditRepository($pdo); $rm = new \ReflectionMethod($repo, 'distinctColumn'); $rm->setAccessible(true); $this->expectException(InvalidArgumentException::class); $rm->invoke($repo, 'action; DROP TABLE audit_log; --'); } }