fork refine

This commit is contained in:
Stefan Pejcic
2024-02-05 10:23:04 +01:00
parent 3fffde9a8f
commit 8496a83edb
3634 changed files with 715528 additions and 2 deletions

View File

@@ -0,0 +1,522 @@
import React from "react";
import { act, waitFor } from "@testing-library/react";
import {
MockJSONServer,
mockLegacyRouterProvider,
render,
TestWrapper,
} from "@test";
import { Authenticated } from "./";
import { AuthProvider } from "src/interfaces";
const legacyMockAuthProvider = {
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
getPermissions: () => Promise.resolve(["admin"]),
getUserIdentity: () => Promise.resolve(),
};
const mockReplace = jest.fn();
const mockLegacyRouter = {
...mockLegacyRouterProvider(),
useHistory: () => ({
replace: mockReplace,
}),
useLocation: () => ({
pathname: "/posts",
search: "",
}),
};
const mockAuthProvider: AuthProvider = {
login: () =>
Promise.resolve({
success: true,
}),
logout: () => Promise.resolve({ success: true }),
onError: () => Promise.resolve({}),
check: () => Promise.resolve({ authenticated: true }),
getPermissions: () => Promise.resolve(),
};
describe("v3LegacyAuthProviderCompatible Authenticated", () => {
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (typeof message !== "undefined") console.warn(message);
});
});
it("should render children successfully", async () => {
const { getByText } = render(
<Authenticated
key="should-render-children-legacy"
v3LegacyAuthProviderCompatible={true}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
legacyAuthProvider: legacyMockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await waitFor(() => getByText("Custom Authenticated"));
});
it("not authenticated test", async () => {
const { queryByText } = render(
<Authenticated
key="not-authenticated-legacy"
v3LegacyAuthProviderCompatible={true}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
legacyAuthProvider: {
...legacyMockAuthProvider,
checkAuth: () => Promise.reject(),
},
legacyRouterProvider: mockLegacyRouter,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await waitFor(() => {
expect(queryByText("Custom Authenticated")).toBeNull();
expect(mockReplace).toBeCalledTimes(1);
});
});
it("not authenticated fallback component test", async () => {
legacyMockAuthProvider.checkAuth = jest
.fn()
.mockImplementation(() => Promise.reject());
const { queryByText } = render(
<Authenticated
key="fallback-component-test"
fallback={<div>Error fallback</div>}
v3LegacyAuthProviderCompatible={true}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
legacyAuthProvider: legacyMockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Error fallback"));
});
});
it("loading test", async () => {
legacyMockAuthProvider.checkAuth = jest
.fn()
.mockImplementation(() => Promise.reject());
const { queryByText } = render(
<Authenticated
key="loading-test"
loading={<div>loading</div>}
v3LegacyAuthProviderCompatible={true}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
legacyAuthProvider: legacyMockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("loading"));
});
});
});
describe("Authenticated", () => {
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (typeof message !== "undefined") console.warn(message);
});
});
it("should render children successfully", async () => {
const { getByText } = render(
<Authenticated key="render-children-successfully">
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: mockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await waitFor(() => getByText("Custom Authenticated"));
});
it("not authenticated test", async () => {
const { queryByText } = render(
<Authenticated key="not-authenticated-test">
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: () => Promise.resolve({ authenticated: false }),
},
resources: [{ name: "posts", route: "posts" }],
legacyRouterProvider: mockLegacyRouter,
}),
},
);
await waitFor(() => {
expect(queryByText("Custom Authenticated")).toBeNull();
expect(mockReplace).toBeCalledTimes(1);
});
});
it("not authenticated fallback component test", async () => {
mockAuthProvider.check = jest.fn().mockImplementation(() =>
Promise.resolve({
authenticated: false,
error: new Error("Not authenticated"),
}),
);
const { queryByText } = render(
<Authenticated
key="not-authenticated-fallback-component-test"
fallback={<div>Error fallback</div>}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: mockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Error fallback"));
});
});
it("loading test", async () => {
const { queryByText } = render(
<Authenticated key="loading-test" loading={<div>loading</div>}>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: mockAuthProvider,
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("loading"));
});
});
it("should redirect to `/my-path` if not authenticated (authProvider's check)", async () => {
const mockGo = jest.fn();
const { queryByText } = render(
<Authenticated key="should-redirect-custom-provider-check">
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: async () => {
return {
authenticated: false,
redirectTo: "/my-path",
};
},
},
routerProvider: {
go: () => mockGo,
},
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Custom Authenticated")).toBeNull();
});
await waitFor(() =>
expect(mockGo).toBeCalledWith(
expect.objectContaining({ to: "/my-path", type: "replace" }),
),
);
});
it("should redirect to `/my-path` if not authenticated (`redirectOnFail` prop)", async () => {
const mockGo = jest.fn();
const { queryByText } = render(
<Authenticated
key="should-redirect-custom-on-fail"
redirectOnFail="/my-path"
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: async () => {
return {
authenticated: false,
redirectTo: "/other-path",
};
},
},
routerProvider: {
go: () => mockGo,
},
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Custom Authenticated")).toBeNull();
});
await waitFor(() =>
expect(mockGo).toBeCalledWith(
expect.objectContaining({ to: "/my-path", type: "replace" }),
),
);
});
it("should redirect to `/my-path` if not authenticated (navigate in fallback)", async () => {
const mockGo = jest.fn();
const NavigateComp = ({ to }: { to: string }) => {
React.useEffect(() => {
mockGo({ to, type: "replace" });
}, [to]);
return null;
};
const { queryByText } = render(
<Authenticated
key="should-redirect-fallback"
fallback={<NavigateComp to="/my-path" />}
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: async () => {
return {
authenticated: false,
redirectTo: "/other-path",
};
},
},
routerProvider: {
go: () => mockGo,
},
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Custom Authenticated")).toBeNull();
});
await waitFor(() =>
expect(mockGo).toBeCalledWith(
expect.objectContaining({ to: "/my-path", type: "replace" }),
),
);
});
it("should redirect to `/my-path?to=/dashboard?current=1&pageSize=2` if not authenticated (`redirectOnFail` with append query)", async () => {
const mockGo = jest.fn();
const currentQuery = {
current: 1,
pageSize: 2,
};
const currentPathname = "dashboard";
const { queryByText } = render(
<Authenticated
key="should-redirect-with-to"
redirectOnFail="/my-path"
appendCurrentPathToQuery
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: async () => {
return {
authenticated: false,
redirectTo: "/other-path",
};
},
},
routerProvider: {
go: () => {
return (config) => {
if (config.type === "path") {
const params = new URLSearchParams(
currentQuery as any,
);
return `/${config.to}?${params.toString()}`;
} else {
return mockGo(config);
}
};
},
parse: () => {
return () => ({
pathname: currentPathname,
params: currentQuery,
});
},
},
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Custom Authenticated")).toBeNull();
});
await waitFor(() =>
expect(mockGo).toBeCalledWith(
expect.objectContaining({
to: "/my-path",
query: {
to: "/dashboard?current=1&pageSize=2",
},
type: "replace",
}),
),
);
});
it("should redirect to `/my-path?to=/dashboard?current=1&pageSize=2` if not authenticated (authProvider's check with append query)", async () => {
const mockGo = jest.fn();
const currentQuery = {
current: 1,
pageSize: 2,
};
const currentPathname = "dashboard";
const { queryByText } = render(
<Authenticated
key="should-redirect-path-from-provider"
appendCurrentPathToQuery
>
Custom Authenticated
</Authenticated>,
{
wrapper: TestWrapper({
dataProvider: MockJSONServer,
authProvider: {
...mockAuthProvider,
check: async () => {
return {
authenticated: false,
redirectTo: "/my-path",
};
},
},
routerProvider: {
go: () => {
return (config) => {
if (config.type === "path") {
const params = new URLSearchParams(
currentQuery as any,
);
return `/${config.to}?${params.toString()}`;
} else {
return mockGo(config);
}
};
},
parse: () => {
return () => ({
pathname: currentPathname,
params: currentQuery,
});
},
},
resources: [{ name: "posts", route: "posts" }],
}),
},
);
await act(async () => {
expect(queryByText("Custom Authenticated")).toBeNull();
});
await waitFor(() =>
expect(mockGo).toBeCalledWith(
expect.objectContaining({
to: "/my-path",
query: {
to: "/dashboard?current=1&pageSize=2",
},
type: "replace",
}),
),
);
});
});

