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(); return { ...orig, listAdminTenants: vi.fn(), }; }); const adminApi = await import('../../resources/js/api/admin'); beforeEach(() => { vi.clearAllMocks(); setActivePinia(createPinia()); }); function makeApiTenant(overrides: Partial = {}): 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: '
' } }, ], }); await router.push('/admin/tenants'); await router.isReady(); return mount(AdminTenantsView, { global: { plugins: [createVuetify(), router], stubs: { ImpersonationDialog: 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 виден + MOCK_TENANTS остаётся', 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).toBeGreaterThan(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(); }); });