Files
clinicpro/assets/admin/pages/MySecretariesPage.test.tsx
T
hamedandClaude Opus 5 ddd5f8f75a feat(permissions): render both permission forms from the catalog, fix borrowed gates
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>
2026-08-07 18:11:05 +03:30

156 lines
6.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { screen, fireEvent } from "@testing-library/react";
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 {},
}));
vi.mock("../stores/authStore", () => ({
useAuthStore: () => ({ doctorUuid: "doc-1", dbUuid: "doc-1", primaryRole: "doctor" }),
}));
vi.mock("../hooks/useSubscription", () => ({
useSubscription: () => ({ maxSecretaries: 5 }),
}));
import { api } from "../lib/api";
import MySecretariesPage from "./MySecretariesPage";
const get = api.get as ReturnType<typeof vi.fn>;
const fullPerms = {
appointments: { view: true, create: false, cancel: false, update_status: false },
patients: { view: false, create: false, update: false, delete: false },
payments: { view: false, create: false, update: false, delete: false },
insurances: { view: false, create: false, update: false, delete: false },
addresses: { view: false, create: false, update: false, delete: false },
clinic_info: { view: false, update: false },
};
const activeSecretary = {
uuid: "sec-1",
user_name: "سارا احمدی",
mobile_number: "09121234567",
doctor_name: "دکتر تست",
doctor_uuid: "doc-1",
is_active: true,
national_code: "1234567890",
address: "یزد",
permissions: fullPerms,
created_at: 1700000000,
};
const previousSecretary = {
...activeSecretary,
uuid: "sec-2",
user_name: "مینا رضایی",
mobile_number: "09129876543",
is_active: false,
national_code: "9999999999",
};
/** فهرست بخش‌ها از GET /api/v1/permission-catalog می‌آید، نه از کد کامپوننت. */
const CATALOG = [
{
key: "appointments", label: "مدیریت نوبت‌ها", clinic_only: false,
actions: [{ key: "view", label: "مشاهده نوبت‌ها" }, { key: "create", label: "ایجاد نوبت" }],
},
{
key: "patients", label: "پرونده بیماران", clinic_only: false,
actions: [{ key: "view", label: "مشاهده بیماران" }],
},
{
key: "clinic_doctors", label: "مدیریت پزشکان کلینیک", clinic_only: true,
actions: [{ key: "view", label: "مشاهده پزشکان" }],
},
];
function mockData(rows = [activeSecretary, previousSecretary], catalog: unknown[] = CATALOG) {
get.mockImplementation((url: string) => {
if (url.includes("permission-catalog")) {
return Promise.resolve({ success: true, data: { version: 1, resources: catalog } });
}
if (url.includes("/secretaries/")) return Promise.resolve({ success: true, data: rows });
return Promise.resolve({ success: true, data: [] });
});
}
beforeEach(() => {
get.mockReset();
mockData();
});
describe("MySecretariesPage", () => {
it("renders the title and both tabs", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
expect(await screen.findByText("لیست منشی ها")).toBeInTheDocument();
expect(screen.getByText("منشی های فعلی")).toBeInTheDocument();
expect(screen.getByText("منشی های قبلی")).toBeInTheDocument();
});
it("shows active secretaries with national code on the default tab", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
expect(await screen.findAllByText("سارا احمدی")).not.toHaveLength(0);
expect(screen.getAllByText("1234567890").length).toBeGreaterThan(0);
// inactive secretary is hidden on the active tab
expect(screen.queryByText("مینا رضایی")).not.toBeInTheDocument();
});
it("switches to the previous tab and lists inactive secretaries", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("منشی های قبلی"));
expect(await screen.findAllByText("مینا رضایی")).not.toHaveLength(0);
expect(screen.queryByText("سارا احمدی")).not.toBeInTheDocument();
});
it("shows an empty state when there are no active secretaries", async () => {
mockData([previousSecretary]);
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
expect(await screen.findByText("هنوز منشی فعالی اضافه نشده است")).toBeInTheDocument();
});
it("opens the add modal with permission sections from the catalog", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
expect(await screen.findByText("اضافه کردن منشی جدید")).toBeInTheDocument();
expect(screen.getByText("مجوزهای دسترسی")).toBeInTheDocument();
expect(await screen.findByText("مدیریت نوبت‌ها")).toBeInTheDocument();
expect(screen.getByText("پرونده بیماران")).toBeInTheDocument();
});
/**
* همان تضمینِ «داینامیک بودن»: منبع تازه فقط به mock اضافه می‌شود و هیچ خطی از
* کد کامپوننت عوض نمی‌شود.
*/
it("shows a resource added to the catalog with no code change", async () => {
mockData(undefined, [
...CATALOG,
{ key: "brand_new_page", label: "صفحهٔ کاملاً تازه", clinic_only: false, actions: [{ key: "view", label: "مشاهده" }] },
]);
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
expect(await screen.findByText("صفحهٔ کاملاً تازه")).toBeInTheDocument();
});
/** منبعِ clinic_only برای پزشکِ مستقل (primaryRole=doctor) نباید دیده شود. */
it("hides clinic-only resources for a standalone doctor", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
expect(await screen.findByText("مدیریت نوبت‌ها")).toBeInTheDocument();
expect(screen.queryByText("مدیریت پزشکان کلینیک")).not.toBeInTheDocument();
});
});