$tenantId, 'title' => 'Страницы', 'body' => 'Текст рассылки', 'sender_name' => 'LIDERRA', 'source' => ClientSmsCampaign::SOURCE_MANUAL, 'status' => ClientSmsCampaign::STATUS_DONE, 'idempotency_key' => 'pages-'.uniqid(), 'segments' => 1, 'planned_count' => $count, 'sent_count' => $count, 'total_sms' => $count, 'price_rub_per_sms' => '8.50', 'estimated_cost_rub' => '8.50', ]); for ($n = 1; $n <= $count; $n++) { ClientSmsMessage::create([ 'tenant_id' => $tenantId, 'campaign_id' => $campaign->id, 'phone' => '7999'.str_pad((string) $campaign->id, 4, '0', STR_PAD_LEFT).str_pad((string) $n, 3, '0', STR_PAD_LEFT), 'operator' => 'mts', 'provider_key' => 'mts', 'status' => ClientSmsMessage::STATUS_SENT, 'delivery_status' => $states[($n - 1) % count($states)], 'cost_rub' => '8.50', 'segments' => 1, ]); } return $campaign; } function pagesUser(): User { $tenant = Tenant::factory()->create(); return User::factory()->create(['tenant_id' => $tenant->id]); } it('отдаёт первую страницу по 50 строк и называет, сколько всего', function () { $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}"); $response->assertOk(); expect($response->json('messages'))->toHaveCount(50) ->and($response->json('messages_page.total'))->toBe(120) ->and($response->json('messages_page.page'))->toBe(1) ->and($response->json('messages_page.per_page'))->toBe(50) ->and($response->json('messages_page.last_page'))->toBe(3); }); it('вторая страница отдаёт ДРУГИЕ строки, а не те же самые', function () { // 🔴 Главный тест этой задачи. Живой прогон 30.07 уже ловил вторую страницу, // неотличимую от первой (В-167): `paginate()` брал номер страницы из глобального // запроса, а не из этого. Сверяем именно НОМЕРА строк, а не факт ответа. $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120); $first = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}?page=1"); $second = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}?page=2"); $firstIds = array_column($first->json('messages'), 'id'); $secondIds = array_column($second->json('messages'), 'id'); expect($secondIds)->toHaveCount(50) ->and($second->json('messages_page.page'))->toBe(2) ->and(array_intersect($firstIds, $secondIds))->toBe([]); }); it('последняя страница отдаёт остаток', function () { $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}?page=3"); expect($response->json('messages'))->toHaveCount(20) ->and($response->json('messages_page.page'))->toBe(3); }); it('размер страницы не задерёшь адресом — потолок 200', function () { // Иначе страницы можно обойти одним параметром `?per_page=100000` и вернуться // ровно туда, откуда ушли (В-166). $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}?per_page=100000"); expect($response->json('messages'))->toHaveCount(120) ->and($response->json('messages_page.per_page'))->toBe(200); }); it('кривой размер страницы молча заменяется обычным', function () { $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 60); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}?per_page=0"); // Ругаться не за что: `per_page` набирает не человек, а экран. expect($response->json('messages'))->toHaveCount(50) ->and($response->json('messages_page.per_page'))->toBe(50); }); it('итог по судьбам считается по ВСЕЙ рассылке, а не по видимой странице', function () { // 🔴 Иначе, листнув страницу, человек увидел бы другой итог и не понял бы, // какому верить. Первая страница — сплошь «доставлено», а в рассылке есть и другие. $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120, [ SmsDeliveryState::DELIVERED, SmsDeliveryState::NOT_DELIVERED, ]); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}"); expect($response->json('messages'))->toHaveCount(50) ->and($response->json('delivery.delivered'))->toBe(60) ->and($response->json('delivery.not_delivered'))->toBe(60); }); it('предложение досыла считается по ВСЕЙ рассылке, а не по видимой странице', function () { // Досыл — деньги. Считать «не дошедших» по видимой странице значило бы называть // человеку заниженное число и брать за досыл не ту сумму. $user = pagesUser(); $campaign = pagesCampaign((int) $user->tenant_id, 120, [ SmsDeliveryState::DELIVERED, SmsDeliveryState::NOT_DELIVERED, ]); $response = $this->actingAs($user)->getJson("/api/sms/campaigns/{$campaign->id}"); expect($response->json('resend.can'))->toBeTrue() ->and($response->json('resend.count'))->toBe(60); }); it('чужую рассылку страницами не открыть', function () { $mine = pagesUser(); $stranger = pagesUser(); $campaign = pagesCampaign((int) $stranger->tenant_id, 60); $this->actingAs($mine)->getJson("/api/sms/campaigns/{$campaign->id}?page=2")->assertNotFound(); });