Files
portal/app/tests/Feature/ClientSms/EndpointTest.php
T

194 lines
8.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\SendClientSmsCampaignJob;
use App\Models\AdWallet;
use App\Models\ClientSmsCampaign;
use App\Models\ClientSmsContact;
use App\Models\ClientSmsTemplate;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Advertising\AdWalletService;
use App\Services\ClientSms\ClientSmsRecipientSelector;
use App\Services\Sms\OperatorNormalizer;
use App\Services\Sms\Providers\MtsSmsProvider;
use App\Services\Sms\SmsRouter;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
/**
* HTTP API клиентской СМС-рассылки (Task 7) — тонкий слой над готовыми сервисами
* ClientSmsAudienceBuilder/ClientSmsRecipientSelector/ClientSmsPricing и джобом
* SendClientSmsCampaignJob. Auth/tenant setup зеркалит AdvertisingCampaignEndpointTest:
* Tenant::factory + User::factory(tenant_id) + actingAs; middleware ['auth:sanctum','tenant'].
*
* Синтетические номера 7999… — реальные НИКОГДА.
*/
uses(RefreshDatabase::class);
beforeEach(function () {
$this->tenant = Tenant::factory()->create();
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
$this->actingAs($this->user);
});
/** Селектор с настоящим МТС-роутером — маршрутизирует mts даже в реальном режиме. */
function realMtsSelectorInstance(): ClientSmsRecipientSelector
{
return new ClientSmsRecipientSelector(
new SmsRouter([new MtsSmsProvider('x', ['mts'], ['*' => 0])]),
new OperatorNormalizer,
);
}
it('preview (source=base) считает смету и НЕ создаёт кампанию', function () {
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'name' => 'А', 'operator' => 'mts']);
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000002', 'name' => 'Б', 'operator' => 'mts']);
$response = $this->postJson('/api/sms/preview', [
'body' => 'Здравствуйте!',
'source' => 'base',
]);
$response->assertOk()
->assertJsonPath('segments', 1)
->assertJsonPath('sendable_count', 2);
expect($response->json('estimated_cost_rub'))->toBeString()
->and($response->json('price_rub_per_sms'))->toBeString();
expect(ClientSmsCampaign::count())->toBe(0);
});
it('store (sandbox) создаёт кампанию queued и ставит джоб в очередь; заморозки нет', function () {
config(['services.sms.sandbox' => true]);
Queue::fake();
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'name' => 'А', 'operator' => 'mts']);
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000002', 'name' => 'Б', 'operator' => 'mts']);
$response = $this->postJson('/api/sms/campaigns', [
'title' => 'Рассылка А',
'body' => 'Здравствуйте!',
'source' => 'base',
]);
$response->assertCreated()
->assertJsonPath('status', ClientSmsCampaign::STATUS_QUEUED);
$id = $response->json('id');
$this->assertDatabaseHas('client_sms_campaigns', [
'id' => $id,
'tenant_id' => $this->tenant->id,
'status' => ClientSmsCampaign::STATUS_QUEUED,
]);
Queue::assertPushed(SendClientSmsCampaignJob::class);
// Sandbox: заморозки нет — кошелёк не создан.
expect(AdWallet::where('tenant_id', $this->tenant->id)->exists())->toBeFalse();
});
it('store (реальный режим, денег не хватает) → 409 и кампания откачена', function () {
config(['services.sms.sandbox' => false]);
Queue::fake();
// Роутер реального режима сам по себе mts не поднимет (нет токена) — подменяем
// селектор на реальный МТС, чтобы получатели прошли и смета была > 0.
$this->app->instance(ClientSmsRecipientSelector::class, realMtsSelectorInstance());
// Кошелёк есть, но денег мало (смета заведомо больше).
app(AdWalletService::class)->topup($this->tenant->id, '1.00', null, 'test');
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'name' => 'А', 'operator' => 'mts']);
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000002', 'name' => 'Б', 'operator' => 'mts']);
$response = $this->postJson('/api/sms/campaigns', [
'title' => 'Рассылка без денег',
'body' => 'Здравствуйте!',
'source' => 'base',
]);
$response->assertStatus(409);
// Кампания откачена — сироты нет.
expect(ClientSmsCampaign::where('tenant_id', $this->tenant->id)->count())->toBe(0);
Queue::assertNothingPushed();
});
it('изоляция тенантов: чужие кампании не видны и show чужой → 404', function () {
$mine = ClientSmsCampaign::create([
'tenant_id' => $this->tenant->id,
'title' => 'Моя', 'body' => 'x', 'sender_name' => 'liderra.ru',
'source' => 'base', 'status' => ClientSmsCampaign::STATUS_QUEUED,
'segments' => 1, 'planned_count' => 0, 'total_sms' => 0,
'price_rub_per_sms' => '9.00', 'estimated_cost_rub' => '0.00',
]);
$tenantB = Tenant::factory()->create();
$foreign = ClientSmsCampaign::create([
'tenant_id' => $tenantB->id,
'title' => 'Чужая', 'body' => 'x', 'sender_name' => 'liderra.ru',
'source' => 'base', 'status' => ClientSmsCampaign::STATUS_QUEUED,
'segments' => 1, 'planned_count' => 0, 'total_sms' => 0,
'price_rub_per_sms' => '9.00', 'estimated_cost_rub' => '0.00',
]);
$index = $this->getJson('/api/sms/campaigns');
$index->assertOk();
$ids = collect($index->json('campaigns'))->pluck('id')->all();
expect($ids)->toContain($mine->id)
->and($ids)->not->toContain($foreign->id);
$this->getJson("/api/sms/campaigns/{$foreign->id}")->assertStatus(404);
});
it('контакты: загрузка, список только своих, удаление, изоляция тенанта', function () {
$upload = $this->postJson('/api/sms/contacts', [
'phones' => ['79990000001', '89990000002', 'мусор'],
]);
$upload->assertOk();
// 2 валидных (мусор пропущен).
expect($upload->json('added'))->toBe(2);
$list = $this->getJson('/api/sms/contacts');
$list->assertOk();
expect($list->json())->toHaveCount(2);
// Чужой тенант не видит наши контакты.
$tenantB = Tenant::factory()->create();
$userB = User::factory()->create(['tenant_id' => $tenantB->id]);
$this->actingAs($userB);
expect($this->getJson('/api/sms/contacts')->json())->toHaveCount(0);
// Возвращаемся к своему и удаляем один.
$this->actingAs($this->user);
$contactId = ClientSmsContact::where('tenant_id', $this->tenant->id)->first()->id;
$this->deleteJson("/api/sms/contacts/{$contactId}")->assertOk();
expect($this->getJson('/api/sms/contacts')->json())->toHaveCount(1);
});
it('шаблоны: CRUD со скоупом тенанта', function () {
$create = $this->postJson('/api/sms/templates', ['title' => 'Привет', 'body' => 'Здравствуйте!']);
$create->assertCreated();
$tplId = $create->json('id');
$this->getJson('/api/sms/templates')->assertOk()->assertJsonCount(1);
$this->patchJson("/api/sms/templates/{$tplId}", ['title' => 'Обновлён'])
->assertOk()->assertJsonPath('title', 'Обновлён');
// Чужой тенант не видит/не трогает.
$tenantB = Tenant::factory()->create();
$userB = User::factory()->create(['tenant_id' => $tenantB->id]);
$this->actingAs($userB);
$this->getJson('/api/sms/templates')->assertOk()->assertJsonCount(0);
$this->patchJson("/api/sms/templates/{$tplId}", ['title' => 'Взлом'])->assertStatus(404);
$this->actingAs($this->user);
$this->deleteJson("/api/sms/templates/{$tplId}")->assertOk();
expect(ClientSmsTemplate::where('tenant_id', $this->tenant->id)->count())->toBe(0);
});