Files
portal/app/tests/Frontend/admin-api.spec.ts
T

357 lines
14 KiB
TypeScript
Raw Normal View History

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 {
impersonationInit,
impersonationVerify,
impersonationEnd,
impersonationActive,
impersonationRecent,
listAdminTenants,
getAdminTenantDetail,
listAdminBilling,
listAdminIncidents,
listSystemSettings,
updateSystemSetting,
} from '../../resources/js/api/admin';
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
const FAKE_TENANT = {
id: 1,
subdomain: 'demo',
organization_name: 'Demo Org',
contact_email: 'demo@x.ru',
status: 'active',
balance_rub: '1000.00',
balance_leads: 0,
is_trial: false,
last_activity_at: null,
tariff_id: 1,
tariff_name: 'basic',
mrr_rub: '5000.00',
desired_daily_numbers: 10,
chargeback_unrecovered_rub: '0.00',
created_at: '2026-05-01T00:00:00Z',
};
describe('api/admin', () => {
beforeEach(() => vi.clearAllMocks());
it('impersonationInit() POSTs /api/admin/impersonation/init + ensureCsrfCookie ПЕРЕД post', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
token_id: 42,
expires_at: '2026-05-14T13:00:00Z',
sent_to_email: 'admin@x.ru',
_dev_plain_code: '123456',
},
});
const payload = {
tenant_id: 1,
requested_by: 9,
reason: 'Investigating webhook delivery failure for tenant',
};
const r = await impersonationInit(payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/impersonation/init', payload);
// verify ensureCsrfCookie вызван ДО apiClient.post
const csrfOrder = vi.mocked(ensureCsrfCookie).mock.invocationCallOrder[0];
const postOrder = vi.mocked(apiClient.post).mock.invocationCallOrder[0];
expect(csrfOrder).toBeLessThan(postOrder);
expect(r.token_id).toBe(42);
expect(r._dev_plain_code).toBe('123456');
});
it('impersonationVerify() POSTs /api/admin/impersonation/verify + ensureCsrfCookie ПЕРЕД post', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
token_id: 42,
tenant_id: 1,
used_at: '2026-05-14T12:30:00Z',
message: 'verified',
},
});
const payload = { token_id: 42, code: '123456' };
const r = await impersonationVerify(payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/impersonation/verify', payload);
const csrfOrder = vi.mocked(ensureCsrfCookie).mock.invocationCallOrder[0];
const postOrder = vi.mocked(apiClient.post).mock.invocationCallOrder[0];
expect(csrfOrder).toBeLessThan(postOrder);
expect(r.message).toBe('verified');
});
it('impersonationEnd() POSTs /api/admin/impersonation/end с {token_id} + ensureCsrfCookie ПЕРЕД post', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
token_id: 42,
session_ended_at: '2026-05-14T13:00:00Z',
message: 'ended',
},
});
const r = await impersonationEnd(42);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/admin/impersonation/end', { token_id: 42 });
const csrfOrder = vi.mocked(ensureCsrfCookie).mock.invocationCallOrder[0];
const postOrder = vi.mocked(apiClient.post).mock.invocationCallOrder[0];
expect(csrfOrder).toBeLessThan(postOrder);
expect(r.token_id).toBe(42);
});
it('impersonationActive() GET /api/admin/impersonation/active + unwraps data.sessions', async () => {
const session = {
token_id: 1,
tenant_id: 1,
tenant_name: 'Demo',
requested_by: 9,
reason: 'r',
sent_to_email: 'a@x.ru',
used_at: '2026-05-14T12:00:00Z',
expires_at: '2026-05-14T13:00:00Z',
};
vi.mocked(apiClient.get).mockResolvedValue({ data: { sessions: [session] } });
const r = await impersonationActive();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/impersonation/active');
expect(r).toHaveLength(1);
expect(r[0].token_id).toBe(1);
});
it('impersonationRecent() GET /api/admin/impersonation/recent + unwraps data.sessions', async () => {
const session = {
token_id: 2,
tenant_id: 1,
tenant_name: 'Demo',
requested_by: 9,
reason: 'r',
used_at: '2026-05-14T10:00:00Z',
session_ended_at: '2026-05-14T11:00:00Z',
duration_seconds: 3600,
};
vi.mocked(apiClient.get).mockResolvedValue({ data: { sessions: [session] } });
const r = await impersonationRecent();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/impersonation/recent');
expect(r[0].duration_seconds).toBe(3600);
});
it('listAdminTenants() без params → default {}', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenants: [],
total: 0,
limit: 50,
offset: 0,
stats: { total: 0, active: 0, trial: 0, overdue: 0 },
},
});
await listAdminTenants();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/tenants', { params: {} });
});
it('listAdminTenants({status,search,limit,offset}) → передаёт params', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenants: [FAKE_TENANT],
total: 1,
limit: 25,
offset: 0,
stats: { total: 1, active: 1, trial: 0, overdue: 0 },
},
});
const params = { status: 'active', search: 'demo', limit: 25, offset: 0 };
const r = await listAdminTenants(params);
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/tenants', { params });
expect(r.tenants).toHaveLength(1);
expect(r.tenants[0].subdomain).toBe('demo');
});
it('getAdminTenantDetail() GET /api/admin/tenants/{subdomain} с обычным slug', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenant: FAKE_TENANT,
users: [],
projects: [],
balance_history: [],
activity: [],
metrics: {
leads_today: 0,
leads_this_week: 0,
leads_this_month: 0,
avg_lead_cost_rub: null,
runway_days: null,
},
},
});
const r = await getAdminTenantDetail('demo');
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/tenants/demo');
expect(r.tenant.id).toBe(1);
});
it('getAdminTenantDetail() применяет encodeURIComponent к slug со спецсимволами', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenant: FAKE_TENANT,
users: [],
projects: [],
balance_history: [],
activity: [],
metrics: {
leads_today: 0,
leads_this_week: 0,
leads_this_month: 0,
avg_lead_cost_rub: null,
runway_days: null,
},
},
});
await getAdminTenantDetail('tenant a&b');
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/tenants/tenant%20a%26b');
});
it('listAdminBilling() без аргумента → search=""', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenants: [],
summary: {
total_mrr_rub: '0.00',
monthly_revenue_rub: '0.00',
overdue_count: 0,
refunds_count_30d: 0,
},
},
});
await listAdminBilling();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/billing', { params: { search: '' } });
});
it('listAdminBilling("query") → передаёт search в params', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
tenants: [],
summary: {
total_mrr_rub: '100.00',
monthly_revenue_rub: '500.00',
overdue_count: 1,
refunds_count_30d: 0,
},
},
});
const r = await listAdminBilling('demo');
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/billing', { params: { search: 'demo' } });
expect(r.summary.overdue_count).toBe(1);
});
it('listAdminIncidents() без params → default {}', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
incidents: [],
total: 0,
limit: 50,
offset: 0,
summary: { open: 0, investigating: 0, rkn_pending: 0, total_unresolved: 0 },
},
});
await listAdminIncidents();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/incidents', { params: {} });
});
it('listAdminIncidents({type,severity,unresolved_only,limit,offset}) → передаёт params', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: {
incidents: [],
total: 0,
limit: 10,
offset: 0,
summary: { open: 0, investigating: 0, rkn_pending: 0, total_unresolved: 0 },
},
});
const params = { type: 'pdn_leak', severity: 'high', unresolved_only: true, limit: 10, offset: 0 };
await listAdminIncidents(params);
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/incidents', { params });
});
it('listSystemSettings() GET /api/admin/system-settings + unwraps data.settings', async () => {
const settings = [
{
key: 'lead.price_default',
value: '500',
type: 'decimal' as const,
description: 'default',
updated_at: '2026-05-14T00:00:00Z',
updated_by: 1,
},
];
vi.mocked(apiClient.get).mockResolvedValue({ data: { settings } });
const r = await listSystemSettings();
expect(apiClient.get).toHaveBeenCalledWith('/api/admin/system-settings');
expect(r).toHaveLength(1);
expect(r[0].key).toBe('lead.price_default');
});
it('updateSystemSetting() PUTs /api/admin/system-settings/{key} + encodeURIComponent + ensureCsrfCookie ПЕРЕД put', async () => {
vi.mocked(apiClient.put).mockResolvedValue({
data: {
key: 'lead.price_default',
value: '600',
previous_value: '500',
updated_at: '2026-05-14T12:00:00Z',
message: 'updated',
},
});
const payload = {
value: '600',
reason: 'Quarterly pricing adjustment for Q2 according to bizdev',
admin_user_id: 1,
};
const r = await updateSystemSetting('lead.price_default', payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.put).toHaveBeenCalledWith(
'/api/admin/system-settings/lead.price_default',
payload,
);
const csrfOrder = vi.mocked(ensureCsrfCookie).mock.invocationCallOrder[0];
const putOrder = vi.mocked(apiClient.put).mock.invocationCallOrder[0];
expect(csrfOrder).toBeLessThan(putOrder);
expect(r.previous_value).toBe('500');
});
it('updateSystemSetting() encodeURIComponent применяется к ключу со спецсимволами', async () => {
vi.mocked(apiClient.put).mockResolvedValue({
data: {
key: 'a/b',
value: 'v',
previous_value: 'p',
updated_at: '2026-05-14T12:00:00Z',
message: 'ok',
},
});
const payload = { value: 'v', reason: 'r'.repeat(40), admin_user_id: 1 };
await updateSystemSetting('a/b', payload);
expect(apiClient.put).toHaveBeenCalledWith('/api/admin/system-settings/a%2Fb', payload);
});
// === Error propagation ===
it('impersonationInit() пробрасывает ошибку из apiClient.post (не глотает)', async () => {
vi.mocked(apiClient.post).mockRejectedValueOnce(new Error('Network'));
await expect(
impersonationInit({ tenant_id: 1, requested_by: 9, reason: 'r'.repeat(30) }),
).rejects.toThrow('Network');
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
});
it('listAdminTenants() пробрасывает ошибку из apiClient.get (не глотает)', async () => {
vi.mocked(apiClient.get).mockRejectedValueOnce(new Error('500 Server Error'));
await expect(listAdminTenants({ status: 'active' })).rejects.toThrow('500 Server Error');
});
});