diff options
Diffstat (limited to 'spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js')
-rw-r--r-- | spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js new file mode 100644 index 00000000000..51b902d97dc --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -0,0 +1,380 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; +import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import { + ADD_VARIABLE_ACTION, + AWS_ACCESS_KEY_ID, + EDIT_VARIABLE_ACTION, + EVENT_LABEL, + EVENT_ACTION, + ENVIRONMENT_SCOPE_LINK_TITLE, +} from '~/ci_variable_list/constants'; +import { mockVariablesWithScopes } from '../mocks'; +import ModalStub from '../stubs'; + +describe('Ci variable modal', () => { + let wrapper; + let trackingSpy; + + const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + + const defaultProvide = { + awsLogoSvgPath: '/logo', + awsTipCommandsLink: '/tips', + awsTipDeployLink: '/deploy', + awsTipLearnLink: '/learn-link', + containsVariableReferenceLink: '/reference', + environmentScopeLink: '/help/environments', + isProtectedByDefault: false, + maskedEnvironmentVariablesLink: '/variables-link', + maskableRegex, + protectedEnvironmentVariablesLink: '/protected-link', + }; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: [], + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + }; + + const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { + wrapper = mountFn(CiVariableModal, { + attachTo: document.body, + provide: { ...defaultProvide, ...provide }, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); + const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); + const findModal = () => wrapper.find(ModalStub); + const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); + const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); + const deleteVariableButton = () => + findModal() + .findAll(GlButton) + .wrappers.find((button) => button.props('variant') === 'danger'); + const findProtectedVariableCheckbox = () => + wrapper.findByTestId('ci-variable-protected-checkbox'); + const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); + const findValueField = () => wrapper.find('#ci-variable-value'); + const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); + const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Adding a variable', () => { + describe('when no key/value pair are present', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the submit button as disabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('when a key/value pair is present', () => { + beforeEach(() => { + createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } }); + }); + + it('shows the submit button as enabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + + describe('events', () => { + const [currentVariable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: currentVariable } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('Dispatches `add-variable` action on submit', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]); + }); + + it('Dispatches the `hideModal` event when dismissing', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + }); + }); + + describe('when protected by default', () => { + describe('when adding a new variable', () => { + beforeEach(() => { + createComponent({ provide: { isProtectedByDefault: true } }); + findModal().vm.$emit('shown'); + }); + + it('updates the protected value to true', () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeTruthy(); + }); + }); + + describe('when editing a variable', () => { + beforeEach(() => { + createComponent({ + provide: { isProtectedByDefault: false }, + props: { + selectedVariable: {}, + mode: EDIT_VARIABLE_ACTION, + }, + }); + findModal().vm.$emit('shown'); + }); + + it('keeps the value as false', async () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeUndefined(); + }); + }); + }); + + describe('Adding a new non-AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); + }); + + it('does not show AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); + }); + }); + + describe('Adding a new AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const AWSKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + }); + + it('shows AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + }); + }); + + describe('Reference warning when adding a variable', () => { + describe('with a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const variableWithDollarSign = { + ...variable, + value: 'valueWith$', + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variableWithDollarSign }, + }); + }); + + it(`renders the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(true); + }); + }); + + describe('without a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variable }, + }); + }); + + it(`does not render the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(false); + }); + }); + }); + + describe('Editing a variable', () => { + const [variable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('button text is Update variable when updating', () => { + expect(findAddorUpdateButton().text()).toBe('Update variable'); + }); + + it('Update variable button dispatches updateVariable with correct variable', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('update-variable')).toEqual([[variable]]); + }); + + it('Propagates the `hideModal` event', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + + it('dispatches `delete-variable` with correct variable to delete', () => { + deleteVariableButton().vm.$emit('click'); + expect(wrapper.emitted('delete-variable')).toEqual([[variable]]); + }); + }); + + describe('Environment scope', () => { + describe('when feature is available', () => { + it('renders the environment dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + }); + + it('renders a link to documentation on scopes', () => { + createComponent({ mountFn: mountExtended }); + + const link = findEnvScopeLink(); + + expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); + expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); + }); + }); + + describe('when feature is not available', () => { + it('disables the dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvScopeInput().attributes('readonly')).toBe('readonly'); + }); + }); + }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the mask state is invalid', () => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidMaskVariable = { + ...variable, + value: 'd:;', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidMaskVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findMaskedVariableCheckbox().trigger('click'); + }); + + it('disables the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + + it('sends the correct tracking event', () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: ';', + }); + }); + }); + + describe.each` + value | masked | eventSent | trackingErrorProperty + ${'secretValue'} | ${false} | ${0} | ${null} + ${'short'} | ${true} | ${0} | ${null} + ${'dollar$ign'} | ${false} | ${1} | ${'$'} + ${'dollar$ign'} | ${true} | ${1} | ${'$'} + ${'unsupported|char'} | ${true} | ${1} | ${'|'} + ${'unsupported|char'} | ${false} | ${0} | ${null} + `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidKeyVariable = { + ...variable, + value: '', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidKeyVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findValueField().vm.$emit('input', value); + if (masked) { + await findMaskedVariableCheckbox().trigger('click'); + } + }); + + it(`${ + eventSent > 0 ? 'sends the correct' : 'does not send the' + } variable validation tracking event with ${value}`, () => { + expect(trackingSpy).toHaveBeenCalledTimes(eventSent); + + if (eventSent > 0) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: trackingErrorProperty, + }); + } + }); + }); + + describe('when masked variable has acceptable value', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: '12345678', + masked: true, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validMaskandKeyVariable }, + }); + }); + + it('does not disable the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + }); +}); |