The three hardcoded resource lists in the admin panel are gone. MySecretariesPage, SecretariesPage and DoctorPermissionsModal now render from GET /api/v1/permission-catalog, so a resource added to the backend registry shows up in all of them with no frontend change. Each has a test that proves exactly that by adding a resource to the mock and asserting it renders. SecretaryPermissions was an interface with a field per resource, which made "dynamic" impossible in TypeScript — every new resource would have been a compile error. It is now an open map. Only two files consumed it. The borrowed gates are corrected: - five resource pages moved off appointment_settings onto their own 'resources' - treatment-cases moved off appointments onto 'treatment' - service-categories moved onto 'services', which is what ServiceCatalogController actually manages (categories, item groups, service relations) — not resources TreatmentCaseController had no permission gate at all, only IS_AUTHENTICATED_FULLY, so any secretary could read and edit treatment cases. All seven of its actions are now gated on treatment view/update. ResourcePermissionTrait takes the resource from an overridable method instead of hardcoding appointment_settings. HolidayController overrides it back, since the holidays page really is appointment settings. The booking gate keeps its appointments.view fallback so a secretary who may book is not blocked by a resource-config permission. Defaults were picked to preserve today's effective access, so no role gains or loses a page from this move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
4.0 KiB
TypeScript
103 lines
4.0 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import { renderWithProviders } from '../../test/utils';
|
|
|
|
vi.mock('../../lib/api', () => ({
|
|
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
|
ApiError: class extends Error {},
|
|
}));
|
|
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
import { api } from '../../lib/api';
|
|
import DoctorPermissionsModal from './DoctorPermissionsModal';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
const resource = (key: string, label: string, actions: [string, string][]) => ({
|
|
key,
|
|
label,
|
|
clinic_only: false,
|
|
actions: actions.map(([k, l]) => ({ key: k, label: l })),
|
|
});
|
|
|
|
const BASE_CATALOG = [
|
|
resource('appointments', 'مدیریت نوبتها', [['view', 'مشاهده'], ['create', 'ایجاد']]),
|
|
resource('patients', 'پرونده بیماران', [['view', 'مشاهده'], ['delete', 'حذف']]),
|
|
resource('services', 'خدمات و تعرفهها', [['view', 'مشاهده']]),
|
|
];
|
|
|
|
const permissionPayload = {
|
|
uuid: 'p1',
|
|
clinic_uuid: 'c1',
|
|
doctor_uuid: 'd1',
|
|
doctor_name: 'دکتر تست',
|
|
active: true,
|
|
permissions: { version: 1, resources: { appointments: { view: true, create: false } } },
|
|
};
|
|
|
|
function mockApi(catalog: unknown[]) {
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('permission-catalog')) {
|
|
return Promise.resolve({ success: true, data: { version: 1, resources: catalog } });
|
|
}
|
|
return Promise.resolve({ success: true, data: permissionPayload });
|
|
});
|
|
}
|
|
|
|
function render(catalog: unknown[] = BASE_CATALOG) {
|
|
mockApi(catalog);
|
|
return renderWithProviders(
|
|
<DoctorPermissionsModal clinicUuid="c1" doctorUuid="d1" doctorName="دکتر تست" onClose={() => {}} />,
|
|
);
|
|
}
|
|
|
|
describe('DoctorPermissionsModal', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('renders one row per catalog resource', async () => {
|
|
render();
|
|
|
|
expect(await screen.findByText('مدیریت نوبتها')).toBeInTheDocument();
|
|
expect(screen.getByText('پرونده بیماران')).toBeInTheDocument();
|
|
expect(screen.getByText('خدمات و تعرفهها')).toBeInTheDocument();
|
|
});
|
|
|
|
/**
|
|
* قلبِ «داینامیک بودن»: منبع تازه فقط به mock اضافه میشود و هیچ خطی از کد
|
|
* کامپوننت عوض نمیشود. اگر این تست بشکند یعنی فهرست دوباره هاردکد شده.
|
|
*/
|
|
it('shows a resource added to the catalog with no code change', async () => {
|
|
render([...BASE_CATALOG, resource('brand_new_page', 'صفحهٔ کاملاً تازه', [['view', 'مشاهده']])]);
|
|
|
|
expect(await screen.findByText('صفحهٔ کاملاً تازه')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders a switch only for actions the resource actually has', async () => {
|
|
render();
|
|
|
|
// patients در کاتالوگ view و delete دارد، ولی create ندارد.
|
|
expect(await screen.findByLabelText('پرونده بیماران — حذف')).toBeInTheDocument();
|
|
expect(screen.queryByLabelText('پرونده بیماران — ایجاد')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('reflects stored values and defaults missing ones to off', async () => {
|
|
render();
|
|
|
|
await waitFor(() => expect(screen.getByLabelText('مدیریت نوبتها — مشاهده')).toBeChecked());
|
|
expect(screen.getByLabelText('مدیریت نوبتها — ایجاد')).not.toBeChecked();
|
|
// services اصلاً در JSONِ ذخیرهشده نیست
|
|
expect(screen.getByLabelText('خدمات و تعرفهها — مشاهده')).not.toBeChecked();
|
|
});
|
|
|
|
it('shows loading rather than an empty table while the catalog is in flight', () => {
|
|
get.mockImplementation(() => new Promise(() => {}));
|
|
|
|
renderWithProviders(
|
|
<DoctorPermissionsModal clinicUuid="c1" doctorUuid="d1" doctorName="دکتر تست" onClose={() => {}} />,
|
|
);
|
|
|
|
expect(screen.getByText('در حال بارگذاری...')).toBeInTheDocument();
|
|
});
|
|
});
|