2026-07-24 23:36:26 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Jobs\SyncCampaignAudienceJob;
|
|
|
|
|
|
use App\Models\AdCampaign;
|
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
2026-07-25 00:07:45 +03:00
|
|
|
|
use Tests\Concerns\SharesSupplierPdo;
|
2026-07-24 23:36:26 +03:00
|
|
|
|
|
|
|
|
|
|
// Джоб намеренно обходит running/pending-кампании ВСЕХ тенантов — без изоляции
|
|
|
|
|
|
// между кейсами кампания из одного теста осталась бы видна следующему (проверка
|
|
|
|
|
|
// "пустая аудитория ⇒ ни одного обращения" иначе ловила бы чужой сегмент).
|
|
|
|
|
|
uses(RefreshDatabase::class);
|
2026-07-25 00:07:45 +03:00
|
|
|
|
// Джоб перечисляет кампании через pgsql_supplier (BYPASSRLS) — без share PDO
|
|
|
|
|
|
// вставленные в тест-транзакции кампании (default pgsql-соединение) не видны
|
|
|
|
|
|
// второму соединению до commit'а.
|
|
|
|
|
|
uses(SharesSupplierPdo::class);
|
2026-07-24 23:36:26 +03:00
|
|
|
|
|
|
|
|
|
|
it('replace-заливает аудиторию активной кампании в Яндекс.Аудитории', function () {
|
|
|
|
|
|
config([
|
|
|
|
|
|
'services.yandex_direct.enabled' => true,
|
|
|
|
|
|
'services.yandex_audience.token' => 'AUDTOKEN',
|
|
|
|
|
|
]);
|
|
|
|
|
|
Http::fake(['*/modify_data*' => Http::response(['ok' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
2026-07-26 21:13:20 +03:00
|
|
|
|
// T3: свой список считается только в mode=manual (auto его игнорирует).
|
2026-07-24 23:36:26 +03:00
|
|
|
|
$campaign = AdCampaign::create([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'name' => 'Кампания ночной заливки',
|
|
|
|
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
|
|
|
|
'yandex_segment_id' => 900001,
|
|
|
|
|
|
'weekly_budget_rub' => '500.00',
|
2026-07-26 21:13:20 +03:00
|
|
|
|
'mode' => AdCampaign::MODE_MANUAL,
|
2026-07-24 23:36:26 +03:00
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
|
'use_uploaded_list' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
DB::table('ad_campaign_phones')->insert([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'campaign_id' => $campaign->id,
|
|
|
|
|
|
'phone' => '79990000001',
|
|
|
|
|
|
'expires_at' => now()->addDays(5),
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
app(SyncCampaignAudienceJob::class)->handle();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(function ($request) {
|
|
|
|
|
|
return str_contains($request->url(), 'modify_data')
|
|
|
|
|
|
&& str_contains($request->url(), 'replace');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рубильник выключен — ни одного обращения к Яндексу', function () {
|
|
|
|
|
|
config([
|
|
|
|
|
|
'services.yandex_direct.enabled' => false,
|
|
|
|
|
|
'services.yandex_audience.token' => 'AUDTOKEN',
|
|
|
|
|
|
]);
|
|
|
|
|
|
Http::fake(['*/modify_data*' => Http::response(['ok' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
$campaign = AdCampaign::create([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'name' => 'Кампания при выключенном рубильнике',
|
|
|
|
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
|
|
|
|
'yandex_segment_id' => 900002,
|
|
|
|
|
|
'weekly_budget_rub' => '500.00',
|
|
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
|
'use_uploaded_list' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
DB::table('ad_campaign_phones')->insert([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'campaign_id' => $campaign->id,
|
|
|
|
|
|
'phone' => '79990000002',
|
|
|
|
|
|
'expires_at' => now()->addDays(5),
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
app(SyncCampaignAudienceJob::class)->handle();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('пустая аудитория кампании — пропускает без обращения к Яндексу', function () {
|
|
|
|
|
|
config([
|
|
|
|
|
|
'services.yandex_direct.enabled' => true,
|
|
|
|
|
|
'services.yandex_audience.token' => 'AUDTOKEN',
|
|
|
|
|
|
]);
|
|
|
|
|
|
Http::fake(['*/modify_data*' => Http::response(['ok' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
AdCampaign::create([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'name' => 'Кампания без аудитории',
|
|
|
|
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
|
|
|
|
'yandex_segment_id' => 900003,
|
|
|
|
|
|
'weekly_budget_rub' => '500.00',
|
|
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
|
'use_uploaded_list' => false,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
app(SyncCampaignAudienceJob::class)->handle();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('кампания без сегмента Яндекса — пропускает', function () {
|
|
|
|
|
|
config([
|
|
|
|
|
|
'services.yandex_direct.enabled' => true,
|
|
|
|
|
|
'services.yandex_audience.token' => 'AUDTOKEN',
|
|
|
|
|
|
]);
|
|
|
|
|
|
Http::fake(['*/modify_data*' => Http::response(['ok' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
$campaign = AdCampaign::create([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'name' => 'Кампания без сегмента',
|
|
|
|
|
|
'status' => AdCampaign::STATUS_RUNNING,
|
|
|
|
|
|
'weekly_budget_rub' => '500.00',
|
|
|
|
|
|
'audience_days' => 10,
|
|
|
|
|
|
'use_uploaded_list' => true,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
DB::table('ad_campaign_phones')->insert([
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'campaign_id' => $campaign->id,
|
|
|
|
|
|
'phone' => '79990000004',
|
|
|
|
|
|
'expires_at' => now()->addDays(5),
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
app(SyncCampaignAudienceJob::class)->handle();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
});
|