dadfdcaa7e
Месячный cron-сброс tenants.delivered_in_month + projects.delivered_in_month 1-го числа каждого месяца в 00:00 МСК. Идёт через pgsql_supplier BYPASSRLS connection (паттерн ResetDeliveredTodayCommand). Идемпотентный (WHERE delivered_in_month <> 0 → повторный запуск 0 affected rows). 4 теста: reset multi-tenant + idempotency + Schedule registration + BYPASSRLS without SET LOCAL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
3.1 KiB
PHP
76 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Project;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Console\Scheduling\Schedule;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
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)");
|
|
});
|
|
|
|
it('resets tenants.delivered_in_month and projects.delivered_in_month to 0', function () {
|
|
$tenantA = Tenant::factory()->create(['delivered_in_month' => 100]);
|
|
$tenantB = Tenant::factory()->create(['delivered_in_month' => 5]);
|
|
$tenantC = Tenant::factory()->create(['delivered_in_month' => 0]);
|
|
|
|
Project::factory()->create(['tenant_id' => $tenantA->id, 'delivered_in_month' => 50]);
|
|
Project::factory()->create(['tenant_id' => $tenantA->id, 'delivered_in_month' => 50]);
|
|
Project::factory()->create(['tenant_id' => $tenantB->id, 'delivered_in_month' => 5]);
|
|
|
|
Artisan::call('projects:reset-monthly');
|
|
|
|
expect($tenantA->fresh()->delivered_in_month)->toBe(0);
|
|
expect($tenantB->fresh()->delivered_in_month)->toBe(0);
|
|
expect($tenantC->fresh()->delivered_in_month)->toBe(0);
|
|
|
|
expect(Project::sum('delivered_in_month'))->toBe(0);
|
|
});
|
|
|
|
it('is idempotent — second run reports 0 affected', function () {
|
|
$tenant = Tenant::factory()->create(['delivered_in_month' => 10]);
|
|
Project::factory()->create(['tenant_id' => $tenant->id, 'delivered_in_month' => 10]);
|
|
|
|
// Первый прогон сбрасывает оба счётчика.
|
|
Artisan::call('projects:reset-monthly');
|
|
|
|
// Второй прогон должен сообщить о 0 затронутых строках (WHERE delivered_in_month <> 0).
|
|
// info() пишет ОДНУ строку «Monthly reset: 0 tenants, 0 projects.» —
|
|
// expectsOutputToContain в Laravel 13 line-by-line, поэтому проверяем
|
|
// обе подстроки одним substring'ом.
|
|
$this->artisan('projects:reset-monthly')
|
|
->expectsOutputToContain('0 tenants, 0 projects')
|
|
->assertExitCode(0);
|
|
});
|
|
|
|
it('Schedule entry registered for monthly on 1st 00:00 Europe/Moscow', function () {
|
|
/** @var Schedule $schedule */
|
|
$schedule = app(Schedule::class);
|
|
|
|
$found = collect($schedule->events())->first(
|
|
fn ($event) => str_contains($event->command ?? '', 'projects:reset-monthly')
|
|
);
|
|
|
|
expect($found)->not->toBeNull();
|
|
expect($found->expression)->toBe('0 0 1 * *');
|
|
expect($found->timezone)->toBe('Europe/Moscow');
|
|
});
|
|
|
|
it('uses pgsql_supplier BYPASSRLS connection (touches all tenants without SET LOCAL)', function () {
|
|
Tenant::factory()->count(3)->create(['delivered_in_month' => 7]);
|
|
|
|
// Без SET LOCAL app.current_tenant_id reset должен затронуть всех 3 tenant'ов.
|
|
// Это тест на использование pgsql_supplier (BYPASSRLS), не default pgsql.
|
|
Artisan::call('projects:reset-monthly');
|
|
|
|
expect(Tenant::where('delivered_in_month', '>', 0)->count())->toBe(0);
|
|
});
|