|
@@ -0,0 +1,91 @@
|
|
|
|
|
+<?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']);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|