From 451a2944f7d55cdbda39383eee07dce8f3615f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 10 May 2026 23:11:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(http):=20timestamp=20validation=20=C2=B124h?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20partition=20guard=20(Plan=202.6=20#iii)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрывает CV.11 audit WARN minor #5 (Carbon::createFromTimestamp(time) без range guard → INSERT CRASH "no partition of relation deals found for row" для timestamp вне текущего месячного окна deals_2026_MM). Изменение: SupplierWebhookController::receive — добавлено min/max constraint на 'time' = [now-24h, now+24h] unix-timestamp. Timestamp вне окна → 422 ValidationException. ±24h: покрывает retry-задержки поставщика (network-сбой) + clock-drift серверов; шире окно (±48h+) = риск partition-промаха на стыке месяцев (нужен Plan 5 partition cron). TDD: +3 теста (-2 days → 422; +2 days → 422; -6h → 202). Regression-fix: existing test 'inserts supplier_lead row' использовал hardcoded 'time' => 1703781939 (Dec 28 2023) — теперь out-of-window. Заменено на time(). phpstan-baseline: postJson() count: 8 → 11 (+3 от Task 3 тестов). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Api/SupplierWebhookController.php | 10 ++++- app/phpstan-baseline.neon | 2 +- .../Http/Webhook/SupplierWebhookTest.php | 37 ++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/app/app/Http/Controllers/Api/SupplierWebhookController.php b/app/app/Http/Controllers/Api/SupplierWebhookController.php index e6c8e660..4e5e0732 100644 --- a/app/app/Http/Controllers/Api/SupplierWebhookController.php +++ b/app/app/Http/Controllers/Api/SupplierWebhookController.php @@ -50,11 +50,19 @@ class SupplierWebhookController extends Controller return response()->json(['message' => 'Not found.'], 404); } + // Plan 2.6 fix #iii: timestamp partition guard. Партиции deals месячные + // (deals_2026_MM); time за пределами текущего месяца → INSERT CRASH + // "no partition of relation deals found for row" в RouteSupplierLeadJob. + // Окно ±24h защищает от wildly out-of-range значений (старый/будущий + // дроп от поставщика); покрывает retry-задержки + clock-drift серверов. + $minTime = now()->subDay()->getTimestamp(); + $maxTime = now()->addDay()->getTimestamp(); + $validated = $request->validate([ 'vid' => 'required|integer|min:1', 'project' => ['required', 'string', 'max:255', 'regex:/^B[123]_.+$/'], 'phone' => ['required', 'string', 'regex:/^7\d{10}$/'], - 'time' => 'required|integer|min:1', + 'time' => ['required', 'integer', "min:{$minTime}", "max:{$maxTime}"], 'tag' => 'nullable|string|max:255', 'phones' => 'nullable|array', 'phones.*' => 'string|regex:/^7\d{10}$/', diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index f238caf2..443856a3 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -597,7 +597,7 @@ parameters: - message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:postJson\(\)\.$#' identifier: method.notFound - count: 8 + count: 11 path: tests/Feature/Http/Webhook/SupplierWebhookTest.php - diff --git a/app/tests/Feature/Http/Webhook/SupplierWebhookTest.php b/app/tests/Feature/Http/Webhook/SupplierWebhookTest.php index 35532c29..ec94a041 100644 --- a/app/tests/Feature/Http/Webhook/SupplierWebhookTest.php +++ b/app/tests/Feature/Http/Webhook/SupplierWebhookTest.php @@ -54,7 +54,7 @@ it('inserts supplier_lead row + dispatches RouteSupplierLeadJob', function () { 'tag' => 'Ваш инвестор', 'phone' => '79991234567', 'phones' => ['79991234567'], - 'time' => 1703781939, + 'time' => time(), ]); $response->assertStatus(202); @@ -122,3 +122,38 @@ it('allows empty IP allowlist в testing env (Plan 2.6 fix #ii — fail-open д $response->assertStatus(202); }); + +it('rejects timestamp older than 24h (Plan 2.6 fix #iii — partition guard)', function () { + $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ + 'vid' => 100001, + 'project' => 'B1_old-time.ru', + 'phone' => '79991234567', + 'time' => now()->subDays(2)->getTimestamp(), + ]); + + $response->assertStatus(422)->assertJsonValidationErrors('time'); +}); + +it('rejects timestamp more than 24h in future (Plan 2.6 fix #iii — partition guard)', function () { + $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ + 'vid' => 100002, + 'project' => 'B1_future-time.ru', + 'phone' => '79991234567', + 'time' => now()->addDays(2)->getTimestamp(), + ]); + + $response->assertStatus(422)->assertJsonValidationErrors('time'); +}); + +it('accepts timestamp within ±24h window (Plan 2.6 fix #iii — partition guard)', function () { + Bus::fake(); + + $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ + 'vid' => 100003, + 'project' => 'B1_valid-time.ru', + 'phone' => '79991234567', + 'time' => now()->subHours(6)->getTimestamp(), + ]); + + $response->assertStatus(202); +});