true]); Queue::fake(); // рассылку никуда не отправляем — проверяем только приём заказа $this->tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); /** * Заказ рассылки. Порядок слагаемых важен: у оператора `+` побеждает ЛЕВАЯ часть, * поэтому переданное вызывающим идёт первым, иначе подмена поля молча не сработает. * * @return array */ function idemPayload(array $extra = []): array { return $extra + [ 'title' => 'Акция', 'source' => 'manual', 'phones' => ['79990000001'], 'body' => 'Здравствуйте!', ]; } it('два запроса с одним ключом заказа создают ОДНУ рассылку', function () { $key = (string) Str::uuid(); $first = $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => $key])) ->assertCreated(); $second = $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => $key])) ->assertOk(); // 200, а не 201 — ничего нового не создано expect($second->json('id'))->toBe($first->json('id')) ->and(ClientSmsCampaign::where('tenant_id', $this->tenant->id)->count())->toBe(1); }); it('повтор с тем же ключом не замораживает деньги второй раз', function () { config(['services.sms.sandbox' => false]); app(AdWalletService::class)->topup($this->tenant->id, '1000.00', null, 'test'); // В реальном режиме нужен и номер с известным оператором, и сам маршрут: у // тестового окружения провайдера МТС нет, получателей вышло бы ноль, и тест // проверял бы «0 == 0», то есть ничего. $this->app->instance(ClientSmsRecipientSelector::class, new ClientSmsRecipientSelector( new SmsRouter([new MtsSmsProvider('x', ['mts'], ['*' => 0])]), new OperatorNormalizer, new AllowedSmsOperators(new OperatorNormalizer), )); ClientSmsContact::create([ 'tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'name' => 'А', 'operator' => 'МТС', ]); $key = (string) Str::uuid(); $this->postJson('/api/sms/campaigns', idemPayload(['source' => 'base', 'idempotency_key' => $key])) ->assertCreated(); $frozenAfterFirst = (string) AdWallet::where('tenant_id', $this->tenant->id)->first()->frozen_rub; $this->postJson('/api/sms/campaigns', idemPayload(['source' => 'base', 'idempotency_key' => $key])) ->assertOk(); $frozenAfterSecond = (string) AdWallet::where('tenant_id', $this->tenant->id)->first()->frozen_rub; expect($frozenAfterSecond)->toBe($frozenAfterFirst) ->and($frozenAfterFirst)->not->toBe('0.00'); // заморозка вообще была — иначе тест пустой }); it('такая же рассылка за 10 минут спрашивает подтверждение, а с ответом «да» уходит', function () { $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => (string) Str::uuid()])) ->assertCreated(); $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => (string) Str::uuid()])) ->assertStatus(409) ->assertJsonPath('code', 'duplicate_recent'); $this->postJson('/api/sms/campaigns', idemPayload([ 'idempotency_key' => (string) Str::uuid(), 'confirmed' => true, ]))->assertCreated(); expect(ClientSmsCampaign::where('tenant_id', $this->tenant->id)->count())->toBe(2); }); it('другой текст за те же 10 минут вопросов не вызывает', function () { $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => (string) Str::uuid()])) ->assertCreated(); $this->postJson('/api/sms/campaigns', idemPayload([ 'body' => 'Совсем другое сообщение', 'idempotency_key' => (string) Str::uuid(), ]))->assertCreated(); }); it('ключ заказа чужого клиента на нашу рассылку не влияет', function () { $key = (string) Str::uuid(); $this->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => $key]))->assertCreated(); // Другой клиент с тем же ключом — своя рассылка, а не чужая. $stranger = Tenant::factory()->create(); $strangerUser = User::factory()->create(['tenant_id' => $stranger->id]); $res = $this->actingAs($strangerUser) ->postJson('/api/sms/campaigns', idemPayload(['idempotency_key' => $key])) ->assertCreated(); expect(ClientSmsCampaign::find($res->json('id'))->tenant_id)->toBe($stranger->id) ->and(ClientSmsCampaign::count())->toBe(2); });