Files
portal/app/tests/Frontend/advertising-api.spec.ts
T
Дмитрий 5a4c0e0235 feat(реклама): показы — баннеры клиента по размерам, два режима аудитории, клиентская цена и наценка
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.

Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.

Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 21:13:20 +03:00

266 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../../resources/js/api/client', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
ensureCsrfCookie: vi.fn().mockResolvedValue(undefined),
}));
import {
fetchWallet,
fetchCampaigns,
fetchCampaign,
createCampaign,
patchCampaign,
fetchAudienceSize,
launchCampaign,
pauseCampaign,
resumeCampaign,
addCreative,
uploadCreativeImage,
deleteCampaign,
uploadCampaignPhones,
createAdvertisingInvoice,
topupAdvertisingByCard,
} from '../../resources/js/api/advertising';
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
describe('api/advertising', () => {
beforeEach(() => vi.clearAllMocks());
it('fetchWallet() GETs /api/advertising/wallet', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { solvent: true, balance_rub: '100.00', frozen_rub: '20.00', free_rub: '80.00' },
});
const w = await fetchWallet();
expect(apiClient.get).toHaveBeenCalledWith('/api/advertising/wallet');
expect(w.solvent).toBe(true);
expect(w.free_rub).toBe('80.00');
});
it('fetchCampaigns() hits GET /api/advertising/campaigns and unwraps data.data', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
data: [
{
id: 1,
name: 'C',
status: 'draft',
weekly_budget_rub: '500.00',
audience_days: 10,
launched_at: null,
},
],
},
});
const rows = await fetchCampaigns();
expect(apiClient.get).toHaveBeenCalledWith('/api/advertising/campaigns');
expect(rows).toHaveLength(1);
expect(rows[0].name).toBe('C');
});
it('fetchCampaigns() returns [] when data.data missing', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: {} });
const rows = await fetchCampaigns();
expect(rows).toEqual([]);
});
it('fetchCampaign(id) GETs /api/advertising/campaigns/{id} and returns campaign+ads+spent_rub', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
campaign: {
id: 3,
name: 'X',
status: 'running',
weekly_budget_rub: '1000.00',
audience_days: 20,
launched_at: '2026-07-01',
},
ads: [],
spent_rub: '10.00',
},
});
const detail = await fetchCampaign(3);
expect(apiClient.get).toHaveBeenCalledWith('/api/advertising/campaigns/3');
expect(detail.campaign.id).toBe(3);
expect(detail.spent_rub).toBe('10.00');
});
it('createCampaign() ensures csrf then POSTs /api/advertising/campaigns with payload', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
id: 5,
name: 'X',
status: 'draft',
weekly_budget_rub: '2500.00',
audience_days: 10,
launched_at: null,
},
});
const payload = { name: 'X', audience_days: 10, use_uploaded_list: false, weekly_budget_rub: '2500.00' };
const c = await createCampaign(payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns', payload);
expect(c.id).toBe(5);
});
it('patchCampaign() ensures csrf then PATCHes /api/advertising/campaigns/{id}', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({
data: {
id: 5,
name: 'X2',
status: 'draft',
weekly_budget_rub: '3000.00',
audience_days: 15,
launched_at: null,
},
});
const c = await patchCampaign(5, { weekly_budget_rub: '3000.00' });
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.patch).toHaveBeenCalledWith('/api/advertising/campaigns/5', { weekly_budget_rub: '3000.00' });
expect(c.name).toBe('X2');
});
it('fetchAudienceSize() passes days as query param when given', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: { size: 120, min: 100, enough: true, hint: null } });
const s = await fetchAudienceSize(3, { days: 14 });
expect(apiClient.get).toHaveBeenCalledWith('/api/advertising/campaigns/3/audience-size', {
params: { days: 14 },
});
expect(s.enough).toBe(true);
});
it('fetchAudienceSize() omits days param when not given', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: { size: 5, min: 100, enough: false, hint: 'мало' } });
const s = await fetchAudienceSize(3);
expect(apiClient.get).toHaveBeenCalledWith('/api/advertising/campaigns/3/audience-size', { params: {} });
expect(s.enough).toBe(false);
});
it('launchCampaign() ensures csrf then POSTs launch endpoint', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { status: 'pending_moderation' } });
const res = await launchCampaign(7);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/launch');
expect(res.status).toBe('pending_moderation');
});
it('pauseCampaign() ensures csrf then POSTs pause endpoint', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { status: 'paused' } });
const res = await pauseCampaign(7);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/pause');
expect(res.status).toBe('paused');
});
it('resumeCampaign() ensures csrf then POSTs resume endpoint', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { status: 'running' } });
const res = await resumeCampaign(7);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/resume');
expect(res.status).toBe('running');
});
it('addCreative() ensures csrf then POSTs ads endpoint with payload', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
id: 9,
campaign_id: 7,
title: 'T',
text: 'Txt',
href: 'https://x.ru',
title2: null,
moderation_status: 'draft',
},
});
const payload = { title: 'T', text: 'Txt', href: 'https://x.ru' };
const ad = await addCreative(7, payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/ads', payload);
expect(ad.id).toBe(9);
});
it('uploadCreativeImage() ensures csrf then POSTs multipart FormData to image endpoint', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { hash: 'abc123' } });
const file = new File(['x'], 'pic.png', { type: 'image/png' });
const res = await uploadCreativeImage(7, 9, file);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/ads/9/image', expect.any(FormData));
const sentForm = vi.mocked(apiClient.post).mock.calls[0][1] as FormData;
expect(sentForm.get('file')).toBe(file);
expect(res.hash).toBe('abc123');
});
it('deleteCampaign() ensures csrf then DELETEs /api/advertising/campaigns/{id}', async () => {
vi.mocked(apiClient.delete).mockResolvedValue({ data: null });
await deleteCampaign(12);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.delete).toHaveBeenCalledWith('/api/advertising/campaigns/12');
});
it('uploadCampaignPhones() ensures csrf then POSTs FormData to phones endpoint and returns {recognized,skipped}', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { recognized: 10, skipped: 2 } });
const file = new File(['79261234567'], 'phones.txt', { type: 'text/plain' });
const res = await uploadCampaignPhones(7, { file, text: '79261111111' });
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/advertising/campaigns/7/phones', expect.any(FormData));
const sentForm = vi.mocked(apiClient.post).mock.calls[0][1] as FormData;
expect(sentForm.get('file')).toBe(file);
expect(sentForm.get('text')).toBe('79261111111');
expect(res).toEqual({ recognized: 10, skipped: 2 });
});
it('uploadCampaignPhones() omits file/text from FormData when not given', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { recognized: 3, skipped: 0 } });
await uploadCampaignPhones(7, {});
const sentForm = vi.mocked(apiClient.post).mock.calls[0][1] as FormData;
expect(sentForm.get('file')).toBeNull();
expect(sentForm.get('text')).toBeNull();
});
it('createAdvertisingInvoice() ensures csrf then POSTs /api/billing/invoices с credit_target=advertising', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
invoice: {
id: 1,
invoice_number: 'СЧ-2026-0001',
amount_total: '5000.00',
pdf_url: '/api/billing/invoices/1/pdf',
},
},
});
const invoice = await createAdvertisingInvoice(5000);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/billing/invoices', {
amount_rub: 5000,
credit_target: 'advertising',
});
expect(invoice.invoice_number).toBe('СЧ-2026-0001');
expect(invoice.pdf_url).toBe('/api/billing/invoices/1/pdf');
});
it('topupAdvertisingByCard() ensures csrf then POSTs /api/billing/topup с credit_target=advertising (шлюз ВКЛ → confirmation_url)', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: { confirmation_url: 'https://yoomoney.ru/checkout/pay_x' },
});
const res = await topupAdvertisingByCard(5000);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/billing/topup', {
amount_rub: 5000,
credit_target: 'advertising',
});
expect(res.confirmation_url).toBe('https://yoomoney.ru/checkout/pay_x');
});
it('topupAdvertisingByCard() returns {ok:true} when the gateway is off (stub instant credit)', async () => {
vi.mocked(apiClient.post).mockResolvedValue({ data: { ok: true } });
const res = await topupAdvertisingByCard(3000);
expect(res.ok).toBe(true);
expect(res.confirmation_url).toBeUndefined();
});
});