From 1f93d3ad69b4927c7cf5867acb45db234aefed33 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 2 Mar 2021 14:27:48 -0500 Subject: [PATCH] Adds tests for workflow save error handling. Removes unnecessary code that was attempting to remove credentials from a new node. --- .../Visualizer.jsx | 20 +- .../Visualizer.test.jsx | 660 +++++++++++++++++- 2 files changed, 664 insertions(+), 16 deletions(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx index 1deb449890..5e0b8e2006 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx @@ -64,10 +64,10 @@ const getAggregatedCredentials = ( templateDefaultCred.credential_type === overrideCred.credential_type ) { if ( - (!templateDefaultCred.vault_id && !overrideCred.inputs.vault_id) || + (!templateDefaultCred.vault_id && !overrideCred.inputs?.vault_id) || (templateDefaultCred.vault_id && - overrideCred.inputs.vault_id && - templateDefaultCred.vault_id === overrideCred.inputs.vault_id) + overrideCred.inputs?.vault_id && + templateDefaultCred.vault_id === overrideCred.inputs?.vault_id) ) { credentialHasOverride = true; } @@ -405,16 +405,7 @@ function Visualizer({ template, i18n }) { failure_nodes: [], always_nodes: [], }; - if (node.promptValues?.removedCredentials?.length > 0) { - node.promptValues.removedCredentials.forEach(cred => { - disassociateCredentialRequests.push( - WorkflowJobTemplateNodesAPI.disassociateCredentials( - data.id, - cred.id - ) - ); - }); - } + if (node.promptValues?.addedCredentials?.length > 0) { node.promptValues.addedCredentials.forEach(cred => { associateCredentialRequests.push( @@ -583,8 +574,9 @@ function Visualizer({ template, i18n }) { {i18n._(t`There was an error saving the workflow.`)} diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx index 26770f16f9..c9a5795929 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx @@ -2,13 +2,37 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { + WorkflowApprovalTemplatesAPI, WorkflowJobTemplateNodesAPI, WorkflowJobTemplatesAPI, } from '../../../api'; import Visualizer from './Visualizer'; +import workflowReducer from '../../../components/Workflow/workflowReducer'; + +jest.mock('../../../components/Workflow/workflowReducer'); + +const realWorkflowReducer = jest.requireActual( + '../../../components/Workflow/workflowReducer' +).default; + jest.mock('../../../api'); +const startNode = { + id: 1, + fullUnifiedJobTemplate: { + name: 'START', + }, +}; + +const defaultLinks = [ + { + linkType: 'always', + source: { id: 1 }, + target: { id: 2 }, + }, +]; + const template = { id: 1, name: 'Foo WFJT', @@ -117,7 +141,6 @@ describe('Visualizer', () => { }); afterAll(() => { - jest.clearAllMocks(); wrapper.unmount(); delete window.SVGElement.prototype.getBBox; delete window.SVGElement.prototype.getBoundingClientRect; @@ -125,6 +148,12 @@ describe('Visualizer', () => { delete window.SVGElement.prototype.width; }); + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + workflowReducer.mockImplementation(realWorkflowReducer); + }); + test('Renders successfully', async () => { await act(async () => { wrapper = mountWithContexts( @@ -185,7 +214,7 @@ describe('Visualizer', () => { wrapper.find('button#link-confirm').simulate('click'); expect(wrapper.find('LinkEditModal').length).toBe(0); await act(async () => { - wrapper.find('button[aria-label="Save"]').simulate('click'); + wrapper.find('Button#visualizer-save').simulate('click'); }); expect( WorkflowJobTemplateNodesAPI.disassociateAlwaysNode @@ -219,6 +248,633 @@ describe('Visualizer', () => { ).toBe(true); }); + test('Error shown when saving fails due to node add error', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + fullUnifiedJobTemplate: { + id: 3, + name: 'PING', + type: 'job_template', + }, + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplatesAPI.createNode.mockRejectedValue(new Error()); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect(WorkflowJobTemplatesAPI.createNode).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to node edit error', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + isEdited: true, + fullUnifiedJobTemplate: { + id: 3, + name: 'PING', + type: 'job_template', + }, + originalNodeObject: { + id: 9000, + }, + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplateNodesAPI.replace.mockRejectedValue(new Error()); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect(WorkflowJobTemplateNodesAPI.replace).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to approval template add error', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + fullUnifiedJobTemplate: { + id: 3, + name: 'Approval', + timeout: 1000, + type: 'workflow_approval_template', + }, + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplatesAPI.createNode.mockResolvedValue({ + data: { + id: 9001, + }, + }); + WorkflowJobTemplateNodesAPI.createApprovalTemplate.mockRejectedValue( + new Error() + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect(WorkflowJobTemplatesAPI.createNode).toHaveBeenCalledTimes(1); + expect( + WorkflowJobTemplateNodesAPI.createApprovalTemplate + ).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to approval template edit error', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + isEdited: true, + fullUnifiedJobTemplate: { + id: 3, + name: 'Approval', + timeout: 1000, + type: 'workflow_approval_template', + }, + originalNodeObject: { + id: 9000, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + }, + }, + }, + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowApprovalTemplatesAPI.update.mockRejectedValue(new Error()); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect(WorkflowApprovalTemplatesAPI.update).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to node disassociate failure', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + fullUnifiedJobTemplate: { + id: 3, + name: 'Approval', + timeout: 1000, + type: 'workflow_approval_template', + }, + originalNodeObject: { + id: 9000, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + }, + }, + success_nodes: [], + failure_nodes: [3], + always_nodes: [], + }, + success_nodes: [3], + failure_nodes: [], + always_nodes: [], + }, + { + id: 3, + fullUnifiedJobTemplate: { + id: 4, + name: 'Approval 2', + timeout: 1000, + type: 'workflow_approval_template', + }, + originalNodeObject: { + id: 9001, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + }, + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + ]; + newState.links = [ + { + linkType: 'always', + source: { id: 1 }, + target: { id: 2 }, + }, + { + linkType: 'success', + source: { id: 2 }, + target: { id: 3 }, + }, + ]; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplateNodesAPI.disassociateFailuresNode.mockRejectedValue( + new Error() + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect( + WorkflowJobTemplateNodesAPI.disassociateFailuresNode + ).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to node associate failure', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + fullUnifiedJobTemplate: { + id: 3, + name: 'Approval', + timeout: 1000, + type: 'workflow_approval_template', + }, + originalNodeObject: { + id: 9000, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + }, + }, + success_nodes: [], + failure_nodes: [3], + always_nodes: [], + }, + success_nodes: [3], + failure_nodes: [], + always_nodes: [], + }, + { + id: 3, + fullUnifiedJobTemplate: { + id: 4, + name: 'Approval 2', + timeout: 1000, + type: 'workflow_approval_template', + }, + originalNodeObject: { + id: 9001, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + }, + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + ]; + newState.links = [ + { + linkType: 'always', + source: { id: 1 }, + target: { id: 2 }, + }, + { + linkType: 'success', + source: { id: 2 }, + target: { id: 3 }, + }, + ]; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplateNodesAPI.disassociateFailuresNode.mockResolvedValue(); + WorkflowJobTemplateNodesAPI.associateSuccessNode.mockRejectedValue( + new Error() + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect( + WorkflowJobTemplateNodesAPI.associateSuccessNode + ).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to credential disassociate failure', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + isEdited: true, + fullUnifiedJobTemplate: { + id: 3, + name: 'Ping', + type: 'job_template', + }, + originalNodeObject: { + id: 9000, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + originalNodeCredentials: [ + { + id: 456, + credential_type: 1, + }, + ], + promptValues: { + credentials: [ + { + id: 123, + credential_type: 1, + }, + ], + }, + launchConfig: { + defaults: { + credentials: [ + { + id: 456, + credential_type: 1, + }, + ], + }, + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplateNodesAPI.replace.mockResolvedValue(); + WorkflowJobTemplateNodesAPI.disassociateCredentials.mockRejectedValue( + new Error() + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect( + WorkflowJobTemplateNodesAPI.disassociateCredentials + ).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + + test('Error shown when saving fails due to credential associate failure', async () => { + workflowReducer.mockImplementation(state => { + const newState = { + ...state, + isLoading: false, + }; + + if (newState.nodes.length === 0) { + newState.nodes = [ + startNode, + { + id: 2, + isEdited: true, + fullUnifiedJobTemplate: { + id: 3, + name: 'Ping', + type: 'job_template', + }, + originalNodeObject: { + id: 9000, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + originalNodeCredentials: [ + { + id: 456, + credential_type: 1, + }, + ], + promptValues: { + credentials: [ + { + id: 123, + credential_type: 1, + }, + ], + }, + launchConfig: { + defaults: { + credentials: [ + { + id: 456, + credential_type: 1, + }, + ], + }, + }, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }, + ]; + newState.links = defaultLinks; + } + + return newState; + }); + WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({ + data: { + count: 0, + results: [], + }, + }); + WorkflowJobTemplateNodesAPI.replace.mockResolvedValue(); + WorkflowJobTemplateNodesAPI.disassociateCredentials.mockResolvedValue(); + WorkflowJobTemplateNodesAPI.associateCredentials.mockRejectedValue( + new Error() + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + wrapper.update(); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(0); + await act(async () => { + wrapper.find('Button#visualizer-save').simulate('click'); + }); + wrapper.update(); + expect( + WorkflowJobTemplateNodesAPI.associateCredentials + ).toHaveBeenCalledTimes(1); + expect( + wrapper.find('AlertModal[title="Error saving the workflow!"]').length + ).toBe(1); + }); + test('Error shown to user when error thrown fetching workflow nodes', async () => { WorkflowJobTemplatesAPI.readNodes.mockRejectedValue(new Error()); await act(async () => {