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;