Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CreateControllerProps,
useCreateController,
} from './useCreateController';
import { useAuthState } from '../../auth';

export default {
title: 'ra-core/controller/useCreateController',
Expand All @@ -39,6 +40,16 @@ const defaultDataProvider = fakeDataProvider(
process.env.NODE_ENV === 'development'
);

const PostList = () => {
useAuthState();
return (
<div style={styles.mainContainer}>
<div>List view</div>
<Link to="/posts/create">Create</Link>
</div>
);
};

const CreatePost = (props: Partial<CreateControllerProps>) => {
const params = useCreateController({
resource: 'posts',
Expand All @@ -47,6 +58,7 @@ const CreatePost = (props: Partial<CreateControllerProps>) => {
return (
<div style={styles.mainContainer}>
{params.isPending ? <p>Loading...</p> : <div>Create view</div>}
<Link to="/posts">List</Link>
</div>
);
};
Expand Down Expand Up @@ -93,16 +105,33 @@ export const DisableAuthentication = ({
dataProvider={dataProvider}
authProvider={authProvider}
>
<CoreAdminUI>
<CoreAdminUI accessDenied={AccessDenied}>
<Resource
name="posts"
list={<PostList />}
create={<CreatePost disableAuthentication />}
/>
</CoreAdminUI>
</CoreAdminContext>
</TestMemoryRouter>
);
};
DisableAuthentication.args = {
authProvider: undefined,
};
DisableAuthentication.argTypes = {
authProvider: {
options: ['default', 'canAccess'],
mapping: {
default: undefined,
canAccess: {
...defaultAuthProvider,
canAccess: () => Promise.resolve(false),
},
},
control: { type: 'inline-radio' },
},
};

export const CanAccess = ({
authProviderDelay = 300,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';

import {
AuthProvider,
CreateContextProvider,
DataProvider,
Form,
Expand All @@ -28,7 +29,10 @@ import {
import { CreateController } from './CreateController';

import { TestMemoryRouter } from '../../routing';
import { CanAccess } from './useCreateController.security.stories';
import {
CanAccess,
DisableAuthentication,
} from './useCreateController.security.stories';

describe('useCreateController', () => {
const defaultProps = {
Expand Down Expand Up @@ -70,8 +74,8 @@ describe('useCreateController', () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
// @ts-ignore
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});

Expand Down Expand Up @@ -224,8 +228,8 @@ describe('useCreateController', () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
// @ts-ignore
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});
const onSuccess = jest.fn();
Expand Down Expand Up @@ -262,8 +266,8 @@ describe('useCreateController', () => {
let saveCallback;
const dataProvider = testDataProvider({
getOne: () => Promise.resolve({ data: { id: 12 } } as any),
// @ts-ignore
create: (_, { data }) =>
// @ts-ignore
Promise.resolve({ data: { id: 123, ...data } }),
});
const onSuccess = jest.fn();
Expand Down Expand Up @@ -692,5 +696,39 @@ describe('useCreateController', () => {
await screen.findByText('Loading...');
await screen.findByText('Create view');
});

it('should not call checkAuth nor canAccess when disableAuthentication is true', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Create view');
expect(authProvider.checkAuth).not.toHaveBeenCalled();
expect(authProvider.canAccess).not.toHaveBeenCalled();
});

it('should not call checkAuth nor canAccess when disableAuthentication is true even if useAuthState was called before', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Create view');
fireEvent.click(await screen.findByText('List'));
await screen.findByText('List view');
fireEvent.click(await screen.findByText('Create'));
await screen.findByText('Create view');
expect(authProvider.checkAuth).toHaveBeenCalledTimes(1);
expect(authProvider.canAccess).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ export const useCreateController = <
const { isPending: isPendingCanAccess } = useRequireAccess<RecordType>({
action: 'create',
resource,
// If disableAuthentication is true then isPendingAuthenticated will always be true so this hook is disabled
enabled: !isPendingAuthenticated,
enabled: !disableAuthentication && !isPendingAuthenticated,
});
const { hasEdit, hasShow } = useResourceDefinition(props);
const finalRedirectTo =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Resource } from '../../core/Resource';
import { AuthProvider, DataProvider } from '../../types';
import { TestMemoryRouter } from '../../routing/TestMemoryRouter';
import { EditControllerProps, useEditController } from './useEditController';
import { useAuthState } from '../..';

export default {
title: 'ra-core/controller/useEditController',
Expand All @@ -36,6 +37,16 @@ const defaultDataProvider = fakeDataProvider(
process.env.NODE_ENV === 'development'
);

const PostList = () => {
useAuthState();
return (
<div style={styles.mainContainer}>
<div>List view</div>
<Link to="/posts/1">Edit</Link>
</div>
);
};

const Post = (props: Partial<EditControllerProps>) => {
const params = useEditController({
id: 1,
Expand All @@ -51,6 +62,7 @@ const Post = (props: Partial<EditControllerProps>) => {
{params.record.title} - {params.record.votes} votes
</div>
)}
<Link to="/posts">List</Link>
</div>
);
};
Expand Down Expand Up @@ -97,16 +109,33 @@ export const DisableAuthentication = ({
dataProvider={dataProvider}
authProvider={authProvider}
>
<CoreAdminUI>
<CoreAdminUI accessDenied={AccessDenied}>
<Resource
name="posts"
list={<PostList />}
edit={<Post disableAuthentication />}
/>
</CoreAdminUI>
</CoreAdminContext>
</TestMemoryRouter>
);
};
DisableAuthentication.args = {
authProvider: undefined,
};
DisableAuthentication.argTypes = {
authProvider: {
options: ['default', 'canAccess'],
mapping: {
default: undefined,
canAccess: {
...defaultAuthProvider,
canAccess: () => Promise.resolve(false),
},
},
control: { type: 'inline-radio' },
},
};

export const CanAccess = ({
authProviderDelay = 300,
Expand Down
34 changes: 34 additions & 0 deletions packages/ra-core/src/controller/edit/useEditController.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1295,5 +1295,39 @@ describe('useEditController', () => {
expect(dataProvider.getOne).toHaveBeenCalled();
expect(authProvider.checkAuth).not.toHaveBeenCalled();
});

it('should not call checkAuth nor canAccess when disableAuthentication is true', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Post #1 - 90 votes');
expect(authProvider.checkAuth).not.toHaveBeenCalled();
expect(authProvider.canAccess).not.toHaveBeenCalled();
});

it('should not call checkAuth nor canAccess when disableAuthentication is true even if useAuthState was called before', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Post #1 - 90 votes');
fireEvent.click(await screen.findByText('List'));
await screen.findByText('List view');
fireEvent.click(await screen.findByText('Edit'));
await screen.findByText('Post #1 - 90 votes');
expect(authProvider.checkAuth).toHaveBeenCalledTimes(1);
expect(authProvider.canAccess).not.toHaveBeenCalled();
});
});
});
3 changes: 1 addition & 2 deletions packages/ra-core/src/controller/edit/useEditController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ export const useEditController = <
const { isPending: isPendingCanAccess } = useRequireAccess<RecordType>({
action: 'edit',
resource,
// If disableAuthentication is true then isPendingAuthenticated will always be true so this hook is disabled
enabled: !isPendingAuthenticated,
enabled: !disableAuthentication && !isPendingAuthenticated,
});

const getRecordRepresentation = useGetRecordRepresentation(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,43 @@ describe('useInfiniteListController', () => {
);
await screen.findByText('A post - 0 votes');
expect(dataProvider.getList).toHaveBeenCalled();
// Only called once by NavigationToFirstResource
expect(authProvider.checkAuth).toHaveBeenCalledTimes(1);
expect(authProvider.checkAuth).not.toHaveBeenCalled();
});

it('should not call checkAuth nor canAccess when disableAuthentication is true', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Post #1 - 90 votes');
expect(authProvider.checkAuth).not.toHaveBeenCalled();
expect(authProvider.canAccess).not.toHaveBeenCalled();
});

it('should not call checkAuth nor canAccess when disableAuthentication is true even if useAuthState was called before', async () => {
const authProvider: AuthProvider = {
checkAuth: jest.fn().mockResolvedValue(true),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
canAccess: jest.fn().mockResolvedValue(false),
};
render(<DisableAuthentication authProvider={authProvider} />);
await screen.findByText('Post #1 - 90 votes');
fireEvent.click(await screen.findByText('Dashboard'));
await screen.findByText('Dashboard view');
fireEvent.click(await screen.findByText('List'));
await screen.findByText('Post #1 - 90 votes');
// checkAuth is called twice: once by RA (with different params)
// and once by our custom Dashboard component
expect(authProvider.checkAuth).toHaveBeenCalledTimes(2);
expect(authProvider.canAccess).not.toHaveBeenCalled();
});
});
});
Loading
Loading