mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
445
packages/core/src/components/canAccess/index.spec.tsx
Normal file
445
packages/core/src/components/canAccess/index.spec.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
import React from "react";
|
||||
|
||||
import { act } from "react-dom/test-utils";
|
||||
|
||||
import {
|
||||
mockLegacyRouterProvider,
|
||||
mockRouterProvider,
|
||||
render,
|
||||
TestWrapper,
|
||||
waitFor,
|
||||
} from "@test";
|
||||
|
||||
import * as UseCanHook from "../../hooks/accessControl/useCan";
|
||||
import { CanAccess } from ".";
|
||||
|
||||
describe("CanAccess Component", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render children", async () => {
|
||||
const onUnauthorized = jest.fn();
|
||||
|
||||
const { container, findByText } = render(
|
||||
<CanAccess
|
||||
action="list"
|
||||
resource="posts"
|
||||
onUnauthorized={(args) => onUnauthorized(args)}
|
||||
>
|
||||
Accessible
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async ({ resource, action }) => {
|
||||
if (action === "list" && resource === "posts") {
|
||||
return {
|
||||
can: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
await findByText("Accessible");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUnauthorized).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render children and call onUnauthorized", async () => {
|
||||
const onUnauthorized = jest.fn();
|
||||
|
||||
const { container, queryByText } = render(
|
||||
<CanAccess
|
||||
action="list"
|
||||
resource="posts"
|
||||
onUnauthorized={(args) => onUnauthorized(args)}
|
||||
>
|
||||
Accessible
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async () => ({
|
||||
can: false,
|
||||
reason: "test",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
expect(container).toBeTruthy();
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUnauthorized).toHaveBeenCalledTimes(1);
|
||||
expect(onUnauthorized).toHaveBeenCalledWith({
|
||||
resource: "posts",
|
||||
action: "list",
|
||||
reason: "test",
|
||||
params: {
|
||||
id: undefined,
|
||||
resource: expect.objectContaining({
|
||||
name: "posts",
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully pass the own attirbute to its children", async () => {
|
||||
const { container, findByText } = render(
|
||||
<CanAccess action="list" resource="posts" data-id="refine">
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async () => ({
|
||||
can: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
const el = await findByText("Accessible");
|
||||
|
||||
expect(el.closest("p")?.getAttribute("data-id"));
|
||||
});
|
||||
|
||||
it("should fallback successfully render when not accessible", async () => {
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess action="list" resource="posts" fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async () => ({ can: false }),
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
|
||||
describe("when no prop is passed", () => {
|
||||
it("should work", async () => {
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
resources: [{ name: "posts", list: "/posts" }],
|
||||
routerProvider: mockRouterProvider({
|
||||
resource: { name: "posts", list: "/posts" },
|
||||
action: "list",
|
||||
id: undefined,
|
||||
}),
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(useCanSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resource: "posts",
|
||||
action: "list",
|
||||
params: {
|
||||
id: undefined,
|
||||
resource: expect.objectContaining({
|
||||
list: "/posts",
|
||||
name: "posts",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
|
||||
test("when fallback is empty", async () => {
|
||||
const { container } = render(
|
||||
<CanAccess action="list" resource="posts">
|
||||
Accessible
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container.nodeValue).toStrictEqual(null);
|
||||
});
|
||||
|
||||
describe("When props not passed", () => {
|
||||
describe("When new router", () => {
|
||||
describe("when resource is an object", () => {
|
||||
it("should deny access", async () => {
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
resources: [{ name: "posts", list: "/posts" }],
|
||||
routerProvider: mockRouterProvider({
|
||||
resource: { name: "posts", list: "/posts" },
|
||||
action: "list",
|
||||
id: undefined,
|
||||
}),
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(useCanSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resource: "posts",
|
||||
action: "list",
|
||||
params: expect.objectContaining({
|
||||
id: undefined,
|
||||
resource: expect.objectContaining({
|
||||
name: "posts",
|
||||
list: "/posts",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when resource is a string", () => {
|
||||
describe("when pick resource is object", () => {
|
||||
it("should deny access", async () => {
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
resources: [
|
||||
{ name: "posts", list: "/posts", identifier: "posts" },
|
||||
],
|
||||
routerProvider: mockRouterProvider({
|
||||
action: "list",
|
||||
id: undefined,
|
||||
resource: {
|
||||
name: "posts",
|
||||
list: "/posts",
|
||||
identifier: "posts",
|
||||
},
|
||||
}),
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(useCanSpy).toHaveBeenCalledWith({
|
||||
resource: "posts",
|
||||
action: "list",
|
||||
params: expect.objectContaining({
|
||||
id: undefined,
|
||||
resource: expect.objectContaining({
|
||||
name: "posts",
|
||||
list: "/posts",
|
||||
}),
|
||||
}),
|
||||
queryOptions: undefined,
|
||||
});
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when pick resource is undefined", () => {
|
||||
it("should work without resource", async () => {
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
routerProvider: mockRouterProvider({
|
||||
id: undefined,
|
||||
action: "list",
|
||||
resource: undefined,
|
||||
}),
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(useCanSpy).toHaveBeenCalledWith({
|
||||
resource: undefined,
|
||||
action: "list",
|
||||
params: expect.objectContaining({
|
||||
id: undefined,
|
||||
resource: undefined,
|
||||
}),
|
||||
queryOptions: undefined,
|
||||
});
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when legacy router", () => {
|
||||
it("should deny access", async () => {
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
const { container, queryByText, findByText } = render(
|
||||
<CanAccess fallback={<p>Access Denied</p>}>
|
||||
<p>Accessible</p>
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
legacyRouterProvider: {
|
||||
...mockLegacyRouterProvider(),
|
||||
useParams: () =>
|
||||
({
|
||||
resource: "posts",
|
||||
id: undefined,
|
||||
action: "list",
|
||||
}) as any,
|
||||
},
|
||||
accessControlProvider: {
|
||||
can: async () => {
|
||||
return { can: false };
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
expect(useCanSpy).toHaveBeenCalledWith({
|
||||
resource: "posts",
|
||||
action: "list",
|
||||
params: expect.objectContaining({
|
||||
id: undefined,
|
||||
resource: expect.objectContaining({
|
||||
name: "posts",
|
||||
}),
|
||||
}),
|
||||
queryOptions: undefined,
|
||||
});
|
||||
|
||||
expect(queryByText("Accessible")).not.toBeInTheDocument();
|
||||
|
||||
await findByText("Access Denied");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect queryOptions from component prop", async () => {
|
||||
const onUnauthorized = jest.fn();
|
||||
|
||||
const { container, queryByText } = render(
|
||||
<CanAccess
|
||||
action="list"
|
||||
resource="posts"
|
||||
queryOptions={{ cacheTime: 10000 }}
|
||||
onUnauthorized={(args) => onUnauthorized(args)}
|
||||
>
|
||||
Accessible
|
||||
</CanAccess>,
|
||||
{
|
||||
wrapper: TestWrapper({
|
||||
accessControlProvider: {
|
||||
can: async () => ({
|
||||
can: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(queryByText("Accessible")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const useCanSpy = jest.spyOn(UseCanHook, "useCan");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useCanSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryOptions: expect.objectContaining({
|
||||
cacheTime: 10000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
118
packages/core/src/components/canAccess/index.tsx
Normal file
118
packages/core/src/components/canAccess/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useEffect } from "react";
|
||||
import type { UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { useCan, useResourceParams } from "@hooks";
|
||||
|
||||
import type { CanReturnType } from "../../contexts/accessControl/types";
|
||||
import type { BaseKey } from "../../contexts/data/types";
|
||||
import type { IResourceItem, ITreeMenu } from "../../contexts/resource/types";
|
||||
|
||||
type CanParams = {
|
||||
resource?: IResourceItem & { children?: ITreeMenu[] };
|
||||
id?: BaseKey;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type OnUnauthorizedProps = {
|
||||
resource?: string;
|
||||
reason?: string;
|
||||
action: string;
|
||||
params: CanParams;
|
||||
};
|
||||
|
||||
type CanAccessBaseProps = {
|
||||
/**
|
||||
* Resource name for API data interactions
|
||||
*/
|
||||
resource?: string;
|
||||
/**
|
||||
* Intended action on resource
|
||||
*/
|
||||
action: string;
|
||||
/**
|
||||
* Parameters associated with the resource
|
||||
* @type { resource?: [IResourceItem](https://refine.dev/docs/api-reference/core/interfaceReferences/#canparams), id?: [BaseKey](https://refine.dev/docs/api-reference/core/interfaceReferences/#basekey), [key: string]: any }
|
||||
*/
|
||||
params?: CanParams;
|
||||
/**
|
||||
* Content to show if access control returns `false`
|
||||
*/
|
||||
fallback?: React.ReactNode;
|
||||
/**
|
||||
* Callback function to be called if access control returns `can: false`
|
||||
*/
|
||||
onUnauthorized?: (props: OnUnauthorizedProps) => void;
|
||||
children: React.ReactNode;
|
||||
queryOptions?: UseQueryOptions<CanReturnType>;
|
||||
};
|
||||
|
||||
type CanAccessWithoutParamsProps = {
|
||||
[key in Exclude<
|
||||
keyof CanAccessBaseProps,
|
||||
"fallback" | "children"
|
||||
>]?: undefined;
|
||||
} & {
|
||||
[key in "fallback" | "children"]?: CanAccessBaseProps[key];
|
||||
};
|
||||
|
||||
export type CanAccessProps = CanAccessBaseProps | CanAccessWithoutParamsProps;
|
||||
|
||||
export const CanAccess: React.FC<CanAccessProps> = ({
|
||||
resource: resourceFromProp,
|
||||
action: actionFromProp,
|
||||
params: paramsFromProp,
|
||||
fallback,
|
||||
onUnauthorized,
|
||||
children,
|
||||
queryOptions: componentQueryOptions,
|
||||
...rest
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
resource,
|
||||
action: fallbackAction = "",
|
||||
} = useResourceParams({
|
||||
resource: resourceFromProp,
|
||||
id: paramsFromProp?.id,
|
||||
});
|
||||
|
||||
const action = actionFromProp ?? fallbackAction;
|
||||
|
||||
const params = paramsFromProp ?? {
|
||||
id,
|
||||
resource,
|
||||
};
|
||||
|
||||
const { data } = useCan({
|
||||
resource: resource?.name,
|
||||
action,
|
||||
params,
|
||||
queryOptions: componentQueryOptions,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onUnauthorized && data?.can === false) {
|
||||
onUnauthorized({
|
||||
resource: resource?.name,
|
||||
action,
|
||||
reason: data?.reason,
|
||||
params,
|
||||
});
|
||||
}
|
||||
}, [data?.can]);
|
||||
|
||||
if (data?.can) {
|
||||
if (React.isValidElement(children)) {
|
||||
const Children = React.cloneElement(children, rest);
|
||||
return Children;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
if (data?.can === false) {
|
||||
return <>{fallback ?? null}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user