- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks. - Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content. - Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
192 lines
8.7 KiB
TypeScript
192 lines
8.7 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||
import { Routes, Route } from 'react-router';
|
||
import { renderWithProviders } from '../test/utils';
|
||
|
||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||
vi.mock('../lib/api', () => ({
|
||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||
ApiError: class extends Error {},
|
||
}));
|
||
|
||
import { api } from '../lib/api';
|
||
import { formatRial } from '../lib/utils';
|
||
import NewSessionPage from './NewSessionPage';
|
||
|
||
const get = api.get as ReturnType<typeof vi.fn>;
|
||
const post = api.post as ReturnType<typeof vi.fn>;
|
||
|
||
const record = (profileOver: object = {}) => ({
|
||
uuid: 'r1',
|
||
user_name: 'ساغر صابری',
|
||
profile: { full_name: 'ساغر صابری', basic_insurance_id: null, supplementary_insurance_id: null, ...profileOver },
|
||
});
|
||
|
||
function mockGets(over: Partial<Record<string, unknown>> = {}) {
|
||
get.mockImplementation((url: string) => {
|
||
if (url === '/api/v1/patient/r1') return Promise.resolve({ success: true, data: over.record ?? record() });
|
||
if (url === '/api/v1/service-sections') {
|
||
return Promise.resolve({ success: true, data: over.sections ?? [{ uuid: 'sec1', name: 'بخش زیبایی' }] });
|
||
}
|
||
if (url === '/api/v1/service-items/sec1') {
|
||
return Promise.resolve({
|
||
success: true,
|
||
data: over.items ?? [
|
||
{ uuid: 'svc1', name: 'کاشت مو', price_rials: 2_400_000, active: true, insurance_covered: false, staff: null },
|
||
],
|
||
});
|
||
}
|
||
if (url === '/api/v1/staff') {
|
||
return Promise.resolve({ success: true, data: over.staff ?? [{ uuid: 'st1', full_name: 'علی رضایی', active: true }] });
|
||
}
|
||
if (url === '/api/v1/inventory-items') {
|
||
return Promise.resolve({
|
||
success: true,
|
||
data: { items: over.inventory ?? [{ uuid: 'inv1', name: 'عینک', unit: 'عدد', price: 1_200_000, stock: 10, status: 'in_stock' }], stats: {} },
|
||
});
|
||
}
|
||
if (url === '/api/v1/inventory-packages') {
|
||
return Promise.resolve({ success: true, data: over.packages ?? [{ uuid: 'pkg1', title: 'پکیج شماره یک', total: 2_400_000, available: true }] });
|
||
}
|
||
if (url === '/api/v1/billing/tenant-insurances') {
|
||
return Promise.resolve({ success: true, data: { data: over.contracts ?? [] } });
|
||
}
|
||
if (url === '/api/v1/insurance-pricing') {
|
||
return Promise.resolve({ success: true, data: { free_visit_price_rials: 500_000 } });
|
||
}
|
||
if (url === '/api/v1/patient/r1/sessions') {
|
||
return Promise.resolve({ success: true, data: over.sessions ?? [] });
|
||
}
|
||
if (url === '/api/v1/patient/r1/wallet') {
|
||
return Promise.resolve({ success: true, data: { balance_rials: 300_000, recent_transactions: [] } });
|
||
}
|
||
return Promise.resolve({ success: true, data: [] });
|
||
});
|
||
}
|
||
|
||
function renderPage() {
|
||
return renderWithProviders(
|
||
<Routes>
|
||
<Route path="/admin/patients/:recordUuid/session/new" element={<NewSessionPage />} />
|
||
</Routes>,
|
||
{ route: '/admin/patients/r1/session/new' },
|
||
);
|
||
}
|
||
|
||
/** انتخاب گزینه از SearchableSelect (react-select) با inputId */
|
||
async function pickOption(inputId: string, option: string) {
|
||
const input = document.getElementById(inputId) as HTMLInputElement;
|
||
fireEvent.focus(input);
|
||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||
fireEvent.click(await screen.findByText(option));
|
||
}
|
||
|
||
beforeEach(() => {
|
||
get.mockReset();
|
||
post.mockReset();
|
||
});
|
||
|
||
describe('NewSessionPage (ویزارد ثبت مراجعه — پورت tauri create-service)', () => {
|
||
it('renders the 3-step stepper and step-1 fields (tauri Step1Details)', async () => {
|
||
mockGets();
|
||
renderPage();
|
||
|
||
// استپر سه گام («ایجاد سرویس» هم برچسب گام است هم دکمه ثبت)
|
||
expect((await screen.findAllByText('ایجاد سرویس')).length).toBeGreaterThanOrEqual(2);
|
||
expect(screen.getByText('پرداخت')).toBeInTheDocument();
|
||
expect(screen.getByText('جزییات')).toBeInTheDocument();
|
||
// فیلدهای گام ۱
|
||
expect(screen.getByText('تاریخ پذیرش')).toBeInTheDocument();
|
||
expect(screen.getByText('ساعت پذیرش')).toBeInTheDocument();
|
||
expect(screen.getByText('انتخاب بخش')).toBeInTheDocument();
|
||
expect(screen.getByText('انتخاب سرویس')).toBeInTheDocument();
|
||
expect(screen.getByText('پرسنل')).toBeInTheDocument();
|
||
expect(screen.getByText('انتخاب کالای مصرفی')).toBeInTheDocument();
|
||
expect(screen.getByText('انتخاب پکیج')).toBeInTheDocument();
|
||
// پذیرشکننده + breadcrumb بیمار
|
||
expect(screen.getByText(/پذیرش کننده:/)).toBeInTheDocument();
|
||
expect((await screen.findAllByText('ساغر صابری')).length).toBeGreaterThan(0);
|
||
// بدون سرویس بیمهدار و بدون بیمهی پروفایل → بلوک بیمه پنهان
|
||
expect(screen.queryByText('بیمه پایه')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('shows the insurance block when patient profile has insurance', async () => {
|
||
mockGets({ record: record({ basic_insurance_id: 7 }) });
|
||
renderPage();
|
||
|
||
expect(await screen.findByText('بیمه پایه')).toBeInTheDocument();
|
||
expect(screen.getByText('بیمه تکمیلی')).toBeInTheDocument();
|
||
});
|
||
|
||
it('creates the session with session_at, consumables, package and staff then moves to payment step', async () => {
|
||
mockGets({
|
||
sessions: [{
|
||
uuid: 's-new', services: [], visit_price_rials: 500_000, final_price_rials: 1_700_000,
|
||
patient_debt_rials: 1_700_000, discount_rials: 0, payments: [], is_paid: false,
|
||
}],
|
||
});
|
||
post.mockResolvedValue({ success: true, data: { uuid: 's-new' } });
|
||
renderPage();
|
||
|
||
await screen.findByText('انتخاب بخش');
|
||
|
||
// بخش → سرویس → افزودن
|
||
await pickOption('section-select', 'بخش زیبایی');
|
||
await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/service-items/sec1'));
|
||
await pickOption('service-select', 'کاشت مو');
|
||
fireEvent.click(screen.getByLabelText('افزودن سرویس'));
|
||
expect(screen.getByText('خدمات انتخاب شده')).toBeInTheDocument();
|
||
|
||
// کالای مصرفی: عینک
|
||
await pickOption('consumable-select', 'عینک');
|
||
fireEvent.click(screen.getByLabelText('افزودن کالا'));
|
||
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(1_200_000)}`)).toBeInTheDocument();
|
||
|
||
// ثبت
|
||
fireEvent.click(screen.getByText('ایجاد سرویس', { selector: 'button' }));
|
||
await waitFor(() => expect(post).toHaveBeenCalledTimes(1));
|
||
|
||
const [url, body] = post.mock.calls[0] as [string, any];
|
||
expect(url).toBe('/api/v1/patient/r1/session');
|
||
expect(typeof body.session_at).toBe('number');
|
||
expect(body.payment_method).toBe('pending');
|
||
expect(body.services).toEqual([{ service_item_uuid: 'svc1', quantity: 1 }]);
|
||
expect(body.consumables).toEqual([{ inventory_item_uuid: 'inv1', quantity: 1 }]);
|
||
// بدون بیمه → درصدها صفر و id ارسال نمیشود
|
||
expect(body.base_insurance_discount_percent).toBe(0);
|
||
expect(body.insurance_base_id).toBeUndefined();
|
||
|
||
// ورود به گام پرداخت با دیتای واقعی session ساختهشده
|
||
expect(await screen.findByText('هزینه سرویس:')).toBeInTheDocument();
|
||
expect(screen.getByText('موجودی کیف پول:')).toBeInTheDocument();
|
||
expect(screen.getAllByText(formatRial(1_700_000)).length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('increments consumable quantity via the counter and reflects the total', async () => {
|
||
mockGets();
|
||
renderPage();
|
||
|
||
await screen.findByText('انتخاب کالای مصرفی');
|
||
await pickOption('consumable-select', 'عینک');
|
||
fireEvent.click(screen.getByLabelText('افزودن کالا'));
|
||
|
||
fireEvent.click(screen.getByLabelText('افزایش'));
|
||
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(2_400_000)}`)).toBeInTheDocument();
|
||
|
||
// کاهش تا صفر → حذف
|
||
fireEvent.click(screen.getByLabelText('کاهش'));
|
||
fireEvent.click(screen.getByLabelText('کاهش'));
|
||
expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders empty option lists without crashing (بدون بخش/کالا/پکیج)', async () => {
|
||
mockGets({ sections: [], inventory: [], packages: [], staff: [] });
|
||
renderPage();
|
||
|
||
expect(await screen.findByText('انتخاب بخش')).toBeInTheDocument();
|
||
expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument();
|
||
// مجموع صفر
|
||
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(0)}`)).toBeInTheDocument();
|
||
});
|
||
});
|