Files
clinicpro/assets/admin/lib/api.test.ts
T
hamed 773f9d4d16 feat: update session payment logic to ensure accurate payable amounts and reflect consumables in cost breakdown
- Adjusted the calculation of payable amounts in PaymentStep to align with server logic, ensuring overpayments are handled correctly.
- Enhanced DetailsStep to include consumables in the itemized cost breakdown, ensuring consistency with patient share calculations.
- Updated tests for SessionPaymentPage to validate new behavior regarding overpayments and consumable listings.
- Modified PatientController to register SessionPayment correctly when settling sessions via wallet, preventing double charges.
- Refactored WalletService to remove outdated methods and ensure wallet transactions reflect the correct amounts after discounts.
- Improved accessibility in SearchableSelect component by adding aria labels and ensuring proper role attributes for screen readers.
- Updated styles to ensure minimum touch targets meet WCAG guidelines for mobile usability.
2026-07-19 13:29:46 +03:30

135 lines
5.4 KiB
TypeScript

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 {
// headers لازم است: api.ts پیش از خواندن بدنه، Content-Length را برای پاسخ خالی چک می‌کند
return { ok, status, headers: new Headers(), 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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue(jsonRes({ ok: 1 }));
await api.get('/api/v1/x');
const headers = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].headers;
expect(headers['Authorization']).toBe('Bearer tok-123');
});
it('بدون توکن، هدر Authorization نمی‌فرستد', async () => {
(fetch as ReturnType<typeof vi.fn>).mockResolvedValue(jsonRes({ ok: 1 }));
await api.get('/api/v1/x');
const headers = (fetch as ReturnType<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue(jsonRes({ ok: 1 }));
await api.post('/api/v1/x', { a: 1 });
const opts = (fetch as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>)
.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<typeof vi.fn>).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<typeof vi.fn>;
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);
});
});