View File

@@ -0,0 +1,222 @@
import React from "react";
import {
useGo,
useNavigation,
useParsed,
useRouterContext,
useRouterType,
useIsAuthenticated,
} from "@hooks";
import { useActiveAuthProvider } from "@definitions/index";
import { GoConfig } from "src/interfaces";
export type AuthenticatedCommonProps = {
/**
* Unique key to identify the component.
* This is required if you have multiple `Authenticated` components at the same level.
* @required
*/
key: React.Key;
/**
* Whether to redirect user if not logged in or not.
* If not set, user will be redirected to `redirectTo` property of the `check` function's response.
* This behavior is only available for new auth providers.
* Legacy auth providers will redirect to `/login` by default if this property is not set.
* If set to a string, user will be redirected to that string.
*
* This property only works if `fallback` is **not set**.
*/
redirectOnFail?: string | true;
/**
* Whether to append current path to search params of the redirect url at `to` property.
*
* By default, `to` parameter is used by successful invocations of the `useLogin` hook.
* If `to` present, it will be used as the redirect url after successful login.
*/
appendCurrentPathToQuery?: boolean;
/**
* Content to show if user is not logged in.
*/
fallback?: React.ReactNode;
/**
* Content to show while checking whether user is logged in or not.
*/
loading?: React.ReactNode;
/**
* Content to show if user is logged in
*/
children?: React.ReactNode;
};
export type LegacyAuthenticatedProps = {
v3LegacyAuthProviderCompatible: true;
} & AuthenticatedCommonProps;
export type AuthenticatedProps = {
v3LegacyAuthProviderCompatible?: false;
} & AuthenticatedCommonProps;
/**
* `<Authenticated>` is the component form of {@link https://refine.dev/docs/api-reference/core/hooks/auth/useAuthenticated `useAuthenticated`}. It internally uses `useAuthenticated` to provide it's functionality.
*
* @requires {@link https://react.dev/learn/rendering-lists#why-does-react-need-keys `key`} prop if you have multiple components at the same level.
* In React, components don't automatically unmount and remount with prop changes, which is generally good for performance. However, for specific cases this can cause issues like unwanted content rendering (`fallback` or `children`). To solve this, assigning unique `key` values to each instance of component is necessary, forcing React to unmount and remount the component, rather than just updating its props.
* @example
*```tsx
* <Authenticated key="dashboard">
* <h1>Dashboard Page</h1>
* </Authenticated>
*```
*
* @see {@link https://refine.dev/docs/core/components/auth/authenticated `<Authenticated>`} component for more details.
*/
export function Authenticated(
props: LegacyAuthenticatedProps,
): JSX.Element | null;
/**
* `<Authenticated>` is the component form of {@link https://refine.dev/docs/api-reference/core/hooks/auth/useAuthenticated `useAuthenticated`}. It internally uses `useAuthenticated` to provide it's functionality.
*
* @requires {@link https://react.dev/learn/rendering-lists#why-does-react-need-keys `key`} prop if you have multiple components at the same level.
* In React, components don't automatically unmount and remount with prop changes, which is generally good for performance. However, for specific cases this can cause issues like unwanted content rendering (`fallback` or `children`). To solve this, assigning unique `key` values to each instance of component is necessary, forcing React to unmount and remount the component, rather than just updating its props.
* @example
*```tsx
* <Authenticated key="dashboard">
* <h1>Dashboard Page</h1>
* </Authenticated>
*```
*
* @see {@link https://refine.dev/docs/core/components/auth/authenticated `<Authenticated>`} component for more details.
*/
export function Authenticated(props: AuthenticatedProps): JSX.Element | null;
export function Authenticated({
redirectOnFail = true,
appendCurrentPathToQuery = true,
children,
fallback: fallbackContent,
loading: loadingContent,
}: AuthenticatedProps | LegacyAuthenticatedProps): JSX.Element | null {
const activeAuthProvider = useActiveAuthProvider();
const routerType = useRouterType();
const hasAuthProvider = Boolean(activeAuthProvider?.isProvided);
const isLegacyAuth = Boolean(activeAuthProvider?.isLegacy);
const isLegacyRouter = routerType === "legacy";
const parsed = useParsed();
const go = useGo();
const { useLocation } = useRouterContext();
const legacyLocation = useLocation();
const {
isFetching,
isSuccess,
data: {
authenticated: isAuthenticatedStatus,
redirectTo: authenticatedRedirect,
} = {},
} = useIsAuthenticated({
v3LegacyAuthProviderCompatible: isLegacyAuth,
});
// Authentication status
const isAuthenticated = hasAuthProvider
? isLegacyAuth
? isSuccess
: isAuthenticatedStatus
: true;
// when there is no auth provider
if (!hasAuthProvider) {
return <>{children ?? null}</>;
}
// when checking authentication status
if (isFetching) {
return <>{loadingContent ?? null}</>;
}
// when user is authenticated return children
if (isAuthenticated) {
return <>{children ?? null}</>;
}
// when user is not authenticated redirect or render fallbackContent
else {
// render fallbackContent if it is exist
if (typeof fallbackContent !== "undefined") {
return <>{fallbackContent ?? null}</>;
}
// if there is no fallbackContent, redirect page
// Redirect url to use. use redirectOnFail if it is set.
// Otherwise use redirectTo property of the check function's response.
// If both are not set, use `/login` as the default redirect url. (only for legacy auth providers)
const appliedRedirect = isLegacyAuth
? typeof redirectOnFail === "string"
? redirectOnFail
: "/login"
: typeof redirectOnFail === "string"
? redirectOnFail
: (authenticatedRedirect as string | undefined);
// Current pathname to append to the redirect url.
// User will be redirected to this url after successful mutation. (like login)
const pathname = `${
isLegacyRouter ? legacyLocation?.pathname : parsed.pathname
}`.replace(/(\?.*|#.*)$/, "");
// Redirect if appliedRedirect is set, otherwise return null.
// Uses `replace` for legacy router and `go` for new router.
if (appliedRedirect) {
if (isLegacyRouter) {
const toQuery = appendCurrentPathToQuery
? `?to=${encodeURIComponent(pathname)}`
: "";
return <RedirectLegacy to={`${appliedRedirect}${toQuery}`} />;
} else {
return (
<Redirect
config={{
to: appliedRedirect,
query: appendCurrentPathToQuery
? {
to: parsed.params?.to
? parsed.params.to
: go({
to: pathname,
options: { keepQuery: true },
type: "path",
}),
}
: undefined,
type: "replace",
}}
/>
);
}
}
return null;
}
}
const Redirect = ({ config }: { config: GoConfig }) => {
const go = useGo();
React.useEffect(() => {
go(config);
}, [go, config]);
return null;
};
const RedirectLegacy = ({ to }: { to: string }) => {
const { replace } = useNavigation();
React.useEffect(() => {
replace(to);
}, [replace, to]);
return null;
};