import { describe, it, expect, vi, beforeEach } from 'vitest'; const { refreshMock, logoutMock } = vi.hoisted(() => ({ refreshMock: vi.fn(), logoutMock: vi.fn(), })); vi.mock('@/stores/authStore', () => ({ useAuthStore: { getState: () => ({ refresh: refreshMock, logout: logoutMock }) }, })); import { api, ApiError } from '@/lib/api'; const replaceMock = vi.fn(); function jsonRes(body: unknown, ok = true, status = 200): Response { return { ok, status, json: () => Promise.resolve(body) } as unknown as Response; } function setToken(token: string) { localStorage.setItem('clinicpro-auth', JSON.stringify({ state: { token } })); } beforeEach(() => { localStorage.clear(); refreshMock.mockReset(); logoutMock.mockReset(); replaceMock.mockReset(); vi.stubGlobal('fetch', vi.fn()); Object.defineProperty(window, 'location', { value: { replace: replaceMock }, writable: true, configurable: true, }); }); describe('request — موفق', () => { it('بدنه JSON را برمی‌گرداند', async () => { (fetch as ReturnType).mockResolvedValue( jsonRes({ success: true, data: { id: 1 } }), ); const out = await api.get<{ data: { id: number } }>('/api/v1/x'); expect(out).toEqual({ success: true, data: { id: 1 } }); expect(fetch).toHaveBeenCalledTimes(1); }); it('وقتی توکن هست هدر Authorization می‌فرستد', async () => { setToken('tok-123'); (fetch as ReturnType).mockResolvedValue(jsonRes({ ok: 1 })); await api.get('/api/v1/x'); const headers = (fetch as ReturnType).mock.calls[0][1].headers; expect(headers['Authorization']).toBe('Bearer tok-123'); }); it('بدون توکن، هدر Authorization نمی‌فرستد', async () => { (fetch as ReturnType).mockResolvedValue(jsonRes({ ok: 1 })); await api.get('/api/v1/x'); const headers = (fetch as ReturnType).mock.calls[0][1].headers; expect(headers['Authorization']).toBeUndefined(); expect(headers['Content-Type']).toBe('application/json'); }); it('post بدنه را JSON.stringify و method=POST می‌فرستد', async () => { (fetch as ReturnType).mockResolvedValue(jsonRes({ ok: 1 })); await api.post('/api/v1/x', { a: 1 }); const opts = (fetch as ReturnType).mock.calls[0][1]; expect(opts.method).toBe('POST'); expect(opts.body).toBe(JSON.stringify({ a: 1 })); }); }); describe('request — خطاهای غیر۴۰۱', () => { it('۴۲۲ → ApiError با code/message از errors[0]', async () => { (fetch as ReturnType).mockResolvedValue( jsonRes({ errors: [{ code: 'ERR_VALIDATION', message: 'نامعتبر' }] }, false, 422), ); await expect(api.get('/api/v1/x')).rejects.toMatchObject({ status: 422, code: 'ERR_VALIDATION', message: 'نامعتبر', }); }); it('بدنه بدون errors → ERR_UNKNOWN', async () => { (fetch as ReturnType).mockResolvedValue(jsonRes({}, false, 500)); const err = (await api.get('/api/v1/x').catch((e) => e)) as ApiError; expect(err).toBeInstanceOf(ApiError); expect(err.code).toBe('ERR_UNKNOWN'); expect(err.status).toBe(500); }); }); describe('request — جریان ۴۰۱', () => { it('refresh موفق → یک‌بار retry و موفقیت', async () => { refreshMock.mockResolvedValue('new-token'); (fetch as ReturnType) .mockResolvedValueOnce(jsonRes({}, false, 401)) .mockResolvedValueOnce(jsonRes({ success: true, data: 'ok' })); const out = await api.get('/api/v1/x'); expect(out).toEqual({ success: true, data: 'ok' }); expect(refreshMock).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(2); expect(logoutMock).not.toHaveBeenCalled(); }); it('refresh ناموفق → logout + redirect + ApiError(401)', async () => { refreshMock.mockResolvedValue(null); (fetch as ReturnType).mockResolvedValue(jsonRes({}, false, 401)); const err = (await api.get('/api/v1/x').catch((e) => e)) as ApiError; expect(err).toBeInstanceOf(ApiError); expect(err.status).toBe(401); expect(err.code).toBe('ERR_UNAUTHORIZED'); expect(logoutMock).toHaveBeenCalledTimes(1); expect(replaceMock).toHaveBeenCalledWith('/admin/login'); }); it('de-dup: دو درخواست همزمان ۴۰۱ فقط یک‌بار refresh می‌زنند', async () => { refreshMock.mockImplementation( () => new Promise((r) => setTimeout(() => r('new-token'), 10)), ); const f = fetch as ReturnType; f.mockImplementation(() => { const n = f.mock.calls.length; return Promise.resolve(n <= 2 ? jsonRes({}, false, 401) : jsonRes({ ok: 1 })); }); await Promise.all([api.get('/api/v1/a'), api.get('/api/v1/b')]); expect(refreshMock).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(4); }); });