diff --git a/awx/ui_next/src/screens/Application/Application/Application.test.jsx b/awx/ui_next/src/screens/Application/Application/Application.test.jsx index 8b8f8fba5bde..136c468075fb 100644 --- a/awx/ui_next/src/screens/Application/Application/Application.test.jsx +++ b/awx/ui_next/src/screens/Application/Application/Application.test.jsx @@ -58,6 +58,7 @@ describe('', () => { expect(ApplicationsAPI.readDetail).toBeCalledWith(1); }); test('should throw error', async () => { + ApplicationsAPI.readOptions.mockResolvedValue(options); ApplicationsAPI.readDetail.mockRejectedValue( new Error({ response: { @@ -70,7 +71,6 @@ describe('', () => { }, }) ); - ApplicationsAPI.readOptions.mockResolvedValue(options); await act(async () => { wrapper = mountWithContexts( {}} />); }); diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx index e72f93b68101..18268ba63a41 100644 --- a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.jsx @@ -1,11 +1,50 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; + import { Card, PageSection } from '@patternfly/react-core'; +import ApplicationForm from '../shared/ApplicationForm'; +import { ApplicationsAPI } from '../../../api'; +import { CardBody } from '../../../components/Card'; + +function ApplicationEdit({ + application, + authorizationOptions, + clientTypeOptions, +}) { + const history = useHistory(); + const { id } = useParams(); + const [submitError, setSubmitError] = useState(null); + + const handleSubmit = async ({ ...values }) => { + values.organization = values.organization.id; + try { + await ApplicationsAPI.update(id, values); + history.push(`/applications/${id}/details`); + } catch (err) { + setSubmitError(err); + } + }; -function ApplicationEdit() { + const handleCancel = () => { + history.push(`/applications/${id}/details`); + }; return ( - - Application Edit - + <> + + + + + + + + ); } export default ApplicationEdit; diff --git a/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.test.jsx b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.test.jsx new file mode 100644 index 000000000000..e3dd4b03ea41 --- /dev/null +++ b/awx/ui_next/src/screens/Application/ApplicationEdit/ApplicationEdit.test.jsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { createMemoryHistory } from 'history'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import { ApplicationsAPI } from '../../../api'; +import ApplicationEdit from './ApplicationEdit'; + +jest.mock('../../../api/models/Applications'); +jest.mock('../../../api/models/Organizations'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + history: () => ({ + location: '/applications/1/edit', + }), + useParams: () => ({ id: 1 }), +})); + +const authorizationOptions = [ + { + key: 'authorization-code', + label: 'Authorization code', + value: 'authorization-code', + }, + { + key: 'password', + label: 'Resource owner password-based', + value: 'password', + }, +]; + +const clientTypeOptions = [ + { key: 'confidential', label: 'Confidential', value: 'confidential' }, + { key: 'public', label: 'Public', value: 'public' }, +]; + +const application = { + id: 1, + type: 'o_auth2_application', + url: '/api/v2/applications/10/', + related: { + named_url: '/api/v2/applications/Alex++bar/', + tokens: '/api/v2/applications/10/tokens/', + activity_stream: '/api/v2/applications/10/activity_stream/', + }, + summary_fields: { + organization: { + id: 230, + name: 'bar', + description: + 'SaleNameBedPersonalityManagerWhileFinanceBreakToothPersoné­²', + }, + user_capabilities: { + edit: true, + delete: true, + }, + tokens: { + count: 0, + results: [], + }, + }, + created: '2020-06-11T17:54:33.983993Z', + modified: '2020-06-11T17:54:33.984039Z', + name: 'Alex', + description: '', + client_id: 'b1dmj8xzkbFm1ZQ27ygw2ZeE9I0AXqqeL74fiyk4', + client_secret: '************', + client_type: 'confidential', + redirect_uris: 'http://www.google.com', + authorization_grant_type: 'authorization-code', + skip_authorization: false, + organization: 230, +}; + +describe('', () => { + let wrapper; + test('should mount properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('ApplicationEdit').length).toBe(1); + }); + test('should cancel form properly', async () => { + const history = createMemoryHistory({ + initialEntries: ['/applications/1/edit'], + }); + + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + await act(async () => { + wrapper.find('Button[aria-label="Cancel"]').prop('onClick')(); + }); + expect(history.location.pathname).toBe('/applications/1/details'); + }); + }); + test('should throw error on submit', async () => { + const error = { + response: { + config: { + method: 'patch', + url: '/api/v2/applications/', + }, + data: { detail: 'An error occurred' }, + }, + }; + ApplicationsAPI.update.mockRejectedValue(error); + const history = createMemoryHistory({ + initialEntries: ['/applications/1/edit'], + }); + + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + await act(async () => { + wrapper.find('Formik').prop('onSubmit')({ + id: 1, + organization: { id: 4 }, + }); + }); + + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); + test('expect values to be updated and submitted properly', async () => { + const history = createMemoryHistory({ + initialEntries: ['/applications/1/edit'], + }); + + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + await act(async () => { + wrapper.find('input#name').simulate('change', { + target: { value: 'new foo', name: 'name' }, + }); + wrapper.find('input#description').simulate('change', { + target: { value: 'new bar', name: 'description' }, + }); + + wrapper.find('input#redirect_uris').simulate('change', { + target: { value: 'https://www.google.com', name: 'redirect_uris' }, + }); + wrapper.find('AnsibleSelect[name="client_type"]').prop('onChange')( + {}, + 'confidential' + ); + wrapper.find('OrganizationLookup').invoke('onChange')({ + id: 1, + name: 'organization', + }); + }); + + wrapper.update(); + expect(wrapper.find('input#name').prop('value')).toBe('new foo'); + expect(wrapper.find('input#description').prop('value')).toBe('new bar'); + expect(wrapper.find('Chip').length).toBe(1); + expect(wrapper.find('Chip').text()).toBe('organization'); + expect( + wrapper + .find('AnsibleSelect[name="authorization_grant_type"]') + .prop('value') + ).toBe('authorization-code'); + expect( + wrapper.find('AnsibleSelect[name="client_type"]').prop('value') + ).toBe('confidential'); + expect(wrapper.find('input#redirect_uris').prop('value')).toBe( + 'https://www.google.com' + ); + await act(async () => { + wrapper.find('Formik').prop('onSubmit')({ + authorization_grant_type: 'authorization-code', + client_type: 'confidential', + description: 'bar', + name: 'foo', + organization: { id: 1 }, + redirect_uris: 'http://www.google.com', + }); + }); + + expect(ApplicationsAPI.update).toBeCalledWith(1, { + authorization_grant_type: 'authorization-code', + client_type: 'confidential', + description: 'bar', + name: 'foo', + organization: 1, + redirect_uris: 'http://www.google.com', + }); + }); +}); diff --git a/awx/ui_next/src/screens/Application/Applications.jsx b/awx/ui_next/src/screens/Application/Applications.jsx index d25245095efc..1842e5bd89b5 100644 --- a/awx/ui_next/src/screens/Application/Applications.jsx +++ b/awx/ui_next/src/screens/Application/Applications.jsx @@ -19,7 +19,6 @@ function Applications({ i18n }) { if (!application) { return; } - setBreadcrumbConfig({ '/applications': i18n._(t`Applications`), '/applications/add': i18n._(t`Create New Application`), diff --git a/awx/ui_next/src/screens/Application/shared/ApplicationForm.test.jsx b/awx/ui_next/src/screens/Application/shared/ApplicationForm.test.jsx index f43fc878d238..e7e950e3f53e 100644 --- a/awx/ui_next/src/screens/Application/shared/ApplicationForm.test.jsx +++ b/awx/ui_next/src/screens/Application/shared/ApplicationForm.test.jsx @@ -178,4 +178,62 @@ describe(' { redirect_uris: 'http://www.google.com', }); }); + test('should render required on Redirect URIs', async () => { + OrganizationsAPI.read.mockResolvedValue({ + results: [{ id: 1 }, { id: 2 }], + }); + const onSubmit = jest.fn(); + const onCancel = jest.fn(); + const application = { + id: 1, + type: 'o_auth2_application', + url: '/api/v2/applications/10/', + related: { + named_url: '/api/v2/applications/Alex++bar/', + tokens: '/api/v2/applications/10/tokens/', + activity_stream: '/api/v2/applications/10/activity_stream/', + }, + summary_fields: { + organization: { + id: 230, + name: 'bar', + description: + 'SaleNameBedPersonalityManagerWhileFinanceBreakToothPersoné­²', + }, + user_capabilities: { + edit: true, + delete: true, + }, + tokens: { + count: 0, + results: [], + }, + }, + created: '2020-06-11T17:54:33.983993Z', + modified: '2020-06-11T17:54:33.984039Z', + name: 'Alex', + description: '', + client_id: 'b1dmj8xzkbFm1ZQ27ygw2ZeE9I0AXqqeL74fiyk4', + client_secret: '************', + client_type: 'confidential', + redirect_uris: 'http://www.google.com', + authorization_grant_type: 'authorization-code', + skip_authorization: false, + organization: 230, + }; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect( + wrapper.find('FormField[name="redirect_uris"]').prop('isRequired') + ).toBe(true); + }); });