Files
portal/app/tests/Frontend/AdminTenantsViewApi.spec.ts
T
2026-05-23 20:02:39 +03:00

197 lines
7.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import AdminTenantsView from '../../resources/js/views/admin/AdminTenantsView.vue';
import { mapApiAdminTenant } from '../../resources/js/composables/adminTenantsMapper';
import type { AdminTenant as ApiAdminTenant } from '../../resources/js/api/admin';
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
return {
...orig,
listAdminTenants: vi.fn(),
};
});
const adminApi = await import('../../resources/js/api/admin');
beforeEach(() => {
vi.clearAllMocks();
setActivePinia(createPinia());
});
function makeApiTenant(overrides: Partial<ApiAdminTenant> = {}): ApiAdminTenant {
return {
id: 1,
subdomain: 'test',
organization_name: 'Test ООО',
contact_email: 'admin@test.io',
status: 'active',
balance_rub: '1000.00',
balance_leads: 10,
is_trial: false,
last_activity_at: new Date().toISOString(),
tariff_id: null,
tariff_name: 'Команда',
mrr_rub: '990.00',
desired_daily_numbers: 5,
chargeback_unrecovered_rub: '0.00',
created_at: new Date().toISOString(),
...overrides,
};
}
const mountView = async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/admin/tenants', component: AdminTenantsView },
{ path: '/admin/tenants/:code', name: 'admin-tenant-detail', component: { template: '<div />' } },
],
});
await router.push('/admin/tenants');
await router.isReady();
return mount(AdminTenantsView, {
global: {
plugins: [createVuetify(), router],
stubs: { ImpersonationDialog: true, TenantBalanceDialog: true },
},
});
};
describe('AdminTenantsView ↔ GET /api/admin/tenants integration', () => {
it('listAdminTenants вызывается на mount', async () => {
vi.mocked(adminApi.listAdminTenants).mockResolvedValueOnce({
tenants: [],
total: 0,
limit: 100,
offset: 0,
stats: { total: 0, active: 0, trial: 0, overdue: 0 },
});
await mountView();
await flushPromises();
expect(adminApi.listAdminTenants).toHaveBeenCalledTimes(1);
});
it('успех — replace tenantsState на API-данные + stats', async () => {
vi.mocked(adminApi.listAdminTenants).mockResolvedValueOnce({
tenants: [
makeApiTenant({ id: 100, organization_name: 'Окна Москва', tariff_name: 'Команда' }),
makeApiTenant({ id: 101, organization_name: 'Двери СПб', tariff_name: 'Pro', is_trial: true }),
],
total: 2,
limit: 100,
offset: 0,
stats: { total: 2, active: 1, trial: 1, overdue: 0 },
});
const wrapper = await mountView();
await flushPromises();
const vm = wrapper.vm as unknown as {
tenantsState: { id: number; name: string; status: string }[];
stats: { total: number; active: number; trial: number; overdue: number };
};
expect(vm.tenantsState).toHaveLength(2);
expect(vm.tenantsState[0].id).toBe(100);
expect(vm.tenantsState[0].name).toBe('Окна Москва');
expect(vm.tenantsState[1].status).toBe('trial'); // is_trial=true → 'trial'
expect(vm.stats.total).toBe(2);
expect(vm.stats.trial).toBe(1);
});
it('reject → fetchError=true + alert виден + tenantsState пустой', async () => {
vi.mocked(adminApi.listAdminTenants).mockRejectedValueOnce(new Error('500'));
const wrapper = await mountView();
await flushPromises();
const vm = wrapper.vm as unknown as { fetchError: boolean; tenantsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.tenantsState.length).toBe(0); // пустой при ошибке, не mock-fallback
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
it('reload-btn вызывает listAdminTenants второй раз', async () => {
vi.mocked(adminApi.listAdminTenants).mockResolvedValue({
tenants: [],
total: 0,
limit: 100,
offset: 0,
stats: { total: 0, active: 0, trial: 0, overdue: 0 },
});
const wrapper = await mountView();
await flushPromises();
expect(adminApi.listAdminTenants).toHaveBeenCalledTimes(1);
await wrapper.find('[data-testid="reload-btn"]').trigger('click');
await flushPromises();
expect(adminApi.listAdminTenants).toHaveBeenCalledTimes(2);
});
});
describe('mapApiAdminTenant', () => {
it('маппит organization_name → name, subdomain → code', () => {
const m = mapApiAdminTenant(makeApiTenant({ subdomain: 'okna', organization_name: 'Окна' }));
expect(m.code).toBe('okna');
expect(m.name).toBe('Окна');
});
it('inn пустой (нет в API)', () => {
const m = mapApiAdminTenant(makeApiTenant());
expect(m.inn).toBe('');
});
it('is_trial=true → status=trial', () => {
const m = mapApiAdminTenant(makeApiTenant({ is_trial: true }));
expect(m.status).toBe('trial');
expect(m.statusText).toBe('Trial');
});
it('chargeback>0 → status=overdue', () => {
const m = mapApiAdminTenant(makeApiTenant({ chargeback_unrecovered_rub: '1000.00' }));
expect(m.status).toBe('overdue');
});
it('balance<0 → status=overdue', () => {
const m = mapApiAdminTenant(makeApiTenant({ balance_rub: '-200.00' }));
expect(m.status).toBe('overdue');
});
it('schema status=suspended → status=suspended', () => {
const m = mapApiAdminTenant(makeApiTenant({ status: 'suspended', is_trial: false }));
expect(m.status).toBe('suspended');
});
it('balance_rub строка → number', () => {
const m = mapApiAdminTenant(makeApiTenant({ balance_rub: '15000.50' }));
expect(m.balanceRub).toBe(15000.5);
});
it('последняя активность форматируется как «X мин назад»', () => {
const fixedNow = new Date('2026-05-09T10:00:00Z');
const tenMinAgo = new Date('2026-05-09T09:50:00Z');
const m = mapApiAdminTenant(makeApiTenant({ last_activity_at: tenMinAgo.toISOString() }), fixedNow);
expect(m.activitySince).toBe('10 мин назад');
});
it('last_activity_at=null → activitySince=«—»', () => {
const m = mapApiAdminTenant(makeApiTenant({ last_activity_at: null }));
expect(m.activitySince).toBe('—');
});
it('mrr_rub строка → number', () => {
const m = mapApiAdminTenant(makeApiTenant({ mrr_rub: '990.00' }));
expect(m.mrrRub).toBe(990);
});
it("mrr_rub=null → mrrRub null (для trial и tenant'ов без тарифа)", () => {
const m = mapApiAdminTenant(makeApiTenant({ mrr_rub: null }));
expect(m.mrrRub).toBeNull();
});
});