fix(supplier): Plan 3 Task 3 code-review fixes (4 Important + 3 Minor)
Закрывает 4 Important issues из code-review Task 3 (6d6181b):
- config/database.php: inline 11-key duplication заменён на single-source
pattern через локальную переменную $pgsqlConnection (config() внутри
config-файла не работает — Repository ещё не bootstrap'нут); 'pgsql' и
'pgsql_supplier' теперь оба ссылаются на $pgsqlConnection; PDO options
block с string-key _role_purpose удалён (PDO ждёт integer ATTR_* keys)
- tests/Concerns/SharesSupplierPdo.php (новый): trait для cross-connection
PDO visibility в DatabaseTransactions; setUp override из TestCase.php
удалён (был global на 562 теста, forced eager PDO connect);
trait применён к 5 supplier-flow тестам: SupplierConnectionTest,
LeadRouterTest, RouteSupplierLeadJobTest, ResetDeliveredTodayCommandTest,
SupplierLeadFlowTest (все нуждаются в cross-connection видимости)
- phpstan-baseline.neon: entry для Pest TestCall->artisan() в
SupplierConnectionTest заменён на inline @phpstan-ignore-next-line
— local + self-documenting; добавлен baseline-entry для
SharesSupplierPdo trait.unused (PHPStan не видит Pest uses() как trait usage)
Plus 3 Minor:
- typos 'dafault'/'corretly' (удалились с setUp override из TestCase.php)
- RouteSupplierLeadJob.php PHPDoc: \$connection → DB_CONNECTION консистентность
Pest: 562 tests, 560 passed + 2 skipped (без regression). PHPStan: 0 errors. Pint: clean.
This commit is contained in:
@@ -306,7 +306,7 @@ class RouteSupplierLeadJob implements ShouldQueue
|
||||
* tenant ещё не определён на момент routing'а) для ручного разбора.
|
||||
* supplier_lead.error апдейтится текстом исключения.
|
||||
*
|
||||
* INSERT с tenant_id=NULL проходит благодаря $connection='pgsql_supplier'
|
||||
* INSERT с tenant_id=NULL проходит благодаря DB_CONNECTION='pgsql_supplier'
|
||||
* (BYPASSRLS-роль crm_supplier_worker — обходит политику tenant_isolation,
|
||||
* которая под обычной ролью отвергла бы NULL). Закрыто Plan 3 Task 3.
|
||||
*/
|
||||
|
||||
+29
-34
@@ -3,6 +3,29 @@
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
// pgsql base config — single source of truth для pgsql + pgsql_supplier (Plan 3 Task 3).
|
||||
// config('database.connections.pgsql') внутри этого файла не работает: config Repository
|
||||
// ещё не bootstrap'нут на момент return этого массива (chicken-and-egg). Используем
|
||||
// локальную переменную: pgsql_supplier = $pgsqlConnection + override (username/password).
|
||||
$pgsqlConnection = [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
// PG session timezone = UTC. Без этого TIMESTAMPTZ возвращается с локальным offset
|
||||
// (+03), а Carbon::parse теряет offset → password reset token expiry-check
|
||||
// и аналогичные TZ-чувствительные сравнения ломаются.
|
||||
'timezone' => env('DB_TIMEZONE', 'UTC'),
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -84,24 +107,7 @@ return [
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
// PG session timezone = UTC. Без этого TIMESTAMPTZ возвращается с локальным offset
|
||||
// (+03), а Carbon::parse теряет offset → password reset token expiry-check
|
||||
// и аналогичные TZ-чувствительные сравнения ломаются.
|
||||
'timezone' => env('DB_TIMEZONE', 'UTC'),
|
||||
],
|
||||
'pgsql' => $pgsqlConnection,
|
||||
|
||||
// Plan 3 Task 3: dedicated PG connection для supplier-flow под BYPASSRLS-ролью
|
||||
// crm_supplier_worker (создана Plan 2.6 #iv 7899071 в db/00_create_roles.sql v1.1).
|
||||
@@ -123,25 +129,14 @@ return [
|
||||
// Brainstorm decision: вариант C (BYPASSRLS-role) из 3 опций. См.
|
||||
// docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.
|
||||
'pgsql_supplier' => array_merge(
|
||||
$pgsqlConnection,
|
||||
[
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
'timezone' => env('DB_TIMEZONE', 'UTC'),
|
||||
],
|
||||
[
|
||||
// crm_supplier_worker (BYPASSRLS) for supplier-flow jobs.
|
||||
// Plan 3 Task 3 — закрывает BLOCKER #6 + WARN #2/#3.
|
||||
// На dev fallback на default DB_USERNAME/DB_PASSWORD; на prod
|
||||
// ОБЯЗАТЕЛЬНО задать DB_SUPPLIER_USERNAME=crm_supplier_worker.
|
||||
'username' => env('DB_SUPPLIER_USERNAME', env('DB_USERNAME', 'root')),
|
||||
'password' => env('DB_SUPPLIER_PASSWORD', env('DB_PASSWORD', '')),
|
||||
'options' => [
|
||||
'_role_purpose' => 'crm_supplier_worker (BYPASSRLS) for supplier-flow jobs',
|
||||
],
|
||||
]
|
||||
),
|
||||
|
||||
|
||||
@@ -756,12 +756,6 @@ parameters:
|
||||
count: 11
|
||||
path: tests/Feature/RemindersDispatchDueTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:artisan\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/Feature/Supplier/SupplierConnectionTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
|
||||
identifier: property.notFound
|
||||
@@ -923,3 +917,9 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 4
|
||||
path: tests/Feature/Console/CheckSupplierWebhookSecretCommandTest.php
|
||||
|
||||
-
|
||||
message: '#^Trait Tests\\Concerns\\SharesSupplierPdo is used zero times and is not analysed\.$#'
|
||||
identifier: trait.unused
|
||||
count: 1
|
||||
path: tests/Concerns/SharesSupplierPdo.php
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Concerns;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Share PDO between pgsql и pgsql_supplier connections в тестах.
|
||||
*
|
||||
* Зачем: Laravel DatabaseTransactions trait оборачивает каждый connection в
|
||||
* свою транзакцию. Записи через default Project::factory() ($pgsql) не видны
|
||||
* с pgsql_supplier connection до commit'а. Sharing PDO означает — обе
|
||||
* connection'ы используют ту же PDO session → одну транзакцию.
|
||||
*
|
||||
* На production обе connection'ы реальные separate PDO, и видимость
|
||||
* обеспечивается READ COMMITTED после commit'а webhook-handler'а перед
|
||||
* dispatch'ом supplier-flow job'а. Этот trait — только для test-окружения.
|
||||
*
|
||||
* Использование: `uses(SharesSupplierPdo::class)` в Pest test-файле, где
|
||||
* нужна cross-connection visibility (например, tests/Feature/Supplier/*).
|
||||
*/
|
||||
trait SharesSupplierPdo
|
||||
{
|
||||
protected function setUpSharesSupplierPdo(): void
|
||||
{
|
||||
if (! config()->has('database.connections.pgsql_supplier')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultConnection = DB::connection('pgsql');
|
||||
DB::connection('pgsql_supplier')->setPdo($defaultConnection->getPdo());
|
||||
DB::connection('pgsql_supplier')->setReadPdo($defaultConnection->getReadPdo());
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ use App\Models\Project;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function () {
|
||||
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
||||
|
||||
@@ -10,8 +10,10 @@ use App\Models\SystemSetting;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
SystemSetting::query()->where('key', 'supplier_webhook_secret')
|
||||
|
||||
@@ -16,8 +16,10 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Mockery as M;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
||||
|
||||
@@ -8,8 +8,10 @@ use App\Models\Tenant;
|
||||
use App\Services\LeadRouter;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
// Clear tenant context — LeadRouter operates without it (sharing across tenants).
|
||||
|
||||
@@ -27,8 +27,10 @@ use App\Models\Tenant;
|
||||
use App\Services\LeadRouter;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
// Симулируем условия supplier-flow на queue worker'е: tenant ещё не определён.
|
||||
@@ -113,6 +115,7 @@ test("ResetDeliveredTodayCommand сбрасывает delivered_today по вс
|
||||
$projectIds[] = $project->id;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line method.notFound (Pest TestCall->artisan() mixin)
|
||||
$this->artisan('projects:reset-delivered-today')->assertExitCode(0);
|
||||
|
||||
$remaining = DB::connection('pgsql_supplier')
|
||||
|
||||
+1
-25
@@ -3,32 +3,8 @@
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Plan 3 Task 3: share PDO between `pgsql` and `pgsql_supplier` connections в тестах,
|
||||
* чтобы DatabaseTransactions corretly rollback'ил данные, созданные через дефолтный
|
||||
* connection, но запрошенные через supplier. Без этого Project::on('pgsql_supplier')
|
||||
* не видит свежесозданные через Project::factory() записи — две PDO-сессии = две
|
||||
* разные транзакции, supplier-side не видит uncommitted INSERTs default-side.
|
||||
*
|
||||
* На production обе роли (crm_app_user + crm_supplier_worker) — две настоящие
|
||||
* сессии, общая видимость через commit. В тестах достаточно одной разделяемой PDO.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// После того как Laravel инициализировал dafault connection (pgsql),
|
||||
// ре-используем его PDO для pgsql_supplier. Делаем только если оба
|
||||
// connection'а есть в конфиге (продакшен может не иметь supplier).
|
||||
if (config()->has('database.connections.pgsql_supplier')) {
|
||||
$defaultPdo = DB::connection('pgsql')->getPdo();
|
||||
$defaultReadPdo = DB::connection('pgsql')->getReadPdo();
|
||||
DB::connection('pgsql_supplier')->setPdo($defaultPdo);
|
||||
DB::connection('pgsql_supplier')->setReadPdo($defaultReadPdo);
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user