Files
portal/app/tests/Frontend/telegram-api.spec.ts
T
Дмитрий 0c9642f3d0 feat(телеграм-модуль): клиентский экран + ручной запуск в песочнице — API, telegram.ts, экран, сквозной сценарий
Сессия 3 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Клиент сам собирает объявление + аудиторию и жмёт «Запустить»; робот кабинета МТС
(в тестах замокан) доводит до черновика. Всё в песочнице: деньги/живой запуск выключены.

- Клиентский API (Api\ClientTg\CampaignController + маршруты /api/telegram/*):
  создание (store) и запуск (launch) РАЗДЕЛЕНЫ. store создаёт ЧЕРНОВИК со сметой и
  числом кандидатов (деньги/робот не трогаются); launch draft→queued, в бою бронирует
  лимит на объявление (budget_cap_rub) и ставит RunTelegramCampaignJob. Нехватка денег
  → 409 (кампания остаётся черновиком); повторный запуск не-черновика → 422; изоляция
  тенантов (чужой show → 404). Валидация текст/ссылка/аудитория/бюджет; deals без срока → 422.
- Фронт-API telegram.ts (зеркало client-sms.ts, уже): fetch/create/get/launch, cookie-сессия.
- Экран AdvertisingTelegramView.vue (/advertising/telegram): форма (объявление, ссылка,
  3 способа аудитории, лимит), кнопка «Запустить» (create→launch), баннер песочницы,
  список кампаний с человеческим ярлыком статуса, сообщение при нехватке денег.
- Канал «Реклама Телеграм» АКТИВИРОВАН: advertisingChannels.ts route, провод в AppSidebar
  и AppMoreDrawer (route → реальный экран, остальные каналы — прежняя заглушка), маршрут
  в router. Тесты каналов обновлены (телеграм больше не заглушка).
- Ручной сквозной сценарий (песочница): создать → запустить → джоб (робот замокан, draft)
  → draft_ready + matched виден в API.

Отличие от СМС-близнеца: отдельного эндпоинта «предпросчёт до создания» нет — смету и
кандидатов клиент видит в самом черновике (создание черновик денег не тратит).

Проверки: 60/60 Pest ClientTg зелёные (+10 новых), 1512 Vitest зелёные (0 регрессий),
phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений. Робот в тестах замокан
(кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 17:39:43 +03:00

80 lines
3.0 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../../resources/js/api/client', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
ensureCsrfCookie: vi.fn().mockResolvedValue(undefined),
}));
import {
fetchTelegram,
createTelegram,
fetchTelegramCampaign,
launchTelegram,
} from '../../resources/js/api/telegram';
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
describe('api/telegram', () => {
beforeEach(() => vi.clearAllMocks());
it('fetchTelegram() GETs /api/telegram/campaigns and unwraps', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { campaigns: [], sandbox: true, balance_rub: '100.00', frozen_rub: '0.00' },
});
const res = await fetchTelegram();
expect(apiClient.get).toHaveBeenCalledWith('/api/telegram/campaigns');
expect(res.sandbox).toBe(true);
expect(res.balance_rub).toBe('100.00');
expect(res.campaigns).toEqual([]);
});
it('createTelegram() ensures csrf then POSTs /api/telegram/campaigns with payload', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
id: 1,
status: 'draft',
ad_text: 'Приходите',
ad_link: 'https://t.me/x',
audience_kind: 'list',
planned_count: 2,
estimated_cost_rub: '10.00',
},
});
const payload = {
ad_text: 'Приходите',
ad_link: 'https://t.me/x',
audience_kind: 'list' as const,
budget_cap_rub: '1000.00',
phones: ['79990000001', '79990000002'],
};
const c = await createTelegram(payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/telegram/campaigns', payload);
expect(c.id).toBe(1);
expect(c.status).toBe('draft');
expect(c.planned_count).toBe(2);
});
it('fetchTelegramCampaign(id) GETs /api/telegram/campaigns/{id} and returns {campaign}', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { campaign: { id: 3, status: 'draft_ready', ad_text: 'Y', ad_link: 'https://t.me/y' } },
});
const res = await fetchTelegramCampaign(3);
expect(apiClient.get).toHaveBeenCalledWith('/api/telegram/campaigns/3');
expect(res.campaign.id).toBe(3);
expect(res.campaign.status).toBe('draft_ready');
});
it('launchTelegram(id) ensures csrf then POSTs /api/telegram/campaigns/{id}/launch', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: { id: 5, status: 'queued', ad_text: 'Z', ad_link: 'https://t.me/z' },
});
const res = await launchTelegram(5);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/telegram/campaigns/5/launch');
expect(res.status).toBe('queued');
});
});