import { act, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import expect from 'expect'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { AuthProvider, CreateContextProvider, DataProvider, Form, InputProps, useCreateController, useInput, } from '../..'; import { CoreAdminContext } from '../../core'; import { testDataProvider } from '../../dataProvider'; import { useNotificationContext } from '../../notification'; import { Middleware, SaveContextProvider, useRegisterMutationMiddleware, } from '../saveContext'; import { CreateController } from './CreateController'; import { TestMemoryRouter } from '../../routing'; import { CanAccess, DisableAuthentication, } from './useCreateController.security.stories'; describe('useCreateController', () => { const defaultProps = { hasCreate: true, hasEdit: true, hasList: true, hasShow: true, resource: 'posts', debounce: 200, }; it('should call the dataProvider.create() function on save', async () => { const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ), }); let saveCallback; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(dataProvider.create).toHaveBeenCalledWith('posts', { data: { foo: 'bar' }, }); }); it('should execute default success side effects on success', async () => { let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: (_, { data }) => // @ts-ignore Promise.resolve({ data: { id: 123, ...data } }), }); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(notificationsSpy).toEqual([ { message: 'resources.posts.notifications.created', type: 'info', notificationOptions: { messageArgs: { smart_count: 1, _: 'ra.notification.created', }, undoable: false, }, }, ]); }); it('should execute default failure side effects on failure', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: () => Promise.reject({ message: 'not good' }), }); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(notificationsSpy).toEqual([ { message: 'not good', type: 'error', notificationOptions: { messageArgs: { _: 'not good' } }, }, ]); }); it('should use the default error message in case no message was provided', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: () => Promise.reject({}), }); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(notificationsSpy).toEqual([ { message: 'ra.notification.http_error', type: 'error', notificationOptions: { messageArgs: { _: undefined } }, }, ]); }); it('should not trigger a notification in case of a validation error (handled by useNotifyIsFormInvalid)', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: () => Promise.reject({ body: { errors: { foo: 'invalid' } } }), }); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(notificationsSpy).toEqual([]); }); it('should allow mutationOptions to override the default success side effects', async () => { let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: (_, { data }) => // @ts-ignore Promise.resolve({ data: { id: 123, ...data } }), }); const onSuccess = jest.fn(); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); await waitFor(() => expect(onSuccess).toHaveBeenCalled()); expect(notificationsSpy).toEqual([]); }); it('should allow the save onSuccess option to override the success side effects override', async () => { let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: (_, { data }) => // @ts-ignore Promise.resolve({ data: { id: 123, ...data } }), }); const onSuccess = jest.fn(); const onSuccessSave = jest.fn(); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback( { foo: 'bar' }, { onSuccess: onSuccessSave, } ) ); expect(onSuccess).not.toHaveBeenCalled(); expect(onSuccessSave).toHaveBeenCalled(); expect(notificationsSpy).toEqual([]); }); it('should allow mutationOptions to override the default failure side effects', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: () => Promise.reject({ message: 'not good' }), }); const onError = jest.fn(); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); await waitFor(() => expect(onError).toHaveBeenCalled()); expect(notificationsSpy).toEqual([]); }); it('should accept meta in mutationOptions', async () => { let saveCallback; const create = jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ); const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create, }); render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar' }, meta: { lorem: 'ipsum' }, }); }); it('should accept meta as a save option', async () => { let saveCallback; const create = jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ); const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create, }); render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' }, { meta: { lorem: 'ipsum' } }) ); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar' }, meta: { lorem: 'ipsum' }, }); }); it('should allow the save onError option to override the failure side effects override', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); let saveCallback; const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create: () => Promise.reject({ message: 'not good' }), }); const onError = jest.fn(); const onErrorSave = jest.fn(); let notificationsSpy; const Notification = () => { const { notifications } = useNotificationContext(); React.useEffect(() => { notificationsSpy = notifications; }, [notifications]); return null; }; render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback( { foo: 'bar' }, { onError: onErrorSave, } ) ); expect(onError).not.toHaveBeenCalled(); expect(onErrorSave).toHaveBeenCalled(); expect(notificationsSpy).toEqual([]); }); it('should allow transform to transform the data before calling create', async () => { let saveCallback; const create = jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ); const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create, }); const transform = jest.fn().mockImplementationOnce(data => ({ ...data, transformed: true, })); render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(transform).toHaveBeenCalledWith({ foo: 'bar' }); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar', transformed: true }, }); }); it('should allow the save transform option to override the controller transform option', async () => { let saveCallback; const create = jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ); const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 12 } } as any), create, }); const transform = jest.fn(); const transformSave = jest.fn().mockImplementationOnce(data => ({ ...data, transformed: true, })); render( {({ save }) => { saveCallback = save; return null; }} ); await act(async () => saveCallback( { foo: 'bar' }, { transform: transformSave, } ) ); expect(transform).not.toHaveBeenCalled(); expect(transformSave).toHaveBeenCalledWith({ foo: 'bar' }); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar', transformed: true }, }); }); it('should allow to register middlewares', async () => { let saveCallback; const create = jest .fn() .mockImplementationOnce((_, { data }) => Promise.resolve({ data: { id: 123, ...data } }) ); const dataProvider = testDataProvider({ create, }); const middleware: Middleware = jest.fn( (resource, params, next) => { return next(resource, { ...params, meta: { addedByMiddleware: true }, }); } ); const Child = () => { useRegisterMutationMiddleware(middleware); return null; }; render( {({ save, saving, registerMutationMiddleware, unregisterMutationMiddleware, }) => { saveCallback = save; return ( ); }} ); await act(async () => saveCallback({ foo: 'bar' })); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar' }, meta: { addedByMiddleware: true }, }); expect(middleware).toHaveBeenCalledWith( 'posts', { data: { foo: 'bar' }, }, expect.any(Function) ); }); it('should return errors from the create call', async () => { const create = jest.fn().mockImplementationOnce(() => { return Promise.reject({ body: { errors: { foo: 'invalid' } } }); }); const dataProvider = { create, } as unknown as DataProvider; let saveCallback; render( {({ save }) => { saveCallback = save; return
; }} ); await new Promise(resolve => setTimeout(resolve, 10)); let errors; await act(async () => { errors = await saveCallback({ foo: 'bar' }); }); expect(errors).toEqual({ foo: 'invalid' }); expect(create).toHaveBeenCalledWith('posts', { data: { foo: 'bar' }, }); }); it('should allow custom redirect with warnWhenUnsavedChanges', async () => { const dataProvider = testDataProvider({ getOne: () => Promise.resolve({ data: { id: 123 } } as any), create: (_, { data }) => new Promise(resolve => setTimeout( // @ts-ignore () => resolve({ data: { id: 123, ...data } }), 300 ) ), }); const Input = (props: InputProps) => { const name = props.source; const { field } = useInput(props); return ( <> ); }; const CreateView = () => { const controllerProps = useCreateController({ ...defaultProps, redirect: 'show', }); return (
<>
Create
); }; const ShowView = () =>
Show
; render( } /> } /> ); await screen.findByText('Create'); fireEvent.change(screen.getByLabelText('foo'), { target: { value: 'bar' }, }); fireEvent.click(screen.getByText('Submit')); expect(await screen.findByText('Show')).not.toBeNull(); }); describe('Security', () => { it('should redirect to the /access-denied page when users do not have access', async () => { render(); await screen.findByText('List'); fireEvent.click(await screen.findByText('posts.create access')); fireEvent.click(await screen.findByText('Create')); await screen.findByText('Loading...'); await screen.findByText('Access denied'); }); it('should display the create view when users have access', async () => { render(); await screen.findByText('List'); fireEvent.click(await screen.findByText('Create')); 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(); 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(); 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(); }); }); });