86d974330b
Этап 3 «Время и цена», Task 1. Закрыты строки листа Н.3 и 3.7. Поведение рассылки ещё НЕ меняется — этим займётся Task 2. Сейчас заведено то, на чём оно будет стоять, и заведено так, чтобы правило нельзя было размножить. 1. Справочник часовых поясов (RegionTimezoneMap). 89 субъектов РФ в том же порядке, что и справочник имён; сторож-тест сверяет составы, чтобы справочники не разъехались. Отдельный тест на ловушку: код субъекта у нас НЕ автомобильный — 77 это Тюменская область, а Москва 82. Неизвестный код и непонятная строка от ДаДаты дают «не знаю», а не ноль: ноль означал бы Гринвич, то есть тихую подмену Камчатки Лондоном. 2. Единственный дом правила 10–20 (SmsQuietHours): можно ли отдавать сейчас, когда откроется окно, осмысленно ли такое окно. Границы читаются из настроек один раз на объект — на 20 000 номеров иначе был бы 20 000-й запрос к базе. 3. Границы окна в общих настройках: миграция добавляет две колонки со значениями 10 и 20. Защита от повторного запуска пошаговая — прерванная ручная подача SQL на бою не должна оставить вторую колонку несозданной (урок В-80). Прав не требует: колонки наследуют привилегии таблицы. Запись схемы v9.12. 4. Админка «СМС»: два поля «Отправляем с / по» и объяснение, что часы — по местному времени получателя и клиент их не настраивает. Окно наизнанку «с 20 до 10» это отправка всю ночь, поэтому сервер его не принимает и говорит человеку почему. Проверяется ПОЛУЧИВШЕЕСЯ окно, а не присланные поля: правка одной границы тоже могла его вывернуть — журнал В-91. Прогоны: СМС 186/186 (было 171, 15 новых тестов), приём лидов 17/17, фронт 1658 зелёных и 3 намеренно пропущенных, phpstan по своим файлам 0, pint чисто, проверка типов без новых ошибок. Вырезанием проверено дважды: убрал проверку окна — покраснели два теста; убрал чтение границ с сервера на экране — покраснел фронтовый тест. Живьём: окно 11–19 сохранилось и пережило перезагрузку, «с 20 до 10» отклонено с человеческим текстом, вернул 10–20. Попутный урок В-92: два моих же новых теста сперва зеленели по неверной причине — ругань приходила за пропущенные поля платы за имя, а не за окно. Теперь тесты шлют полное письмо и проверяют, за какое поле ругаются.
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
|
||
vi.mock('../../resources/js/api/client', () => ({
|
||
apiClient: {
|
||
get: vi.fn(),
|
||
post: vi.fn(),
|
||
put: vi.fn(),
|
||
patch: vi.fn(),
|
||
delete: vi.fn(),
|
||
},
|
||
ensureCsrfCookie: vi.fn(),
|
||
}));
|
||
|
||
import {
|
||
fetchSmsTariffs,
|
||
updateSmsTariffs,
|
||
updateSmsSettings,
|
||
fetchSmsSenders,
|
||
approveSmsSender,
|
||
rejectSmsSender,
|
||
disableSmsSender,
|
||
} from '../../resources/js/api/admin';
|
||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||
|
||
describe('api/admin — СМС: тарифы и имена (Task 16b)', () => {
|
||
beforeEach(() => vi.clearAllMocks());
|
||
|
||
it('fetchSmsTariffs() GETs /api/admin/sms/tariffs', async () => {
|
||
const payload = {
|
||
tariffs: [{ id: 1, min_qty: 1, price_rub: '3.50' }],
|
||
settings: { name_fee_rub_per_operator: '500.00', name_debt_grace_days: 5 },
|
||
};
|
||
vi.mocked(apiClient.get).mockResolvedValue({ data: payload });
|
||
|
||
const r = await fetchSmsTariffs();
|
||
|
||
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/sms/tariffs');
|
||
expect(r).toEqual(payload);
|
||
});
|
||
|
||
it('updateSmsTariffs(rows) ensureCsrfCookie ПЕРЕД PUT /api/admin/sms/tariffs с {rows}', async () => {
|
||
const rows = [
|
||
{ min_qty: 1, price_rub: '3.50' },
|
||
{ min_qty: 1000, price_rub: '3.00' },
|
||
];
|
||
vi.mocked(apiClient.put).mockResolvedValue({
|
||
data: { tariffs: [{ id: 1, min_qty: 1, price_rub: '3.50' }] },
|
||
});
|
||
|
||
const r = await updateSmsTariffs(rows);
|
||
|
||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||
expect(apiClient.put).toHaveBeenCalledWith('/api/admin/sms/tariffs', { rows });
|
||
const csrfOrder = vi.mocked(ensureCsrfCookie).mock.invocationCallOrder[0];
|
||
const putOrder = vi.mocked(apiClient.put).mock.invocationCallOrder[0];
|
||
expect(csrfOrder).toBeLessThan(putOrder);
|
||
expect(r.tariffs).toHaveLength(1);
|
||
});
|
||
|
||
it('updateSmsSettings(payload) PUTs /api/admin/sms/settings с телом, включая границы окна', async () => {
|
||
vi.mocked(apiClient.put).mockResolvedValue({
|
||
data: {
|
||
name_fee_rub_per_operator: '600.00',
|
||
name_debt_grace_days: 7,
|
||
quiet_from_hour: 11,
|
||
quiet_to_hour: 19,
|
||
},
|
||
});
|
||
|
||
const r = await updateSmsSettings({
|
||
name_fee_rub_per_operator: '600.00',
|
||
name_debt_grace_days: 7,
|
||
quiet_from_hour: 11,
|
||
quiet_to_hour: 19,
|
||
});
|
||
|
||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||
expect(apiClient.put).toHaveBeenCalledWith('/api/admin/sms/settings', {
|
||
name_fee_rub_per_operator: '600.00',
|
||
name_debt_grace_days: 7,
|
||
quiet_from_hour: 11,
|
||
quiet_to_hour: 19,
|
||
});
|
||
expect(r.name_debt_grace_days).toBe(7);
|
||
expect(r.quiet_from_hour).toBe(11);
|
||
});
|
||
|
||
it('fetchSmsSenders(status) GETs /api/admin/sms/senders с params.status', async () => {
|
||
const senders = [
|
||
{
|
||
id: 1,
|
||
tenant_id: 5,
|
||
name: 'ОКНА МСК',
|
||
name_type: 'company',
|
||
status: 'pending',
|
||
monthly_fee_rub: '500.00',
|
||
note: null,
|
||
},
|
||
];
|
||
vi.mocked(apiClient.get).mockResolvedValue({ data: { senders } });
|
||
|
||
const r = await fetchSmsSenders('pending');
|
||
|
||
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/sms/senders', { params: { status: 'pending' } });
|
||
expect(r).toEqual(senders);
|
||
});
|
||
|
||
it('fetchSmsSenders() без статуса — params пустой', async () => {
|
||
vi.mocked(apiClient.get).mockResolvedValue({ data: { senders: [] } });
|
||
|
||
await fetchSmsSenders();
|
||
|
||
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/sms/senders', { params: {} });
|
||
});
|
||
|
||
it('approveSmsSender(id) POSTs /api/admin/sms/senders/{id}/approve', async () => {
|
||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, status: 'active' } });
|
||
|
||
const r = await approveSmsSender(1);
|
||
|
||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/sms/senders/1/approve');
|
||
expect(r.status).toBe('active');
|
||
});
|
||
|
||
it('rejectSmsSender(id, note) POSTs /api/admin/sms/senders/{id}/reject с note', async () => {
|
||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, status: 'rejected' } });
|
||
|
||
const r = await rejectSmsSender(1, 'Похоже на чужой бренд');
|
||
|
||
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/sms/senders/1/reject', {
|
||
note: 'Похоже на чужой бренд',
|
||
});
|
||
expect(r.status).toBe('rejected');
|
||
});
|
||
|
||
it('rejectSmsSender(id) без note шлёт пустое тело', async () => {
|
||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, status: 'rejected' } });
|
||
|
||
await rejectSmsSender(1);
|
||
|
||
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/sms/senders/1/reject', {});
|
||
});
|
||
|
||
it('disableSmsSender(id) POSTs /api/admin/sms/senders/{id}/disable', async () => {
|
||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, status: 'suspended' } });
|
||
|
||
const r = await disableSmsSender(1);
|
||
|
||
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/sms/senders/1/disable');
|
||
expect(r.status).toBe('suspended');
|
||
});
|
||
});
|