| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Controllers;
- use App\Controllers\AuditController;
- use PHPUnit\Framework\Attributes\DataProvider;
- use PHPUnit\Framework\TestCase;
- /**
- * R01-N12: pin AuditController::validateDateFilters. The repo concatenates
- * "T00:00:00Z" / "T23:59:59Z" onto whatever string it gets and binds the
- * result against the ISO-8601 `occurred_at` column — garbage in silently
- * hides rows. The controller is the gate; this test pins which inputs
- * pass through and which get dropped + flagged.
- */
- final class AuditControllerTest extends TestCase
- {
- /**
- * @return list<array{string,string,string,string,list<string>,string}>
- */
- public static function dateCases(): array
- {
- // [from, to, expectedFrom, expectedTo, expectedErrorKeys, label]
- return [
- ['', '', '', '', [], 'no filters → no errors'],
- ['2026-01-31', '', '2026-01-31', '', [], 'from-only valid'],
- ['', '2026-12-31', '', '2026-12-31', [], 'to-only valid'],
- ['2026-01-01', '2026-12-31', '2026-01-01', '2026-12-31', [], 'both valid'],
- ['2024/01/01', '', '', '', ['from_date'], 'slashes → from dropped, error reported'],
- ['', '2024/01/01', '', '', ['to_date'], 'slashes → to dropped, error reported'],
- ['2024/01/01', '2024/12/31', '', '', ['from_date', 'to_date'], 'both garbage → both dropped, both errors'],
- ['2026-01-31', 'tomorrow', '2026-01-31', '', ['to_date'], 'one-side invalid → only that side errors, the other passes'],
- ['2026-13-01', '', '', '', ['from_date'], 'impossible month → rejected'],
- ['2026-02-30', '', '', '', ['from_date'], 'impossible day for month → rejected (strict round-trip)'],
- ['2026-1-1', '', '', '', ['from_date'], 'unpadded → rejected (strict round-trip)'],
- [' 2026-01-01', '', '', '', ['from_date'], 'leading space → rejected'],
- ['2026-01-01 ', '', '', '', ['from_date'], 'trailing space → rejected'],
- ["'; DROP TABLE audit_log; --", '', '', '', ['from_date'], 'injection-shaped string → rejected'],
- ];
- }
- /** @param list<string> $expectedErrorKeys */
- #[DataProvider('dateCases')]
- public function testValidateDateFilters(
- string $from,
- string $to,
- string $expectedFrom,
- string $expectedTo,
- array $expectedErrorKeys,
- string $label,
- ): void {
- $out = AuditController::validateDateFilters($from, $to);
- $this->assertSame($expectedFrom, $out['from'], "{$label}: from");
- $this->assertSame($expectedTo, $out['to'], "{$label}: to");
- $this->assertSame(
- $expectedErrorKeys,
- array_keys($out['errors']),
- "{$label}: error keys",
- );
- // Error messages are non-empty when present (keeps the contract for
- // the view, which renders {{ dateErrors.from_date }} verbatim).
- foreach ($out['errors'] as $key => $msg) {
- $this->assertNotSame('', $msg, "{$label}: error message for {$key} is non-empty");
- }
- }
- public function testValidatorPreservesValidValuesVerbatim(): void
- {
- // Boundary that the repo concatenates onto: a leap day. Confirms
- // we don't normalise or reformat — the repo gets exactly what the
- // user typed (after gating).
- $out = AuditController::validateDateFilters('2024-02-29', '2024-02-29');
- $this->assertSame('2024-02-29', $out['from']);
- $this->assertSame('2024-02-29', $out['to']);
- $this->assertSame([], $out['errors']);
- }
- public function testValidatorRejectsNonLeapFebTwentyNine(): void
- {
- // 2025 is not a leap year. createFromFormat is lenient and silently
- // rolls over to 2025-03-01; the round-trip equality check we use
- // is what catches it.
- $out = AuditController::validateDateFilters('2025-02-29', '');
- $this->assertSame('', $out['from']);
- $this->assertArrayHasKey('from_date', $out['errors']);
- }
- }
|