diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx index bdcd89398a..550c5caf6c 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx @@ -13,6 +13,8 @@ import CredentialPluginPrompt from './CredentialPluginPrompt'; jest.mock('../../../../../../api/models/Credentials'); jest.mock('../../../../../../api/models/CredentialTypes'); +CredentialsAPI.test.mockResolvedValue({}); + CredentialsAPI.read.mockResolvedValue({ data: { count: 3, @@ -234,5 +236,13 @@ describe('', () => { wrapper.find('input#credential-secret_version').prop('value') ).toBe('9000'); }); + test('clicking Test button makes correct call', async () => { + await act(async () => { + wrapper.find('Button#credential-plugin-test').simulate('click'); + }); + expect(CredentialsAPI.test).toHaveBeenCalledWith(1, { + metadata: { secret_path: '/foo/bar', secret_version: '9000' }, + }); + }); }); }); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx index 69dd105a5c..86e8f148c6 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx @@ -13,6 +13,7 @@ import FormField from '../../../../../../components/FormField'; import { FormFullWidthLayout } from '../../../../../../components/FormLayout'; import useRequest from '../../../../../../util/useRequest'; import { required } from '../../../../../../util/validators'; +import { CredentialPluginTestAlert } from '..'; const QuestionCircleIcon = styled(PFQuestionCircleIcon)` margin-left: 10px; @@ -27,6 +28,21 @@ function MetadataStep({ i18n }) { const [selectedCredential] = useField('credential'); const [inputValues] = useField('inputs'); + const { + result: testPluginSuccess, + error: testPluginError, + request: testPluginMetadata, + } = useRequest( + useCallback( + async (credential, metadata) => + CredentialsAPI.test(credential.id, { + metadata, + }), + [] + ), + null + ); + const { result: fields, error, @@ -65,10 +81,6 @@ function MetadataStep({ i18n }) { fetchMetadataOptions(); }, [fetchMetadataOptions]); - const testMetadata = () => { - // https://github.com/ansible/awx/issues/7126 - }; - if (isLoading) { return ; } @@ -143,13 +155,21 @@ function MetadataStep({ i18n }) { position="right" > testMetadata()} + onClick={() => + testPluginMetadata(selectedCredential.value, inputValues.value) + } > {i18n._(t`Test`)} + ); } diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.jsx new file mode 100644 index 0000000000..866ca1dae3 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.jsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Alert, + AlertActionCloseButton, + AlertGroup, +} from '@patternfly/react-core'; + +function CredentialPluginTestAlert({ + i18n, + credentialName, + successResponse, + errorResponse, +}) { + const [testMessage, setTestMessage] = useState(''); + const [testVariant, setTestVariant] = useState(false); + useEffect(() => { + if (errorResponse) { + if (errorResponse?.response?.data?.inputs) { + if (errorResponse.response.data.inputs.startsWith('HTTP')) { + const [ + errorCode, + errorStr, + ] = errorResponse.response.data.inputs.split('\n'); + try { + const errorJSON = JSON.parse(errorStr); + setTestMessage( + `${errorCode}${ + errorJSON?.errors[0] ? `: ${errorJSON.errors[0]}` : '' + }` + ); + } catch { + setTestMessage(errorResponse.response.data.inputs); + } + } else { + setTestMessage(errorResponse.response.data.inputs); + } + } else { + setTestMessage( + i18n._( + t`Something went wrong with the request to test this credential and metadata.` + ) + ); + } + setTestVariant('danger'); + } else if (successResponse) { + setTestMessage(i18n._(t`Test passed`)); + setTestVariant('success'); + } + }, [i18n, successResponse, errorResponse]); + + return ( + + {testMessage && testVariant && ( + + {credentialName} +

{testMessage}

+ + } + variant={testVariant} + action={ + { + setTestMessage(null); + setTestVariant(null); + }} + /> + } + /> + )} +
+ ); +} + +CredentialPluginTestAlert.propTypes = {}; + +CredentialPluginTestAlert.defaultProps = {}; + +export default withI18n()(CredentialPluginTestAlert); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.test.jsx new file mode 100644 index 0000000000..3934c866e6 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginTestAlert.test.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import CredentialPluginTestAlert from './CredentialPluginTestAlert'; + +describe('', () => { + let wrapper; + afterEach(() => { + wrapper.unmount(); + }); + test('renders expected content when test is successful', () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar'); + expect(wrapper.find('p#credential-plugin-test-message').text()).toBe( + 'Test passed' + ); + }); + test('renders expected content when test fails with the expected return string formatting', () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar'); + expect(wrapper.find('p#credential-plugin-test-message').text()).toBe( + 'HTTP 404: not found' + ); + }); + test('renders expected content when test fails without the expected return string formatting', () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar'); + expect(wrapper.find('p#credential-plugin-test-message').text()).toBe( + 'usernamee is not present at /secret/foo/bar/baz' + ); + }); +}); diff --git a/awx/ui_next/src/util/useRequest.js b/awx/ui_next/src/util/useRequest.js index 027e82f86f..764297941a 100644 --- a/awx/ui_next/src/util/useRequest.js +++ b/awx/ui_next/src/util/useRequest.js @@ -39,6 +39,7 @@ export default function useRequest(makeRequest, initialValue) { async (...args) => { setIsLoading(true); if (isMounted.current) { + setResult(initialValue); setError(null); } try { @@ -56,6 +57,7 @@ export default function useRequest(makeRequest, initialValue) { } } }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [makeRequest] ), setValue: setResult, diff --git a/awx/ui_next/src/util/useRequest.test.jsx b/awx/ui_next/src/util/useRequest.test.jsx index d0b1072d26..369a7ee07a 100644 --- a/awx/ui_next/src/util/useRequest.test.jsx +++ b/awx/ui_next/src/util/useRequest.test.jsx @@ -96,6 +96,37 @@ describe('useRequest hooks', () => { expect(wrapper.find('TestInner').prop('error')).toEqual(error); }); + test('should reset error/result on each request', async () => { + const error = new Error('error'); + const makeRequest = throwError => { + if (throwError) { + throw error; + } + + return { data: 'foo' }; + }; + const wrapper = mount(); + + await act(async () => { + wrapper.find('TestInner').invoke('request')(true); + }); + wrapper.update(); + expect(wrapper.find('TestInner').prop('result')).toEqual({}); + expect(wrapper.find('TestInner').prop('error')).toEqual(error); + await act(async () => { + wrapper.find('TestInner').invoke('request')(); + }); + wrapper.update(); + expect(wrapper.find('TestInner').prop('result')).toEqual({ data: 'foo' }); + expect(wrapper.find('TestInner').prop('error')).toEqual(null); + await act(async () => { + wrapper.find('TestInner').invoke('request')(true); + }); + wrapper.update(); + expect(wrapper.find('TestInner').prop('result')).toEqual({}); + expect(wrapper.find('TestInner').prop('error')).toEqual(error); + }); + test('should not update state after unmount', async () => { const makeRequest = jest.fn(); let resolve;