diff options
author | softwarefactory-project-zuul[bot] <33884098+softwarefactory-project-zuul[bot]@users.noreply.github.com> | 2020-04-20 22:11:29 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-20 22:11:29 +0200 |
commit | e9b254b9d20eee415a85b1c402bda639def01c3c (patch) | |
tree | f1ce5a62164f1faf08c0adad5b4cf0b08b8d56ed | |
parent | Merge pull request #6752 from fherbert/job_template_notification (diff) | |
parent | adds test for new webhook component (diff) | |
download | awx-e9b254b9d20eee415a85b1c402bda639def01c3c.tar.xz awx-e9b254b9d20eee415a85b1c402bda639def01c3c.zip |
Merge pull request #6654 from AlexSCorey/4962-EnableWebhooksForJT
Adds webhooks to Job template form
Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
11 files changed, 562 insertions, 3 deletions
diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 522240c733..0e2eba8079 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -87,6 +87,10 @@ class JobTemplates extends SchedulesMixin( readWebhookKey(id) { return this.http.get(`${this.baseUrl}${id}/webhook_key/`); } + + updateWebhookKey(id) { + return this.http.post(`${this.baseUrl}${id}/webhook_key/`); + } } export default JobTemplates; diff --git a/awx/ui_next/src/components/Lookup/index.js b/awx/ui_next/src/components/Lookup/index.js index cde48e2bcd..9321fb08e9 100644 --- a/awx/ui_next/src/components/Lookup/index.js +++ b/awx/ui_next/src/components/Lookup/index.js @@ -3,3 +3,4 @@ export { default as InstanceGroupsLookup } from './InstanceGroupsLookup'; export { default as InventoryLookup } from './InventoryLookup'; export { default as ProjectLookup } from './ProjectLookup'; export { default as MultiCredentialsLookup } from './MultiCredentialsLookup'; +export { default as CredentialLookup } from './CredentialLookup'; diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx index 201fdb2b3c..0b959320fa 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.jsx @@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom'; import { Card, PageSection } from '@patternfly/react-core'; import { CardBody } from '@components/Card'; import JobTemplateForm from '../shared/JobTemplateForm'; -import { JobTemplatesAPI } from '@api'; +import { JobTemplatesAPI, OrganizationsAPI } from '@api'; function JobTemplateAdd() { const [formSubmitError, setFormSubmitError] = useState(null); @@ -15,11 +15,13 @@ function JobTemplateAdd() { instanceGroups, initialInstanceGroups, credentials, + webhook_credential, ...remainingValues } = values; setFormSubmitError(null); remainingValues.project = remainingValues.project.id; + remainingValues.webhook_credential = webhook_credential?.id; try { const { data: { id, type }, @@ -36,6 +38,16 @@ function JobTemplateAdd() { } async function submitLabels(templateId, labels = [], orgId) { + if (!orgId) { + try { + const { + data: { results }, + } = await OrganizationsAPI.read(); + orgId = results[0].id; + } catch (err) { + throw err; + } + } const associationPromises = labels.map(label => JobTemplatesAPI.associateLabel(templateId, label, orgId) ); diff --git a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx index 504b815d87..dbc55e8580 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.jsx @@ -152,6 +152,10 @@ describe('<JobTemplateAdd />', () => { project: 2, playbook: 'Baz', inventory: 2, + webhook_credential: undefined, + webhook_key: '', + webhook_service: '', + webhook_url: '', }); }); diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx index 5b5c36cbcc..9848f53006 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.jsx @@ -100,11 +100,13 @@ class JobTemplateEdit extends Component { instanceGroups, initialInstanceGroups, credentials, + webhook_credential, ...remainingValues } = values; this.setState({ formSubmitError: null }); remainingValues.project = values.project.id; + remainingValues.webhook_credential = webhook_credential?.id || null; try { await JobTemplatesAPI.update(template.id, remainingValues); await Promise.all([ diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index 8c296b0630..8a8d7131fc 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -62,6 +62,12 @@ const mockJobTemplate = { type: 'job_template', use_fact_cache: false, verbosity: '0', + webhook_credential: null, + webhook_key: 'webhook Key', + webhook_service: 'gitlab', + related: { + webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/', + }, }; const mockRelatedCredentials = { @@ -245,6 +251,8 @@ describe('<JobTemplateEdit />', () => { delete expected.summary_fields; delete expected.id; delete expected.type; + delete expected.related; + expected.webhook_url = `${window.location.origin}${mockJobTemplate.related.webhook_receiver}`; expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, expected); expect(JobTemplatesAPI.disassociateLabel).toHaveBeenCalledTimes(2); expect(JobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(4); @@ -308,6 +316,12 @@ describe('<JobTemplateEdit />', () => { { id: 1, kind: 'cloud', name: 'Foo' }, { id: 2, kind: 'ssh', name: 'Bar' }, ], + webhook_credential: { + id: 7, + name: 'webhook credential', + kind: 'github_token', + credential_type_id: 12, + }, }, }; await act(async () => diff --git a/awx/ui_next/src/screens/Template/Template.jsx b/awx/ui_next/src/screens/Template/Template.jsx index c916500685..80e4df8539 100644 --- a/awx/ui_next/src/screens/Template/Template.jsx +++ b/awx/ui_next/src/screens/Template/Template.jsx @@ -45,6 +45,12 @@ function Template({ i18n, me, setBreadcrumb }) { role_level: 'notification_admin_role', }), ]); + if (data.webhook_service && data?.related?.webhook_key) { + const { + data: { webhook_key }, + } = await JobTemplatesAPI.readWebhookKey(templateId); + data.webhook_key = webhook_key; + } setBreadcrumb(data); return { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 37e40bee9a..0e96b8995d 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -40,6 +40,9 @@ import { import { JobTemplatesAPI, ProjectsAPI } from '@api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; +import WebhookSubForm from './WebhookSubForm'; + +const { origin } = document.location; function JobTemplateForm({ template, @@ -59,6 +62,10 @@ function JobTemplateForm({ Boolean(template?.host_config_key) ); + const [enableWebhooks, setEnableWebhooks] = useState( + Boolean(template.webhook_service) + ); + const { values: formikValues } = useFormikContext(); const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({ name: 'job_type', @@ -174,7 +181,6 @@ function JobTemplateForm({ ]; let callbackUrl; if (template?.related) { - const { origin } = document.location; const path = template.related.callback || `${template.url}callback`; callbackUrl = `${origin}${path}`; } @@ -498,6 +504,25 @@ function JobTemplateForm({ setAllowCallbacks(checked); }} /> + <Checkbox + aria-label={i18n._(t`Enable Webhook`)} + label={ + <span> + {i18n._(t`Enable Webhook`)} + + <FieldTooltip + content={i18n._( + t`Enable webhook for this workflow job template.` + )} + /> + </span> + } + id="wfjt-enabled-webhooks" + isChecked={enableWebhooks} + onChange={checked => { + setEnableWebhooks(checked); + }} + /> <CheckboxField id="option-concurrent" name="allow_simultaneous" @@ -516,6 +541,7 @@ function JobTemplateForm({ </FormCheckboxLayout> </FormGroup> </FormFullWidthLayout> + <WebhookSubForm enableWebhooks={enableWebhooks} /> {allowCallbacks && ( <> {callbackUrl && ( @@ -572,7 +598,7 @@ JobTemplateForm.defaultProps = { }; const FormikApp = withFormik({ - mapPropsToValues({ template = {} }) { + mapPropsToValues({ template = {}, i18n }) { const { summary_fields = { labels: { results: [] }, @@ -616,6 +642,14 @@ const FormikApp = withFormik({ instanceGroups: [], credentials: summary_fields.credentials || [], extra_vars: template.extra_vars || '---\n', + webhook_service: template.webhook_service || '', + webhook_url: template?.related?.webhook_receiver + ? `${origin}${template.related.webhook_receiver}` + : i18n._(t`a new webhook url will be generated on save.`).toUpperCase(), + webhook_key: + template.webhook_key || + i18n._(t`a new webhook key will be generated on save.`).toUpperCase(), + webhook_credential: template?.summary_fields?.webhook_credential || null, }; }, handleSubmit: async (values, { props, setErrors }) => { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx index 383bef39f0..bb2d8be6e2 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx @@ -2,6 +2,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { sleep } from '@testUtils/testUtils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import JobTemplateForm from './JobTemplateForm'; import { LabelsAPI, JobTemplatesAPI, ProjectsAPI, CredentialsAPI } from '@api'; @@ -34,6 +36,10 @@ describe('<JobTemplateForm />', () => { { id: 2, kind: 'ssh', name: 'Bar' }, ], }, + related: { webhook_receiver: '/api/v2/workflow_job_templates/57/gitlab/' }, + webhook_key: 'webhook key', + webhook_service: 'github', + webhook_credential: 7, }; const mockInstanceGroups = [ { @@ -86,6 +92,9 @@ describe('<JobTemplateForm />', () => { JobTemplatesAPI.readInstanceGroups.mockReturnValue({ data: { results: mockInstanceGroups }, }); + JobTemplatesAPI.updateWebhookKey.mockReturnValue({ + data: { webhook_key: 'webhook key' }, + }); ProjectsAPI.readPlaybooks.mockReturnValue({ data: ['debug.yml'], }); @@ -209,6 +218,123 @@ describe('<JobTemplateForm />', () => { ]); }); + test('webhooks and enable concurrent jobs functions properly', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/1/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + <Route + path="/templates/job_template/:id/edit" + component={() => ( + <JobTemplateForm + template={mockData} + handleSubmit={jest.fn()} + handleCancel={jest.fn()} + /> + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + }); + act(() => { + wrapper.find('Checkbox[aria-label="Enable Webhook"]').invoke('onChange')( + true, + { + currentTarget: { value: true, type: 'change', checked: true }, + } + ); + }); + wrapper.update(); + expect( + wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked') + ).toBe(true); + + expect( + wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly') + ).toBe(true); + expect( + wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('webhook key'); + await act(() => + wrapper.find('Button[aria-label="Update webhook key"]').prop('onClick')() + ); + expect(JobTemplatesAPI.updateWebhookKey).toBeCalledWith('1'); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/workflow_job_templates/57/gitlab/'); + + wrapper.update(); + + expect(wrapper.find('FormGroup[name="webhook_service"]').length).toBe(1); + + await act(async () => + wrapper.find('AnsibleSelect#webhook_service').prop('onChange')( + {}, + 'gitlab' + ) + ); + wrapper.update(); + + expect(wrapper.find('AnsibleSelect#webhook_service').prop('value')).toBe( + 'gitlab' + ); + }); + + test('webhooks should render properly, without data', async () => { + let wrapper; + const history = createMemoryHistory({ + initialEntries: ['/templates/job_template/1/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + <Route + path="/templates/job_template/:id/edit" + component={() => ( + <JobTemplateForm + template={{ + ...mockData, + webhook_credential: null, + webhook_key: '', + webhook_service: 'github', + related: { webhook_receiver: '' }, + }} + handleSubmit={jest.fn()} + handleCancel={jest.fn()} + /> + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + }); + expect( + wrapper.find('TextInputBase#template-webhook_key').prop('value') + ).toBe('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'); + expect( + wrapper.find('Button[aria-label="Update webhook key"]').prop('isDisabled') + ).toBe(true); + }); test('should call handleSubmit when Submit button is clicked', async () => { const handleSubmit = jest.fn(); let wrapper; diff --git a/awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx new file mode 100644 index 0000000000..7211990c90 --- /dev/null +++ b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.jsx @@ -0,0 +1,232 @@ +import React, { useEffect, useCallback } from 'react'; +import { SyncAltIcon } from '@patternfly/react-icons'; +import { useParams, useLocation } from 'react-router-dom'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { + FormGroup, + TextInput, + InputGroup, + Button, +} from '@patternfly/react-core'; +import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; +import useRequest from '@util/useRequest'; +import { useField } from 'formik'; +import { FormColumnLayout } from '@components/FormLayout'; +import { CredentialLookup } from '@components/Lookup'; +import AnsibleSelect from '@components/AnsibleSelect'; +import { FieldTooltip } from '@components/FormField'; +import { JobTemplatesAPI, CredentialTypesAPI } from '@api'; + +function WebhookSubForm({ i18n, enableWebhooks }) { + const { id, templateType } = useParams(); + const { pathname } = useLocation(); + + const { origin } = document.location; + + const [ + webhookServiceField, + webhookServiceMeta, + webhookServiceHelpers, + ] = useField('webhook_service'); + + const [webhookUrlField, webhookUrlMeta, webhookUrlHelpers] = useField( + 'webhook_url' + ); + const [webhookKeyField, webhookKeyMeta, webhookKeyHelpers] = useField( + 'webhook_key' + ); + const [ + webhookCredentialField, + webhookCredentialMeta, + webhookCredentialHelpers, + ] = useField('webhook_credential'); + + const { + request: loadCredentialType, + error, + isLoading, + result: credTypeId, + } = useRequest( + useCallback(async () => { + let results; + if (webhookServiceField.value) { + results = await CredentialTypesAPI.read({ + namespace: `${webhookServiceField.value}_token`, + }); + // TODO: Consider how to handle the situation where the results returns + // and empty array, or any of the other values is undefined or null (data, results, id) + } + return results?.data?.results[0]?.id; + }, [webhookServiceField.value]) + ); + + useEffect(() => { + loadCredentialType(); + }, [loadCredentialType]); + + useEffect(() => { + if (enableWebhooks) { + webhookServiceHelpers.setValue(webhookServiceMeta.initialValue); + webhookUrlHelpers.setValue(webhookUrlMeta.initialValue); + webhookKeyHelpers.setValue(webhookKeyMeta.initialValue); + webhookCredentialHelpers.setValue(webhookCredentialMeta.initialValue); + } else { + webhookServiceHelpers.setValue(''); + webhookUrlHelpers.setValue(''); + webhookKeyHelpers.setValue(''); + webhookCredentialHelpers.setValue(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enableWebhooks]); + + const { request: fetchWebhookKey, error: webhookKeyError } = useRequest( + useCallback(async () => { + const { + data: { webhook_key: key }, + } = await JobTemplatesAPI.updateWebhookKey(id); + webhookKeyHelpers.setValue(key); + }, [webhookKeyHelpers, id]) + ); + + const changeWebhookKey = async () => { + await fetchWebhookKey(); + }; + const isUpdateKeyDisabled = + pathname.endsWith('/add') || + webhookKeyMeta.initialValue === + 'A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'; + const webhookServiceOptions = [ + { + value: '', + key: '', + label: i18n._(t`Choose a Webhook Service`), + isDisabled: true, + }, + { + value: 'github', + key: 'github', + label: i18n._(t`GitHub`), + isDisabled: false, + }, + { + value: 'gitlab', + key: 'gitlab', + label: i18n._(t`GitLab`), + isDisabled: false, + }, + ]; + + if (error || webhookKeyError) { + return <ContentError error={error} />; + } + if (isLoading) { + return <ContentLoading />; + } + return ( + enableWebhooks && ( + <FormColumnLayout> + <FormGroup + name="webhook_service" + fieldId="webhook_service" + helperTextInvalid={webhookServiceMeta.error} + label={i18n._(t`Webhook Service`)} + > + <FieldTooltip content={i18n._(t`Select a webhook service.`)} /> + <AnsibleSelect + {...webhookServiceField} + id="webhook_service" + data={webhookServiceOptions} + onChange={(event, val) => { + webhookServiceHelpers.setValue(val); + webhookUrlHelpers.setValue( + pathname.endsWith('/add') + ? i18n + ._(t`a new webhook url will be generated on save.`) + .toUpperCase() + : `${origin}/api/v2/${templateType}s/${id}/${val}/` + ); + if (val === webhookServiceMeta.initialValue || val === '') { + webhookKeyHelpers.setValue(webhookKeyMeta.initialValue); + webhookCredentialHelpers.setValue( + webhookCredentialMeta.initialValue + ); + } else { + webhookKeyHelpers.setValue( + i18n + ._(t`a new webhook key will be generated on save.`) + .toUpperCase() + ); + webhookCredentialHelpers.setValue(null); + } + }} + /> + </FormGroup> + <> + <FormGroup + type="text" + fieldId="jt-webhookURL" + label={i18n._(t`Webhook URL`)} + name="webhook_url" + > + <FieldTooltip + content={i18n._( + t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.` + )} + /> + <TextInput + id="t-webhookURL" + aria-label={i18n._(t`Webhook URL`)} + value={webhookUrlField.value} + isReadOnly + /> + </FormGroup> + <FormGroup + label={i18n._(t`Webhook Key`)} + fieldId="template-webhook_key" + > + <FieldTooltip + content={i18n._( + t`Webhook services can use this as a shared secret.` + )} + /> + <InputGroup> + <TextInput + id="template-webhook_key" + isReadOnly + aria-label="wfjt-webhook-key" + value={webhookKeyField.value} + /> + <Button + isDisabled={isUpdateKeyDisabled} + variant="tertiary" + aria-label={i18n._(t`Update webhook key`)} + onClick={changeWebhookKey} + > + <SyncAltIcon /> + </Button> + </InputGroup> + </FormGroup> + </> + + {credTypeId && ( + <CredentialLookup + label={i18n._(t`Webhook Credential`)} + tooltip={i18n._( + t`Optionally select the credential to use to send status updates back to the webhook service.` + )} + credentialTypeId={credTypeId} + onChange={value => { + webhookCredentialHelpers.setValue(value || null); + }} + isValid={!webhookCredentialMeta.error} + helperTextInvalid={webhookCredentialMeta.error} + value={webhookCredentialField.value} + /> + )} + </FormColumnLayout> + ) + ); +} +export default withI18n()(WebhookSubForm); diff --git a/awx/ui_next/src/screens/Template/shared/WebhookSubForm.test.jsx b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.test.jsx new file mode 100644 index 0000000000..e23afb136b --- /dev/null +++ b/awx/ui_next/src/screens/Template/shared/WebhookSubForm.test.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { CredentialsAPI } from '@api'; +import { Formik } from 'formik'; + +import WebhookSubForm from './WebhookSubForm'; + +jest.mock('@api'); + +describe('<WebhooksSubForm />', () => { + let wrapper; + let history; + const initialValues = { + webhook_url: '/api/v2/job_templates/51/github/', + webhook_credential: { id: 1, name: 'Github credential' }, + webhook_service: 'github', + webhook_key: 'webhook key', + }; + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['templates/job_template/51/edit'], + }); + CredentialsAPI.read.mockResolvedValue({ + data: { results: [{ id: 12, name: 'Github credential' }] }, + }); + await act(async () => { + wrapper = mountWithContexts( + <Route path="templates/:templateType/:id/edit"> + <Formik initialValues={initialValues}> + <WebhookSubForm enableWebhooks /> + </Formik> + </Route>, + { + context: { + router: { + history, + route: { + location: { pathname: 'templates/job_template/51/edit' }, + match: { params: { id: 51, templateType: 'job_template' } }, + }, + }, + }, + } + ); + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('mounts properly', () => { + expect(wrapper.length).toBe(1); + }); + test('should render initial values properly', () => { + waitForElement(wrapper, 'Lookup__ChipHolder', el => el.lenth > 0); + expect(wrapper.find('AnsibleSelect').prop('value')).toBe('github'); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/job_templates/51/github/'); + expect( + wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('webhook key'); + expect( + wrapper + .find('Chip') + .find('span') + .text() + ).toBe('Github credential'); + }); + test('should make other credential type available', async () => { + CredentialsAPI.read.mockResolvedValue({ + data: { results: [{ id: 13, name: 'GitLab credential' }] }, + }); + await act(async () => + wrapper.find('AnsibleSelect').prop('onChange')({}, 'gitlab') + ); + expect(CredentialsAPI.read).toHaveBeenCalledWith({ + namespace: 'gitlab_token', + }); + wrapper.update(); + expect( + wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value') + ).toContain('/api/v2/job_templates/51/gitlab/'); + expect( + wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value') + ).toBe('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.'); + }); + test('should have disabled button to update webhook key', async () => { + let newWrapper; + await act(async () => { + newWrapper = mountWithContexts( + <Route path="templates/:templateType/:id/edit"> + <Formik + initialValues={{ + ...initialValues, + webhook_key: 'A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.', + }} + > + <WebhookSubForm enableWebhooks /> + </Formik> + </Route>, + { + context: { + router: { + history, + route: { + location: { pathname: 'templates/job_template/51/edit' }, + match: { params: { id: 51, templateType: 'job_template' } }, + }, + }, + }, + } + ); + }); + expect( + newWrapper + .find("Button[aria-label='Update webhook key']") + .prop('isDisabled') + ).toBe(true); + }); +}); |