diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue index 517af4cad9ddaf91dd2fab758786f5f6cf69b693..f49ac91dc92991c8b24815cf04b4e4bc6fcfb33c 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/editor_layout.vue @@ -16,7 +16,6 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue'; import DimDisableContainer from 'ee/security_orchestration/components/policy_editor/dim_disable_container.vue'; import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue'; -import { isGroup } from 'ee/security_orchestration/components/utils'; import { NAMESPACE_TYPES } from '../../constants'; import { POLICY_TYPE_COMPONENT_OPTIONS } from '../constants'; import { @@ -179,9 +178,6 @@ export default { initialValue: this.isInitiallyEnabled, }); }, - isGroupLevel() { - return isGroup(this.namespaceType); - }, deleteModalTitle() { return sprintf(s__('SecurityOrchestration|Delete policy: %{policy}'), { policy: this.policy.name, diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/action/action_section.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/action/action_section.vue index c9a7ee6a04e6e8169015df8e43a4517935269030..613d69a33f7fd629561aafb0e97d35c5efc854d1 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/action/action_section.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/action/action_section.vue @@ -1,8 +1,4 @@ <script> -import { debounce } from 'lodash'; -import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import Api from 'ee/api'; import { parseCustomFileConfiguration } from 'ee/security_orchestration/components/policy_editor/utils'; import getProjectId from 'ee/security_orchestration/graphql/queries/get_project_id.query.graphql'; import CodeBlockFilePath from '../../scan_execution/action/code_block_file_path.vue'; @@ -16,6 +12,11 @@ export default { type: Object, required: true, }, + doesFileExist: { + type: Boolean, + required: false, + default: true, + }, strategy: { type: String, required: true, @@ -23,7 +24,6 @@ export default { }, data() { return { - doesFileExist: true, selectedProject: undefined, }; }, @@ -38,23 +38,6 @@ export default { return this.ciConfigurationPath.ref; }, }, - watch: { - filePath() { - this.resetValidation(); - this.handleFileValidation(); - }, - selectedProject() { - this.resetValidation(); - this.handleFileValidation(); - }, - selectedRef() { - this.resetValidation(); - this.handleFileValidation(); - }, - }, - created() { - this.handleFileValidation = debounce(this.validateFilePath, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); - }, async mounted() { const { project: selectedProject } = parseCustomFileConfiguration(this.action.include?.[0]); @@ -62,13 +45,6 @@ export default { selectedProject.id = await this.getProjectId(selectedProject.fullPath); this.selectedProject = selectedProject; } - - if (!this.selectedProject) { - this.validateFilePath(); - } - }, - destroyed() { - this.handleFileValidation.cancel(); }, methods: { async getProjectId(fullPath) { @@ -80,16 +56,11 @@ export default { }, }); - return data.project?.id || ''; + return data?.project?.id || ''; } catch (e) { return ''; } }, - resetValidation() { - if (!this.doesFileExist) { - this.doesFileExist = true; - } - }, setStrategy(strategy) { this.$emit('changed', 'pipeline_config_strategy', strategy); }, @@ -117,29 +88,6 @@ export default { updatedFilePath(path) { this.setCiConfigurationPath({ ...this.ciConfigurationPath, file: path }); }, - async validateFilePath() { - const selectedProjectId = getIdFromGraphQLId(this.selectedProject?.id); - const ref = this.selectedRef || this.selectedProject?.repository?.rootRef; - - // For when the id is removed or when selectedProject is set to null temporarily above - if (!selectedProjectId) { - this.doesFileExist = false; - return; - } - - // For existing policies with existing project selected, rootRef will not be available - if (!ref) { - this.doesFileExist = true; - return; - } - - try { - await Api.getFile(selectedProjectId, this.filePath, { ref }); - this.doesFileExist = true; - } catch { - this.doesFileExist = false; - } - }, setCiConfigurationPath(pathConfig) { this.$emit('changed', 'content', { include: [pathConfig] }); }, diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue index 85414e618ee2541c088317e85e2c98d39cbc032b..a9cba01da6ff2521909c33da21464944c3407be0 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue @@ -1,7 +1,9 @@ <script> import { GlEmptyState } from '@gitlab/ui'; +import { debounce } from 'lodash'; import { joinPaths, setUrlFragment, visitUrl } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { ACTIONS_LABEL, EDITOR_MODE_RULE, @@ -10,7 +12,7 @@ import { PARSING_ERROR_MESSAGE, SECURITY_POLICY_ACTIONS, } from '../constants'; -import { assignSecurityPolicyProject, modifyPolicy } from '../utils'; +import { assignSecurityPolicyProject, doesFileExist, modifyPolicy } from '../utils'; import EditorLayout from '../editor_layout.vue'; import DimDisableContainer from '../dim_disable_container.vue'; import ActionSection from './action/action_section.vue'; @@ -77,6 +79,7 @@ export default { 'pipeline-execution-policy-editor', ), hasParsingError, + disableSubmit: false, isCreatingMR: false, isRemovingPolicy: false, mode: EDITOR_MODE_RULE, @@ -92,6 +95,32 @@ export default { strategy() { return this.policy.pipeline_config_strategy; }, + content() { + return this.policy?.content; + }, + disableSubmitButton() { + return this.disableSubmit || this.hasEmptyContent; + }, + hasEmptyContent() { + const { project, file } = this.policy?.content?.include?.[0] || {}; + return !project && !file; + }, + }, + watch: { + content(newVal) { + this.handleFileValidation(newVal); + }, + }, + mounted() { + if (this.existingPolicy) { + this.handleFileValidation(this.existingPolicy?.content); + } + }, + created() { + this.handleFileValidation = debounce(this.doesFileExist, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + }, + destroyed() { + this.handleFileValidation.cancel(); }, methods: { changeEditorMode(mode) { @@ -137,6 +166,21 @@ export default { this.setLoadingFlag(action, false); } }, + async doesFileExist(value) { + const { project, ref = null, file } = value?.include?.[0] || {}; + + try { + const exists = await doesFileExist({ + fullPath: project, + filePath: file, + ref, + }); + + this.disableSubmit = !exists; + } catch { + this.disableSubmit = true; + } + }, handleUpdateProperty(property, value) { this.policy[property] = value; this.updateYamlEditorValue(this.policy); @@ -177,6 +221,7 @@ export default { <editor-layout v-if="!disableScanPolicyUpdate" :custom-save-button-text="$options.i18n.createMergeRequest" + :disable-update="disableSubmitButton" :has-parsing-error="hasParsingError" :is-editing="isEditing" :is-removing-policy="isRemovingPolicy" @@ -217,6 +262,7 @@ export default { <action-section class="gl-mb-4 security-policies-bg-gray-10 gl-rounded-base gl-p-5" :action="policy.content" + :does-file-exist="!disableSubmit" :strategy="strategy" @changed="handleUpdateProperty" /> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js index 311f4c3187d7a65f76ba24486f4c6db59c46e176..29e8125da90480176d5a3eb2d1a909a216c31482 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js @@ -3,6 +3,7 @@ import { isValidCron } from 'cron-validator'; import { sprintf, s__ } from '~/locale'; import createPolicyProject from 'ee/security_orchestration/graphql/mutations/create_policy_project.mutation.graphql'; import createScanExecutionPolicy from 'ee/security_orchestration/graphql/mutations/create_scan_execution_policy.mutation.graphql'; +import getFile from 'ee/security_orchestration/graphql/queries/get_file.query.graphql'; import { gqClient } from 'ee/security_orchestration/utils'; import createMergeRequestMutation from '~/graphql_shared/mutations/create_merge_request.mutation.graphql'; @@ -547,3 +548,27 @@ export const parseError = (error) => { return []; } }; + +/** + * Check if file exist on particular branch in particular project + * @param fullPath full path to a project + * @param ref branch name + * @param filePath full file name + * @returns {Promise<boolean>} + */ +export const doesFileExist = async ({ fullPath = {}, ref = null, filePath = '' } = {}) => { + try { + const { data } = await gqClient.query({ + query: getFile, + variables: { + fullPath, + filePath, + ref, + }, + }); + + return data?.project?.repository?.blobs?.nodes?.length > 0; + } catch { + return false; + } +}; diff --git a/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_file.query.graphql b/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_file.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..8b86d14794ed73c34289be5694dee5e6011801d3 --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/graphql/queries/get_file.query.graphql @@ -0,0 +1,12 @@ +query getFile($fullPath: ID!, $filePath: String!, $ref: String) { + project(fullPath: $fullPath) { + id + repository { + blobs(paths: [$filePath], ref: $ref) { + nodes { + id + } + } + } + } +} diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/action/action_section_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/action/action_section_spec.js index 5e43172018a8a7398a1a15d276e8fb25df392569..dc78cdb07f3e0852f13aa1d5217a28b3c3f481d8 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/action/action_section_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/action/action_section_spec.js @@ -1,6 +1,5 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import Api from 'ee/api'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -76,6 +75,7 @@ describe('ActionSection', () => { expect(findCodeBlockFilePath().exists()).toBe(true); expect(requestHandler).toHaveBeenCalledWith({ fullPath }); + expect(findCodeBlockFilePath().props()).toEqual( expect.objectContaining({ filePath: '.pipeline-execution.yml', @@ -146,130 +146,13 @@ describe('ActionSection', () => { }); describe('file validation', () => { - beforeEach(() => { - jest.spyOn(Api, 'getFile').mockResolvedValue(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('no validation', () => { - it('does not validate on new linked file section', () => { - factory(); - expect(Api.getFile).not.toHaveBeenCalled(); - }); - - it('does not validate when ref is not selected', async () => { - factory({ propsData: { action: { include: [{ project: fullPath }] } } }); - await waitForPromises(); - expect(Api.getFile).not.toHaveBeenCalled(); - expect(requestHandler).toHaveBeenCalledWith({ fullPath }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - }); - - describe('existing selection', () => { - it('makes a call to validate the selection', async () => { - factory({ propsData: { action: { include: [{ project: fullPath, ref }] } } }); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledWith(projectId, undefined, { ref }); - }); - - it('succeeds validation', async () => { - factory({ propsData: { action: { include: [{ project: fullPath, ref }] } } }); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledTimes(1); - expect(requestHandler).toHaveBeenCalledWith({ fullPath }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - - it('fails validation', async () => { - jest.spyOn(Api, 'getFile').mockRejectedValue(); - factory({ propsData: { action: { include: [{ project: fullPath, ref: 'not-main' }] } } }); - await waitForPromises(); - expect(requestHandler).toHaveBeenCalledWith({ fullPath }); - expect(Api.getFile).toHaveBeenCalledTimes(1); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(false); - }); - }); - - describe('successful validation', () => { - describe('simple scenarios', () => { - beforeEach(async () => { - factory({ - propsData: { action: { include: [{ project: fullPath, ref, file: filePath }] } }, - }); - await waitForPromises(); - }); - - it('verifies on file path change', async () => { - expect(Api.getFile).toHaveBeenCalledTimes(1); - await wrapper.setProps({ action: { include: [{ ref, file: 'new-path' }] } }); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledTimes(2); - expect(Api.getFile).toHaveBeenLastCalledWith(projectId, 'new-path', { ref }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - - it('verifies on project change when ref is selected', async () => { - expect(Api.getFile).toHaveBeenCalledTimes(1); - await findCodeBlockFilePath().vm.$emit('select-project', project); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledTimes(2); - expect(Api.getFile).toHaveBeenLastCalledWith(projectId, filePath, { ref }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - - it('verifies on ref change', async () => { - expect(Api.getFile).toHaveBeenCalledTimes(1); - await wrapper.setProps({ action: { include: [{ ref: 'new-ref', file: filePath }] } }); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledTimes(2); - expect(Api.getFile).toHaveBeenLastCalledWith(projectId, filePath, { ref: 'new-ref' }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - }); - - describe('complex scenarios', () => { - it('verifies on project change when ref is not selected', async () => { - await factory({ - propsData: { action: { include: [{ project: fullPath, file: filePath }] } }, - }); - await findCodeBlockFilePath().vm.$emit('select-project', project); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledWith(projectId, filePath, { - ref: project.repository.rootRef, - }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); - }); - }); - }); - - describe('failed validation', () => { - it('fails when a file does not exists on a ref', async () => { - jest.spyOn(Api, 'getFile').mockRejectedValue(); - factory({ propsData: { action: { include: [{ project: fullPath, ref: 'not-main' }] } } }); - await wrapper.setProps({ action: { include: [{ ref: 'new-ref' }] } }); - await waitForPromises(); - expect(Api.getFile).toHaveBeenCalledTimes(1); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(false); - }); - - it('fails validation when a project is not selected', async () => { - await factory({ propsData: { action: { include: [{}] } } }); - expect(findCodeBlockFilePath().props('doesFileExist')).toBe(false); - }); - }); - describe('updating validation status', () => { it('updates a failed validation to a successful one', async () => { - jest.spyOn(Api, 'getFile').mockRejectedValue(); - factory({ propsData: { action: { include: [{ project: fullPath, ref }] } } }); + factory({ propsData: { doesFileExist: false } }); await waitForPromises(); expect(requestHandler).toHaveBeenCalledWith({ fullPath }); expect(findCodeBlockFilePath().props('doesFileExist')).toBe(false); - await wrapper.setProps({ action: { include: { ref: 'new-ref', file: 'new-path' } } }); + await wrapper.setProps({ doesFileExist: true }); expect(findCodeBlockFilePath().props('doesFileExist')).toBe(true); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js index 1feae8d24b220b2146b448a438d367d32ac100b5..4de8a91499ab220b8242425371885a7dd69924d5 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js @@ -9,7 +9,10 @@ import EditorLayout from 'ee/security_orchestration/components/policy_editor/edi import { DEFAULT_PIPELINE_EXECUTION_POLICY } from 'ee/security_orchestration/components/policy_editor/pipeline_execution/constants'; import { fromYaml } from 'ee/security_orchestration/components/policy_editor/pipeline_execution/utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import { modifyPolicy } from 'ee/security_orchestration/components/policy_editor/utils'; +import { + modifyPolicy, + doesFileExist, +} from 'ee/security_orchestration/components/policy_editor/utils'; import { EDITOR_MODE_YAML, SECURITY_POLICY_ACTIONS, @@ -20,6 +23,7 @@ import { NEW_POLICY_PROJECT, } from 'ee_jest/security_orchestration/mocks/mock_data'; import { + mockPipelineExecutionManifest, mockWithoutRefPipelineExecutionManifest, mockWithoutRefPipelineExecutionObject, } from 'ee_jest/security_orchestration/mocks/mock_pipeline_execution_policy_data'; @@ -36,6 +40,17 @@ jest.mock('ee/security_orchestration/components/policy_editor/utils', () => ({ fullPath: 'path/to/new-project', }), modifyPolicy: jest.fn().mockResolvedValue({ id: '2' }), + doesFileExist: jest.fn().mockResolvedValue({ + data: { + project: { + repository: { + blobs: { + nodes: [{ fileName: 'file ' }], + }, + }, + }, + }, + }), })); describe('EditorComponent', () => { @@ -226,4 +241,82 @@ describe('EditorComponent', () => { }); }); }); + + describe('action validation error', () => { + describe('no validation', () => { + it('does not validate on new linked file section', () => { + factory(); + expect(doesFileExist).toHaveBeenCalledTimes(0); + expect(findPolicyEditorLayout().props('disableUpdate')).toBe(true); + }); + }); + + describe('new policy', () => { + beforeEach(async () => { + factory(); + await findPolicyEditorLayout().vm.$emit('update-property', 'name', 'New name'); + }); + + it.each` + payload | expectedResult + ${{ include: [{ project: 'project-path' }] }} | ${{ filePath: undefined, fullPath: 'project-path', ref: null }} + ${{ include: [{ project: 'project-path', ref: 'main', file: 'file-name' }] }} | ${{ filePath: 'file-name', fullPath: 'project-path', ref: 'main' }} + `('makes a call to validate the selection', async ({ payload, expectedResult }) => { + expect(doesFileExist).toHaveBeenCalledTimes(0); + + await findActionSection().vm.$emit('set-ref', 'main'); + await findActionSection().vm.$emit('changed', 'content', payload); + + expect(doesFileExist).toHaveBeenCalledWith(expectedResult); + expect(findPolicyEditorLayout().props('disableUpdate')).toBe(false); + }); + + it('calls validation when switched to yaml mode', async () => { + await changesToYamlMode(); + + expect(doesFileExist).toHaveBeenCalledTimes(0); + + await findPolicyEditorLayout().vm.$emit('update-yaml', mockPipelineExecutionManifest); + + expect(doesFileExist).toHaveBeenCalledWith({ + filePath: 'test_path', + fullPath: 'gitlab-policies/js6', + ref: 'main', + }); + expect(findPolicyEditorLayout().props('disableUpdate')).toBe(false); + }); + }); + + describe('existing policy', () => { + beforeEach(() => { + mockWithoutRefPipelineExecutionObject.content.include[0].ref = 'main'; + factory({ + propsData: { + existingPolicy: { ...mockWithoutRefPipelineExecutionObject }, + }, + }); + }); + it('validates on existing policy initial state', () => { + expect(doesFileExist).toHaveBeenCalledWith({ + filePath: '.pipeline-execution.yml', + fullPath: 'GitLab.org/GitLab', + ref: 'main', + }); + }); + + it.each` + payload | expectedResult + ${{ include: [{ project: 'project-path' }] }} | ${{ filePath: undefined, fullPath: 'project-path', ref: null }} + ${{ include: [{ project: 'project-path', ref: 'main', file: 'file-name' }] }} | ${{ filePath: 'file-name', fullPath: 'project-path', ref: 'main' }} + `('makes a call to validate the selection', async ({ payload, expectedResult }) => { + expect(doesFileExist).toHaveBeenCalledTimes(1); + + await findActionSection().vm.$emit('set-ref', 'main'); + await findActionSection().vm.$emit('changed', 'content', payload); + + expect(doesFileExist).toHaveBeenCalledWith(expectedResult); + expect(findPolicyEditorLayout().props('disableUpdate')).toBe(false); + }); + }); + }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js index 7482f7552e82a822a5e987b289e697c0e1653fe5..74c1dbfd91af94cbc3d40abcba7e11c7c0870bfe 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js @@ -1,6 +1,7 @@ import { addIdsToPolicy, assignSecurityPolicyProject, + doesFileExist, getPolicyLimitDetails, modifyPolicy, createHumanizedScanners, @@ -81,6 +82,22 @@ const mockApolloResponses = (shouldReject) => { }; }; +const mockApolloQueryResponse = (nodes = []) => { + return () => { + return Promise.resolve({ + data: { + project: { + repository: { + blobs: { + nodes, + }, + }, + }, + }, + }); + }; +}; + describe('addIdsToPolicy', () => { it('adds ids to a policy with actions and rules', () => { expect(addIdsToPolicy({ actions: [{}], rules: [{}] })).toStrictEqual({ @@ -492,4 +509,34 @@ describe('mapBranchesToExceptions', () => { `('should check if branches has duplicates', ({ branches, output }) => { expect(mapBranchesToExceptions(branches)).toEqual(output); }); + + describe('doesFileExist', () => { + it.each` + files | expectedResult + ${[{ fileName: 'filePath' }]} | ${true} + ${[]} | ${false} + `('returns $expectedResult when file exist', async ({ files, expectedResult }) => { + gqClient.query.mockImplementation(mockApolloQueryResponse(files)); + const exists = await doesFileExist({ + filePath: 'filePath', + fullPath: 'fullPath', + ref: 'main', + }); + + expect(exists).toBe(expectedResult); + }); + + it('returns false when fullPath is not provides', async () => { + gqClient.query.mockImplementation(mockApolloQueryResponse()); + const exists = await doesFileExist({ fullPath: 'fullPath', ref: 'main' }); + + expect(exists).toBe(false); + }); + + it('fallbacks to false when request fails', async () => { + gqClient.query.mockRejectedValue({}); + const exists = await doesFileExist({ fullPath: 'fullPath', ref: 'main' }); + expect(exists).toBe(false); + }); + }); }); diff --git a/ee/spec/frontend/security_orchestration/mocks/mock_pipeline_execution_policy_data.js b/ee/spec/frontend/security_orchestration/mocks/mock_pipeline_execution_policy_data.js index e221f01c4da7f0a7719dcbfb25050b48ea903e7d..97513171adc4f23e1d65021846a5c0c90d2b6fb9 100644 --- a/ee/spec/frontend/security_orchestration/mocks/mock_pipeline_execution_policy_data.js +++ b/ee/spec/frontend/security_orchestration/mocks/mock_pipeline_execution_policy_data.js @@ -44,6 +44,7 @@ content: export const mockPipelineExecutionManifest = `type: pipeline_execution_policy name: Include external file description: This policy enforces pipeline execution with configuration from external file +pipeline_config_strategy: inject_ci enabled: false content: include: