diff options
Diffstat (limited to 'spec/frontend/ci_variable_list')
7 files changed, 554 insertions, 7 deletions
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js new file mode 100644 index 00000000000..93b185bd242 --- /dev/null +++ b/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js @@ -0,0 +1,203 @@ +import $ from 'jquery'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; + +const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables'; +const HIDE_CLASS = 'hide'; + +describe('AjaxFormVariableList', () => { + preloadFixtures('projects/ci_cd_settings.html'); + preloadFixtures('projects/ci_cd_settings_with_variables.html'); + + let container; + let saveButton; + let errorBox; + + let mock; + let ajaxVariableList; + + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html'); + container = document.querySelector('.js-ci-variable-list-section'); + + mock = new MockAdapter(axios); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + maskableRegex: container.dataset.maskableRegex, + }); + + jest.spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables'); + jest.spyOn(ajaxVariableList.variableList, 'toggleEnableRow'); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('onSaveClicked', () => { + it('shows loading spinner while waiting for the request', () => { + const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon'); + + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false); + + return [200, {}]; + }); + + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); + }); + }); + + it('calls `updateRowsWithPersistedVariables` with the persisted variables', () => { + const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }]; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, { + variables: variablesResponse, + }); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(ajaxVariableList.updateRowsWithPersistedVariables).toHaveBeenCalledWith( + variablesResponse, + ); + }); + }); + + it('hides any previous error box', () => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + }); + }); + + it('disables remove buttons while waiting for the request', () => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false); + + return [200, {}]; + }); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true); + }); + }); + + it('hides secret values', () => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {}); + + const row = container.querySelector('.js-row'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + const valuePlaceholder = row.querySelector('.js-secret-value-placeholder'); + + valueInput.value = 'bar'; + $(valueInput).trigger('input'); + + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true); + }); + }); + + it('shows error box with validation errors', () => { + const validationError = 'some validation error'; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [validationError]); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false); + expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual( + `Validation failed ${validationError}`, + ); + }); + }); + + it('shows flash message when request fails', () => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); + + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + + return ajaxVariableList.onSaveClicked().then(() => { + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); + }); + }); + }); + + describe('updateRowsWithPersistedVariables', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings_with_variables.html'); + container = document.querySelector('.js-ci-variable-list-section'); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + }); + }); + + it('removes variable that was removed', () => { + expect(container.querySelectorAll('.js-row').length).toBe(3); + + container.querySelector('.js-row-remove-button').click(); + + expect(container.querySelectorAll('.js-row').length).toBe(3); + + ajaxVariableList.updateRowsWithPersistedVariables([]); + + expect(container.querySelectorAll('.js-row').length).toBe(2); + }); + + it('updates new variable row with persisted ID', () => { + const row = container.querySelector('.js-row:last-child'); + const idInput = row.querySelector('.js-ci-variable-input-id'); + const keyInput = row.querySelector('.js-ci-variable-input-key'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + + keyInput.value = 'foo'; + $(keyInput).trigger('input'); + valueInput.value = 'bar'; + $(valueInput).trigger('input'); + + expect(idInput.value).toEqual(''); + + ajaxVariableList.updateRowsWithPersistedVariables([ + { + id: 3, + key: 'foo', + value: 'bar', + }, + ]); + + expect(idInput.value).toEqual('3'); + expect(row.dataset.isPersisted).toEqual('true'); + }); + }); + + describe('maskableRegex', () => { + it('takes in the regex provided by the data attribute', () => { + expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$'); + expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js new file mode 100644 index 00000000000..9508203e5c2 --- /dev/null +++ b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js @@ -0,0 +1,282 @@ +import $ from 'jquery'; +import waitForPromises from 'helpers/wait_for_promises'; +import VariableList from '~/ci_variable_list/ci_variable_list'; + +const HIDE_CLASS = 'hide'; + +describe('VariableList', () => { + preloadFixtures('pipeline_schedules/edit.html'); + preloadFixtures('pipeline_schedules/edit_with_variables.html'); + preloadFixtures('projects/ci_cd_settings.html'); + + let $wrapper; + let variableList; + + describe('with only key/value inputs', () => { + describe('with no variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should remove the row when clicking the remove button', () => { + $wrapper.find('.js-row-remove-button').trigger('click'); + + expect($wrapper.find('.js-row').length).toBe(0); + }); + + it('should add another row when editing the last rows key input', () => { + const $row = $wrapper.find('.js-row'); + $row + .find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + + expect($keyInput.val()).toBe(''); + }); + + it('should add another row when editing the last rows value textarea', () => { + const $row = $wrapper.find('.js-row'); + $row + .find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + + expect($valueInput.val()).toBe(''); + }); + + it('should remove empty row after blurring', () => { + const $row = $wrapper.find('.js-row'); + $row + .find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + $row + .find('.js-ci-variable-input-key') + .val('') + .trigger('input') + .trigger('blur'); + + expect($wrapper.find('.js-row').length).toBe(1); + }); + }); + + describe('with persisted variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should have "Reveal values" button initially when there are already variables', () => { + expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values'); + }); + + it('should reveal hidden values', () => { + const $row = $wrapper.find('.js-row:first-child'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); + + // Reveal values + $wrapper.find('.js-secret-value-reveal-button').click(); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); + }); + }); + }); + + describe('with all inputs(key, value, protected)', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html'); + $wrapper = $('.js-ci-variable-list-section'); + + $wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should not add another row when editing the last rows protected checkbox', () => { + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-protected-item .js-project-feature-toggle').click(); + + return waitForPromises().then(() => { + expect($wrapper.find('.js-row').length).toBe(1); + }); + }); + + it('should not add another row when editing the last rows masked checkbox', () => { + jest.spyOn(variableList, 'checkIfRowTouched'); + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-masked-item .js-project-feature-toggle').click(); + + return waitForPromises().then(() => { + // This validates that we are checking after the event listener has run + expect(variableList.checkIfRowTouched).toHaveBeenCalled(); + expect($wrapper.find('.js-row').length).toBe(1); + }); + }); + + describe('validateMaskability', () => { + let $row; + + const maskingErrorElement = '.js-row:last-child .masking-validation-error'; + const clickToggle = () => + $row.find('.ci-variable-masked-item .js-project-feature-toggle').click(); + + beforeEach(() => { + $row = $wrapper.find('.js-row:last-child'); + }); + + it('has a regex provided via a data attribute', () => { + clickToggle(); + + expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$'); + }); + + it('allows values that are 8 characters long', () => { + $row.find('.js-ci-variable-input-value').val('looooong'); + + clickToggle(); + + expect($wrapper.find(maskingErrorElement)).toHaveClass('hide'); + }); + + it('rejects values that are shorter than 8 characters', () => { + $row.find('.js-ci-variable-input-value').val('short'); + + clickToggle(); + + expect($wrapper.find(maskingErrorElement)).toBeVisible(); + }); + + it('allows values with base 64 characters', () => { + $row.find('.js-ci-variable-input-value').val('abcABC123_+=/-'); + + clickToggle(); + + expect($wrapper.find(maskingErrorElement)).toHaveClass('hide'); + }); + + it('rejects values with other special characters', () => { + $row.find('.js-ci-variable-input-value').val('1234567$'); + + clickToggle(); + + expect($wrapper.find(maskingErrorElement)).toBeVisible(); + }); + }); + }); + + describe('toggleEnableRow method', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should disable all key inputs', () => { + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + }); + + it('should disable all remove buttons', () => { + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + }); + + it('should enable all remove buttons', () => { + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + }); + + it('should enable all key inputs', () => { + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + }); + }); + + describe('hideValues', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should hide value input and show placeholder stars', () => { + const $row = $wrapper.find('.js-row'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + $row + .find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); + + variableList.hideValues(); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js new file mode 100644 index 00000000000..4982b68fa81 --- /dev/null +++ b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js @@ -0,0 +1,37 @@ +import $ from 'jquery'; +import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; + +describe('NativeFormVariableList', () => { + preloadFixtures('pipeline_schedules/edit.html'); + + let $wrapper; + + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.js-ci-variable-list-section'); + + setupNativeFormVariableList({ + container: $wrapper, + formField: 'schedule', + }); + }); + + describe('onFormSubmit', () => { + it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { + const $row = $wrapper.find('.js-row'); + + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe( + 'schedule[variables_attributes][][key]', + ); + + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe( + 'schedule[variables_attributes][][secret_value]', + ); + + $wrapper.closest('form').trigger('trigger-submit'); + + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(''); + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(''); + }); + }); +}); 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 index 7b8d69df35e..9179302f786 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -96,6 +96,13 @@ describe('Ci variable modal', () => { findModal().vm.$emit('hidden'); expect(store.dispatch).toHaveBeenCalledWith('clearModal'); }); + + it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => { + store.state.isProtectedByDefault = true; + findModal().vm.$emit('shown'); + + expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected'); + }); }); describe('Editing a variable', () => { diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js index 09c6cd9de21..7dab33050d9 100644 --- a/spec/frontend/ci_variable_list/services/mock_data.js +++ b/spec/frontend/ci_variable_list/services/mock_data.js @@ -8,7 +8,7 @@ export default { protected: false, secret_value: 'test_val', value: 'test_val', - variable_type: 'Var', + variable_type: 'Variable', }, ], @@ -44,7 +44,7 @@ export default { protected: false, secret_value: 'test_val', value: 'test_val', - variable_type: 'Var', + variable_type: 'Variable', }, { environment_scope: 'All (default)', @@ -104,7 +104,7 @@ export default { id: 28, key: 'goku_var', value: 'goku_val', - variable_type: 'Var', + variable_type: 'Variable', protected: true, masked: true, environment_scope: 'staging', @@ -114,7 +114,7 @@ export default { id: 25, key: 'test_var_4', value: 'test_val_4', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'production', @@ -134,7 +134,7 @@ export default { id: 24, key: 'test_var_3', value: 'test_val_3', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'All (default)', @@ -144,7 +144,7 @@ export default { id: 26, key: 'test_var_5', value: 'test_val_5', - variable_type: 'Var', + variable_type: 'Variable', protected: false, masked: false, environment_scope: 'production', diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js index 84455612f0c..12b4311d0f5 100644 --- a/spec/frontend/ci_variable_list/store/actions_spec.js +++ b/spec/frontend/ci_variable_list/store/actions_spec.js @@ -75,6 +75,16 @@ describe('CI variable list store actions', () => { }); }); + describe('setVariableProtected', () => { + it('commits SET_VARIABLE_PROTECTED mutation', () => { + testAction(actions.setVariableProtected, {}, {}, [ + { + type: types.SET_VARIABLE_PROTECTED, + }, + ]); + }); + }); + describe('deleteVariable', () => { it('dispatch correct actions on successful deleted variable', done => { mock.onPatch(state.endpoint).reply(200); diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js index 8652359f3df..1934d108957 100644 --- a/spec/frontend/ci_variable_list/store/mutations_spec.js +++ b/spec/frontend/ci_variable_list/store/mutations_spec.js @@ -47,7 +47,7 @@ describe('CI variable list mutations', () => { describe('CLEAR_MODAL', () => { it('should clear modal state ', () => { const modalState = { - variable_type: 'Var', + variable_type: 'Variable', key: '', secret_value: '', protected: false, @@ -97,4 +97,12 @@ describe('CI variable list mutations', () => { expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']); }); }); + + describe('SET_VARIABLE_PROTECTED', () => { + it('should set protected value to true', () => { + mutations[types.SET_VARIABLE_PROTECTED](stateCopy); + + expect(stateCopy.variable.protected).toBe(true); + }); + }); }); |