diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 9bd033146b..eb944c3052 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -898,7 +898,7 @@ register( field_class=fields.CharField, allow_blank=True, default='', - label=_('GitHub OAuth2 Secret'), + label=_('GitHub Enterprise OAuth2 Secret'), help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise developer application.'), category=_('GitHub OAuth2'), category_slug='github-enterprise', @@ -952,7 +952,7 @@ register( field_class=fields.CharField, allow_blank=False, default='', - label=_('GitHub Enterprise URL'), + label=_('GitHub Enterprise Organization URL'), help_text=_('The URL for your Github Enteprise.'), category=_('GitHub Enterprise OAuth2'), category_slug='github-enterprise-org', @@ -963,7 +963,7 @@ register( field_class=fields.CharField, allow_blank=False, default='', - label=_('GitHub Enterprise API URL'), + label=_('GitHub Enterprise Organization API URL'), help_text=_('The API URL for your GitHub Enterprise.'), category=_('GitHub Enterprise OAuth2'), category_slug='github-enterprise-org', @@ -1052,8 +1052,8 @@ register( field_class=fields.CharField, allow_blank=False, default='', - label=_('GitHub Enterprise URL'), - help_text=_('The URL for your Github Enteprise.'), + label=_('GitHub Enterprise Team URL'), + help_text=_('The URL for your Github Enterprise.'), category=_('GitHub Enterprise OAuth2'), category_slug='github-enterprise-team', ) @@ -1063,7 +1063,7 @@ register( field_class=fields.CharField, allow_blank=False, default='', - label=_('GitHub Enterprise API URL'), + label=_('GitHub Enterprise Team API URL'), help_text=_('The API URL for your GitHub Enterprise.'), category=_('GitHub Enterprise OAuth2'), category_slug='github-enterprise-team', diff --git a/awx/ui_next/src/screens/Login/Login.jsx b/awx/ui_next/src/screens/Login/Login.jsx index cad87f1dfa..8887d7351f 100644 --- a/awx/ui_next/src/screens/Login/Login.jsx +++ b/awx/ui_next/src/screens/Login/Login.jsx @@ -165,6 +165,41 @@ function AWXLogin({ alt, i18n, isAuthenticated }) { ); } + if (authKey === 'github-enterprise') { + return ( + + + + + + ); + } + if (authKey === 'github-enterprise-org') { + return ( + + + + + + ); + } + if (authKey === 'github-enterprise-team') { + return ( + + + + + + ); + } if (authKey === 'google-oauth2') { return ( diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx index 03d92e5899..aa23ae58ce 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.jsx @@ -8,6 +8,9 @@ import GitHubDetail from './GitHubDetail'; import GitHubEdit from './GitHubEdit'; import GitHubOrgEdit from './GitHubOrgEdit'; import GitHubTeamEdit from './GitHubTeamEdit'; +import GitHubEnterpriseEdit from './GitHubEnterpriseEdit'; +import GitHubEnterpriseOrgEdit from './GitHubEnterpriseOrgEdit'; +import GitHubEnterpriseTeamEdit from './GitHubEnterpriseTeamEdit'; function GitHub({ i18n }) { const baseURL = '/settings/github'; @@ -40,6 +43,15 @@ function GitHub({ i18n }) { + + + + + + + + + diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx index 68572d6c35..12a2a95261 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHub.test.jsx @@ -48,6 +48,44 @@ describe('', () => { SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, }, }); + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost/url', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: 'https://localhost/apiurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: 'ent_key', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: {}, + }, + }); + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-org/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost/url', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: 'https://localhost/apiurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: 'ent_org_key', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: 'ent_org_name', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: {}, + }, + }); + SettingsAPI.readCategory.mockResolvedValueOnce({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-team/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost/url', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: 'https://localhost/apiurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: 'ent_team_key', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: 'ent_team_id', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: {}, + }, + }); }); afterEach(() => { diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx index 5dc76348fb..816ae229b8 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.jsx @@ -31,21 +31,33 @@ function GitHubDetail({ i18n }) { { data: gitHubDefault }, { data: gitHubOrganization }, { data: gitHubTeam }, + { data: gitHubEnterprise }, + { data: gitHubEnterpriseOrganization }, + { data: gitHubEnterpriseTeam }, ] = await Promise.all([ SettingsAPI.readCategory('github'), SettingsAPI.readCategory('github-org'), SettingsAPI.readCategory('github-team'), + SettingsAPI.readCategory('github-enterprise'), + SettingsAPI.readCategory('github-enterprise-org'), + SettingsAPI.readCategory('github-enterprise-team'), ]); return { default: gitHubDefault, organization: gitHubOrganization, team: gitHubTeam, + enterprise: gitHubEnterprise, + enterprise_organization: gitHubEnterpriseOrganization, + enterprise_team: gitHubEnterpriseTeam, }; }, []), { default: null, organization: null, team: null, + enterprise: null, + enterprise_organization: null, + enterprise_team: null, } ); @@ -79,6 +91,21 @@ function GitHubDetail({ i18n }) { link: `${baseURL}/team/details`, id: 2, }, + { + name: i18n._(t`GitHub Enterprise`), + link: `${baseURL}/enterprise/details`, + id: 3, + }, + { + name: i18n._(t`GitHub Enterprise Organization`), + link: `${baseURL}/enterprise_organization/details`, + id: 4, + }, + { + name: i18n._(t`GitHub Enterprise Team`), + link: `${baseURL}/enterprise_team/details`, + id: 5, + }, ]; if (!Object.keys(gitHubDetails).includes(category)) { diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx index 11db4b57f0..3d2551e3a3 100644 --- a/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubDetail/GitHubDetail.test.jsx @@ -51,6 +51,44 @@ const mockTeam = { SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP: {}, }, }; +const mockEnterprise = { + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost/enterpriseurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: 'https://localhost/enterpriseapi', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: 'foobar', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null, + }, +}; +const mockEnterpriseOrg = { + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-org/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost/orgurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: 'https://localhost/orgapi', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: 'foobar', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: 'foo', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null, + }, +}; +const mockEnterpriseTeam = { + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-team/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost/teamurl', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: 'https://localhost/teamapi', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: 'foobar', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: 'foo', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null, + }, +}; describe('', () => { describe('Default', () => { @@ -60,6 +98,9 @@ describe('', () => { SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); useRouteMatch.mockImplementation(() => ({ url: '/settings/github/default/details', path: '/settings/github/:category/details', @@ -90,6 +131,9 @@ describe('', () => { 'GitHub Default', 'GitHub Organization', 'GitHub Team', + 'GitHub Enterprise', + 'GitHub Enterprise Organization', + 'GitHub Enterprise Team', ]; wrapper.find('RoutedTabs li').forEach((tab, index) => { expect(tab.text()).toEqual(expectedTabs[index]); @@ -149,6 +193,9 @@ describe('', () => { SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); useRouteMatch.mockImplementation(() => ({ url: '/settings/github/organization/details', path: '/settings/github/:category/details', @@ -198,6 +245,9 @@ describe('', () => { SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); useRouteMatch.mockImplementation(() => ({ url: '/settings/github/team/details', path: '/settings/github/:category/details', @@ -236,6 +286,199 @@ describe('', () => { }); }); + describe('Enterprise', () => { + let wrapper; + + beforeAll(async () => { + SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); + SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); + useRouteMatch.mockImplementation(() => ({ + url: '/settings/github/enterprise/details', + path: '/settings/github/:category/details', + params: { category: 'enterprise' }, + })); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + test('should render expected details', () => { + assertDetail( + wrapper, + 'GitHub Enterprise OAuth2 Callback URL', + 'https://towerhost/sso/complete/github-enterprise/' + ); + assertDetail( + wrapper, + 'GitHub Enterprise URL', + 'https://localhost/enterpriseurl' + ); + assertDetail( + wrapper, + 'GitHub Enterprise API URL', + 'https://localhost/enterpriseapi' + ); + assertDetail(wrapper, 'GitHub Enterprise OAuth2 Key', 'foobar'); + assertDetail(wrapper, 'GitHub Enterprise OAuth2 Secret', 'Encrypted'); + assertVariableDetail( + wrapper, + 'GitHub Enterprise OAuth2 Organization Map', + '{}' + ); + assertVariableDetail(wrapper, 'GitHub Enterprise OAuth2 Team Map', '{}'); + }); + }); + + describe('Enterprise Org', () => { + let wrapper; + + beforeAll(async () => { + SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); + SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); + useRouteMatch.mockImplementation(() => ({ + url: '/settings/github/enterprise_organization/details', + path: '/settings/github/:category/details', + params: { category: 'enterprise_organization' }, + })); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + test('should render expected details', () => { + assertDetail( + wrapper, + 'GitHub Enterprise Organization OAuth2 Callback URL', + 'https://towerhost/sso/complete/github-enterprise-org/' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Organization URL', + 'https://localhost/orgurl' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Organization API URL', + 'https://localhost/orgapi' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Organization OAuth2 Key', + 'foobar' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Organization OAuth2 Secret', + 'Encrypted' + ); + assertDetail(wrapper, 'GitHub Enterprise Organization Name', 'foo'); + assertVariableDetail( + wrapper, + 'GitHub Enterprise Organization OAuth2 Organization Map', + '{}' + ); + assertVariableDetail( + wrapper, + 'GitHub Enterprise Organization OAuth2 Team Map', + '{}' + ); + }); + }); + + describe('Enterprise Team', () => { + let wrapper; + + beforeAll(async () => { + SettingsAPI.readCategory.mockResolvedValueOnce(mockDefault); + SettingsAPI.readCategory.mockResolvedValueOnce(mockOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockTeam); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterprise); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseOrg); + SettingsAPI.readCategory.mockResolvedValueOnce(mockEnterpriseTeam); + useRouteMatch.mockImplementation(() => ({ + url: '/settings/github/enterprise_team/details', + path: '/settings/github/:category/details', + params: { category: 'enterprise_team' }, + })); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + test('should render expected details', () => { + assertDetail( + wrapper, + 'GitHub Enterprise Team OAuth2 Callback URL', + 'https://towerhost/sso/complete/github-enterprise-team/' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Team URL', + 'https://localhost/teamurl' + ); + assertDetail( + wrapper, + 'GitHub Enterprise Team API URL', + 'https://localhost/teamapi' + ); + assertDetail(wrapper, 'GitHub Enterprise Team OAuth2 Key', 'foobar'); + assertDetail( + wrapper, + 'GitHub Enterprise Team OAuth2 Secret', + 'Encrypted' + ); + assertDetail(wrapper, 'GitHub Enterprise Team ID', 'foo'); + assertVariableDetail( + wrapper, + 'GitHub Enterprise Team OAuth2 Organization Map', + '{}' + ); + assertVariableDetail( + wrapper, + 'GitHub Enterprise Team OAuth2 Team Map', + '{}' + ); + }); + }); + describe('Redirect', () => { test('should render redirect when user navigates to erroneous category', async () => { let wrapper; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx new file mode 100644 index 0000000000..3d6ee60c3e --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.jsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx new file mode 100644 index 0000000000..f0bb46ab23 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/GitHubEnterpriseEdit.test.jsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseEdit from './GitHubEnterpriseEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise API URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Key"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Secret"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise OAuth2 Team Map"]') + .length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', + }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise/details' + ); + }); + + test('should navigate to github enterprise detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js new file mode 100644 index 0000000000..5b5377e423 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx new file mode 100644 index 0000000000..272b1866ff --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.jsx @@ -0,0 +1,157 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseOrgEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise-org'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise_organization/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise_organization/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseOrgEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx new file mode 100644 index 0000000000..278dd5d37e --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/GitHubEnterpriseOrgEdit.test.jsx @@ -0,0 +1,212 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseOrgEdit from './GitHubEnterpriseOrgEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-org/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise_organization/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseOrgEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization URL"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization API URL"]') + .length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Key"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Secret"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Organization Name"]') + .length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Organization OAuth2 Team Map"]' + ).length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', + }, + }); + wrapper + .find( + 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP' + ) + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise org detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_organization/details' + ); + }); + + test('should navigate to github enterprise org detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_organization/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js new file mode 100644 index 0000000000..e86b1f78f0 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseOrgEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseOrgEdit'; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx new file mode 100644 index 0000000000..d9b725dc13 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.jsx @@ -0,0 +1,157 @@ +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GitHubEnterpriseTeamEdit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { isLoading, error, request: fetchGithub, result: github } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('github-enterprise-team'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGithub(); + }, [fetchGithub]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/github/enterprise_team/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(github).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/github/enterprise_team/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); + + return ( + + {isLoading && } + {!isLoading && error && } + {!isLoading && github && ( + + {formik => ( +
+ + + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} +
+ )} +
+ ); +} + +export default GitHubEnterpriseTeamEdit; diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx new file mode 100644 index 0000000000..764c9fa377 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/GitHubEnterpriseTeamEdit.test.jsx @@ -0,0 +1,206 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import GitHubEnterpriseTeamEdit from './GitHubEnterpriseTeamEdit'; + +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL: + 'https://towerhost/sso/complete/github-enterprise-team/', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '$encrypted$', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null, + }, +}); + +describe('', () => { + let wrapper; + let history; + + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/github/enterprise_team/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + test('initially renders without crashing', () => { + expect(wrapper.find('GitHubEnterpriseTeamEdit').length).toBe(1); + }); + + test('should display expected form fields', async () => { + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team API URL"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Key"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Secret"]') + .length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team ID"]').length + ).toBe(1); + expect( + wrapper.find( + 'FormGroup[label="GitHub Enterprise Team OAuth2 Organization Map"]' + ).length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="GitHub Enterprise Team OAuth2 Team Map"]') + .length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find('input#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL') + .simulate('change', { + target: { + value: 'https://localhost', + name: 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', + }, + }); + wrapper + .find( + 'CodeMirrorInput#SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP' + ) + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL: 'https://localhost', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID: '', + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP: {}, + SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to github enterprise team detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_team/details' + ); + }); + + test('should navigate to github enterprise team detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/github/enterprise_team/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js new file mode 100644 index 0000000000..002e319910 --- /dev/null +++ b/awx/ui_next/src/screens/Setting/GitHub/GitHubEnterpriseTeamEdit/index.js @@ -0,0 +1 @@ +export { default } from './GitHubEnterpriseTeamEdit'; diff --git a/awx/ui_next/src/screens/Setting/Settings.jsx b/awx/ui_next/src/screens/Setting/Settings.jsx index a535384afa..8b9c2db334 100644 --- a/awx/ui_next/src/screens/Setting/Settings.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.jsx @@ -57,6 +57,17 @@ function Settings({ i18n }) { '/settings/github/team': i18n._(t`GitHub Team`), '/settings/github/team/details': i18n._(t`Details`), '/settings/github/team/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise': i18n._(t`GitHub Enterprise`), + '/settings/github/enterprise/details': i18n._(t`Details`), + '/settings/github/enterprise/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise_organization': i18n._( + t`GitHub Enterprise Organization` + ), + '/settings/github/enterprise_organization/details': i18n._(t`Details`), + '/settings/github/enterprise_organization/edit': i18n._(t`Edit Details`), + '/settings/github/enterprise_team': i18n._(t`GitHub Enterprise Team`), + '/settings/github/enterprise_team/details': i18n._(t`Details`), + '/settings/github/enterprise_team/edit': i18n._(t`Edit Details`), '/settings/google_oauth2': i18n._(t`Google OAuth2`), '/settings/google_oauth2/details': i18n._(t`Details`), '/settings/google_oauth2/edit': i18n._(t`Edit Details`),