diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx index beaedb9d9f..d75282fc5c 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx @@ -72,7 +72,7 @@ function CredentialPluginField(props) { )} {showPluginWizard && ( setShowPluginWizard(false)} onSubmit={val => { val.touched = true; diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.jsx index d8a66e8426..831291871e 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { func } from 'prop-types'; +import { func, shape } from 'prop-types'; import { Formik, useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -14,6 +14,7 @@ function CredentialPluginWizard({ i18n, handleSubmit, onClose }) { id: 1, name: i18n._(t`Credential`), component: , + enableNext: !!selectedCredential.value, }, { id: 2, @@ -58,8 +59,11 @@ function CredentialPluginPrompt({ i18n, onClose, onSubmit, initialValues }) { CredentialPluginPrompt.propTypes = { onClose: func.isRequired, onSubmit: func.isRequired, + initialValues: shape({}), }; -CredentialPluginPrompt.defaultProps = {}; +CredentialPluginPrompt.defaultProps = { + initialValues: {}, +}; export default withI18n()(CredentialPluginPrompt); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx index 095e203f38..2634301e07 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx @@ -1,18 +1,228 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../../testUtils/enzymeHelpers'; +import { CredentialsAPI, CredentialTypesAPI } from '../../../../../api'; +import selectedCredential from '../../data.cyberArkCredential.json'; +import azureVaultCredential from '../../data.azureVaultCredential.json'; +import hashiCorpCredential from '../../data.hashiCorpCredential.json'; import CredentialPluginPrompt from './CredentialPluginPrompt'; +jest.mock('../../../../../api/models/Credentials'); +jest.mock('../../../../../api/models/CredentialTypes'); + +CredentialsAPI.read.mockResolvedValue({ + data: { + count: 3, + results: [selectedCredential, azureVaultCredential, hashiCorpCredential], + }, +}); + +CredentialTypesAPI.readDetail.mockResolvedValue({ + data: { + id: 20, + type: 'credential_type', + url: '/api/v2/credential_types/20/', + related: { + named_url: + '/api/v2/credential_types/CyberArk Conjur Secret Lookup+external/', + credentials: '/api/v2/credential_types/20/credentials/', + activity_stream: '/api/v2/credential_types/20/activity_stream/', + }, + summary_fields: { user_capabilities: { edit: false, delete: false } }, + created: '2020-05-18T21:53:35.398260Z', + modified: '2020-05-18T21:54:05.451444Z', + name: 'CyberArk Conjur Secret Lookup', + description: '', + kind: 'external', + namespace: 'conjur', + managed_by_tower: true, + inputs: { + fields: [ + { id: 'url', label: 'Conjur URL', type: 'string', format: 'url' }, + { id: 'api_key', label: 'API Key', type: 'string', secret: true }, + { id: 'account', label: 'Account', type: 'string' }, + { id: 'username', label: 'Username', type: 'string' }, + { + id: 'cacert', + label: 'Public Key Certificate', + type: 'string', + multiline: true, + }, + ], + metadata: [ + { + id: 'secret_path', + label: 'Secret Identifier', + type: 'string', + help_text: 'The identifier for the secret e.g., /some/identifier', + }, + { + id: 'secret_version', + label: 'Secret Version', + type: 'string', + help_text: + 'Used to specify a specific secret version (if left empty, the latest version will be used).', + }, + ], + required: ['url', 'api_key', 'account', 'username'], + }, + injectors: {}, + }, +}); + describe('', () => { - let wrapper; - beforeAll(() => { - wrapper = mountWithContexts( - - ); + describe('Plugin not configured', () => { + let wrapper; + const onClose = jest.fn(); + const onSubmit = jest.fn(); + beforeAll(async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + }); + afterAll(() => { + wrapper.unmount(); + }); + test('should render Wizard with all steps', async () => { + const wizard = await waitForElement(wrapper, 'Wizard'); + const steps = wizard.prop('steps'); + + expect(steps).toHaveLength(2); + expect(steps[0].name).toEqual('Credential'); + expect(steps[1].name).toEqual('Metadata'); + }); + test('credentials step renders correctly', () => { + expect(wrapper.find('CredentialsStep').length).toBe(1); + expect(wrapper.find('DataListItem').length).toBe(3); + expect( + wrapper.find('Radio').filterWhere(radio => radio.isChecked).length + ).toBe(0); + }); + test('next button disabled until credential selected', () => { + expect(wrapper.find('Button[children="Next"]').prop('isDisabled')).toBe( + true + ); + }); + test('clicking cancel button calls correct function', () => { + wrapper.find('Button[children="Cancel"]').simulate('click'); + expect(onClose).toHaveBeenCalledTimes(1); + }); + test('clicking credential row enables next button', async () => { + await act(async () => { + wrapper + .find('Radio') + .at(0) + .invoke('onChange')(true); + }); + wrapper.update(); + expect( + wrapper + .find('Radio') + .at(0) + .prop('isChecked') + ).toBe(true); + expect(wrapper.find('Button[children="Next"]').prop('isDisabled')).toBe( + false + ); + }); + test('clicking next button shows metatdata step', async () => { + await act(async () => { + wrapper.find('Button[children="Next"]').simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('MetadataStep').length).toBe(1); + expect(wrapper.find('FormField').length).toBe(2); + }); + test('submit button calls correct function with parameters', async () => { + await act(async () => { + wrapper.find('input#credential-secret_path').simulate('change', { + target: { value: '/foo/bar', name: 'secret_path' }, + }); + }); + await act(async () => { + wrapper.find('input#credential-secret_version').simulate('change', { + target: { value: '9000', name: 'secret_version' }, + }); + }); + await act(async () => { + wrapper.find('Button[children="OK"]').simulate('click'); + }); + // expect(wrapper.debug()).toBe(false); + // wrapper.find('Button[children="OK"]').simulate('click'); + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + credential: selectedCredential, + secret_path: '/foo/bar', + secret_version: '9000', + }), + expect.anything() + ); + }); }); - afterAll(() => { - wrapper.unmount(); - }); - test('renders the expected content', () => { - expect(wrapper).toHaveLength(1); + + describe('Plugin already configured', () => { + let wrapper; + const onClose = jest.fn(); + const onSubmit = jest.fn(); + beforeAll(async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + }); + afterAll(() => { + wrapper.unmount(); + }); + test('should render Wizard with all steps', async () => { + const wizard = await waitForElement(wrapper, 'Wizard'); + const steps = wizard.prop('steps'); + + expect(steps).toHaveLength(2); + expect(steps[0].name).toEqual('Credential'); + expect(steps[1].name).toEqual('Metadata'); + }); + test('credentials step renders correctly', () => { + expect(wrapper.find('CredentialsStep').length).toBe(1); + expect(wrapper.find('DataListItem').length).toBe(3); + expect( + wrapper + .find('Radio') + .at(0) + .prop('isChecked') + ).toBe(true); + expect(wrapper.find('Button[children="Next"]').prop('isDisabled')).toBe( + false + ); + }); + test('metadata step renders correctly', async () => { + await act(async () => { + wrapper.find('Button[children="Next"]').simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('MetadataStep').length).toBe(1); + expect(wrapper.find('FormField').length).toBe(2); + expect(wrapper.find('input#credential-secret_path').prop('value')).toBe( + '/foo/bar' + ); + expect( + wrapper.find('input#credential-secret_version').prop('value') + ).toBe('9000'); + }); }); }); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx index 114f3136f6..c612fc4fdb 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx @@ -66,7 +66,7 @@ function MetadataStep({ i18n }) { }, [fetchMetadataOptions]); const testMetadata = () => { - // todo: implement + // https://github.com/ansible/awx/issues/7126 }; if (isLoading) { diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginSelected.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginSelected.jsx index a7d408d49f..d97a7a39df 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginSelected.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginSelected.jsx @@ -11,7 +11,6 @@ import { Credential } from '../../../../types'; const SelectedCredential = styled.div` display: flex; justify-content: space-between; - margin-top: 10px; background-color: white; border-bottom-color: var(--pf-global--BorderColor--200); `; @@ -20,6 +19,10 @@ const SpacedCredentialChip = styled(CredentialChip)` margin: 5px 8px; `; +const PluginHelpText = styled.p` + margin-top: 5px; +`; + function CredentialPluginSelected({ i18n, credential, @@ -28,12 +31,6 @@ function CredentialPluginSelected({ }) { return ( <> -

- - This field will be retrieved from an external secret management system - using the following credential: - -

+ + + This field will be retrieved from an external secret management system + using the specified credential. + + ); } diff --git a/awx/ui_next/src/screens/Credential/shared/data.azureVaultCredential.json b/awx/ui_next/src/screens/Credential/shared/data.azureVaultCredential.json new file mode 100644 index 0000000000..efedfe045d --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/data.azureVaultCredential.json @@ -0,0 +1,85 @@ +{ + "id": 12, + "type": "credential", + "url": "/api/v2/credentials/12/", + "related": { + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "activity_stream": "/api/v2/credentials/12/activity_stream/", + "access_list": "/api/v2/credentials/12/access_list/", + "object_roles": "/api/v2/credentials/12/object_roles/", + "owner_users": "/api/v2/credentials/12/owner_users/", + "owner_teams": "/api/v2/credentials/12/owner_teams/", + "copy": "/api/v2/credentials/12/copy/", + "input_sources": "/api/v2/credentials/12/input_sources/", + "credential_type": "/api/v2/credential_types/19/", + "user": "/api/v2/users/1/" + }, + "summary_fields": { + "credential_type": { + "id": 19, + "name": "Microsoft Azure Key Vault", + "description": "" + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the credential", + "name": "Admin", + "id": 60 + }, + "use_role": { + "description": "Can use the credential in a job template", + "name": "Use", + "id": 61 + }, + "read_role": { + "description": "May view settings for the credential", + "name": "Read", + "id": 62 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "copy": true, + "use": true + }, + "owners": [ + { + "id": 1, + "type": "user", + "name": "admin", + "description": " ", + "url": "/api/v2/users/1/" + } + ] + }, + "created": "2020-05-26T14:54:45.612847Z", + "modified": "2020-05-26T14:54:45.612861Z", + "name": "Microsoft Azure Key Vault", + "description": "", + "organization": null, + "credential_type": 19, + "inputs": { + "url": "https://localhost", + "client": "foo", + "secret": "$encrypted$", + "tenant": "9000", + "cloud_name": "AzureCloud" + }, + "kind": "azure_kv", + "cloud": false, + "kubernetes": false +} diff --git a/awx/ui_next/src/screens/Credential/shared/data.hashiCorpCredential.json b/awx/ui_next/src/screens/Credential/shared/data.hashiCorpCredential.json new file mode 100644 index 0000000000..426b622c58 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/data.hashiCorpCredential.json @@ -0,0 +1,82 @@ +{ + "id": 11, + "type": "credential", + "url": "/api/v2/credentials/11/", + "related": { + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "activity_stream": "/api/v2/credentials/11/activity_stream/", + "access_list": "/api/v2/credentials/11/access_list/", + "object_roles": "/api/v2/credentials/11/object_roles/", + "owner_users": "/api/v2/credentials/11/owner_users/", + "owner_teams": "/api/v2/credentials/11/owner_teams/", + "copy": "/api/v2/credentials/11/copy/", + "input_sources": "/api/v2/credentials/11/input_sources/", + "credential_type": "/api/v2/credential_types/21/", + "user": "/api/v2/users/1/" + }, + "summary_fields": { + "credential_type": { + "id": 21, + "name": "HashiCorp Vault Secret Lookup", + "description": "" + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the credential", + "name": "Admin", + "id": 57 + }, + "use_role": { + "description": "Can use the credential in a job template", + "name": "Use", + "id": 58 + }, + "read_role": { + "description": "May view settings for the credential", + "name": "Read", + "id": 59 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "copy": true, + "use": true + }, + "owners": [ + { + "id": 1, + "type": "user", + "name": "admin", + "description": " ", + "url": "/api/v2/users/1/" + } + ] + }, + "created": "2020-05-26T14:54:00.674404Z", + "modified": "2020-05-26T14:54:00.674418Z", + "name": "HashiCorp Vault Secret Lookup", + "description": "", + "organization": null, + "credential_type": 21, + "inputs": { + "url": "https://localhost", + "api_version": "v1" + }, + "kind": "hashivault_kv", + "cloud": false, + "kubernetes": false +}