Commit a55cfc1e authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'ce-39118-dynamic-pipeline-variables-fe' into 'master'

Dynamic CI secret variables -- CE backport

See merge request gitlab-org/gitlab-ce!16842
parents 024c8a42 efcdc269
import _ from 'underscore';
import axios from '../lib/utils/axios_utils';
import { s__ } from '../locale';
import Flash from '../flash';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(errorString => `
<li>
${_.escape(errorString)}
</li>
`);
return `
<p>
${s__('CiVariable|Validation failed')}
</p>
<ul>
${errorList.join('')}
</ul>
`;
}
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
constructor({
container,
saveButton,
errorBox,
formField = 'variables',
saveEndpoint,
}) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
this.saveEndpoint = saveEndpoint;
this.variableList = new VariableList({
container: this.container,
formField,
});
this.bindEvents();
this.variableList.init();
}
bindEvents() {
this.saveButton.addEventListener('click', this.onSaveClicked.bind(this));
}
onSaveClicked() {
const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance
// to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false);
return axios.patch(this.saveEndpoint, {
variables_attributes: this.variableList.getAllData(),
}, {
// We want to be able to process the `res.data` from a 400 error response
// and print the validation messages such as duplicate variable keys
validateStatus: status => (
status >= statusCodes.OK &&
status < statusCodes.MULTIPLE_CHOICES
) ||
status === statusCodes.BAD_REQUEST,
})
.then((res) => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables);
} else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data);
this.errorBox.classList.toggle('hide', false);
}
})
.catch(() => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
Flash(s__('CiVariable|Error occured while saving variables'));
});
}
updateRowsWithPersistedVariables(persistedVariables = []) {
const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({
...variableMap,
[variable.key]: variable,
}), {});
this.container.querySelectorAll('.js-row').forEach((row) => {
// If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) {
row.remove();
// Update the ID input so any future edits and `_destroy` will apply on the BE
} else {
const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key];
if (persistedVariable) {
// eslint-disable-next-line no-param-reassign
row.querySelector('.js-ci-variable-input-id').value = persistedVariable.id;
row.setAttribute('data-is-persisted', 'true');
}
}
});
}
}
...@@ -11,7 +11,7 @@ function createEnvironmentItem(value) { ...@@ -11,7 +11,7 @@ function createEnvironmentItem(value) {
return { return {
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
id: value, id: value,
text: value, text: value === '*' ? s__('CiVariable|* (All environments)') : value,
}; };
} }
...@@ -41,11 +41,11 @@ export default class VariableList { ...@@ -41,11 +41,11 @@ export default class VariableList {
selector: '.js-ci-variable-input-protected', selector: '.js-ci-variable-input-protected',
default: 'true', default: 'true',
}, },
environment: { environment_scope: {
// We can't use a `.js-` class here because // We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class // gl_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
default: '*', default: '*',
}, },
_destroy: { _destroy: {
...@@ -104,12 +104,15 @@ export default class VariableList { ...@@ -104,12 +104,15 @@ export default class VariableList {
setupToggleButtons($row[0]); setupToggleButtons($row[0]);
// Reset the resizable textarea
$row.find(this.inputMap.value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle'); const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) { if ($environmentSelect.length) {
const createItemDropdown = new CreateItemDropdown({ const createItemDropdown = new CreateItemDropdown({
$dropdown: $environmentSelect, $dropdown: $environmentSelect,
defaultToggleLabel: ALL_ENVIRONMENTS_STRING, defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
fieldName: `${this.formField}[variables_attributes][][environment]`, fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
getData: (term, callback) => callback(this.getEnvironmentValues()), getData: (term, callback) => callback(this.getEnvironmentValues()),
createNewItemFromValue: createEnvironmentItem, createNewItemFromValue: createEnvironmentItem,
onSelect: () => { onSelect: () => {
...@@ -117,7 +120,7 @@ export default class VariableList { ...@@ -117,7 +120,7 @@ export default class VariableList {
// so they have the new value we just picked // so they have the new value we just picked
this.refreshDropdownData(); this.refreshDropdownData();
$row.find(this.inputMap.environment.selector).trigger('trigger-change'); $row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
}, },
}); });
...@@ -143,7 +146,8 @@ export default class VariableList { ...@@ -143,7 +146,8 @@ export default class VariableList {
$row.after($rowClone); $row.after($rowClone);
} }
removeRow($row) { removeRow(row) {
const $row = $(row);
const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) { if (isPersisted) {
...@@ -155,6 +159,10 @@ export default class VariableList { ...@@ -155,6 +159,10 @@ export default class VariableList {
} else { } else {
$row.remove(); $row.remove();
} }
// Refresh the other dropdowns in the variable list
// so any value with the variable deleted is gone
this.refreshDropdownData();
} }
checkIfRowTouched($row) { checkIfRowTouched($row) {
...@@ -165,6 +173,11 @@ export default class VariableList { ...@@ -165,6 +173,11 @@ export default class VariableList {
}); });
} }
toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
...@@ -185,7 +198,7 @@ export default class VariableList { ...@@ -185,7 +198,7 @@ export default class VariableList {
} }
getEnvironmentValues() { getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray()
.reduce((prevValueMap, envInput) => ({ .reduce((prevValueMap, envInput) => ({
...prevValueMap, ...prevValueMap,
[envInput.value]: envInput.value, [envInput.value]: envInput.value,
......
...@@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches || ...@@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches ||
while (i >= 0 && elms.item(i) !== this) { i -= 1; } while (i >= 0 && elms.item(i) !== this) { i -= 1; }
return i > -1; return i > -1;
}; };
// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
((arr) => {
arr.forEach((item) => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
if (this.parentNode !== null) {
this.parentNode.removeChild(this);
}
},
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
...@@ -6,4 +6,6 @@ export default { ...@@ -6,4 +6,6 @@ export default {
ABORTED: 0, ABORTED: 0,
NO_CONTENT: 204, NO_CONTENT: 204,
OK: 200, OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
}; };
import SecretValues from '~/behaviors/secret_values'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => { export default () => {
const secretVariableTable = document.querySelector('.js-secret-variable-table'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
if (secretVariableTable) { // eslint-disable-next-line no-new
const secretVariableTableValues = new SecretValues({ new AjaxVariableList({
container: secretVariableTable, container: variableListEl,
}); saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
secretVariableTableValues.init(); errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
} saveEndpoint: variableListEl.dataset.saveEndpoint,
});
}; };
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values'; import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () { export default function () {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token'); const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) { if (runnerToken) {
const runnerTokenSecretValue = new SecretValues({ const runnerTokenSecretValue = new SecretValues({
...@@ -12,11 +14,12 @@ export default function () { ...@@ -12,11 +14,12 @@ export default function () {
runnerTokenSecretValue.init(); runnerTokenSecretValue.init();
} }
const secretVariableTable = document.querySelector('.js-secret-variable-table'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
if (secretVariableTable) { // eslint-disable-next-line no-new
const secretVariableTableValues = new SecretValues({ new AjaxVariableList({
container: secretVariableTable, container: variableListEl,
}); saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
secretVariableTableValues.init(); errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
} saveEndpoint: variableListEl.dataset.saveEndpoint,
});
} }
...@@ -8,7 +8,11 @@ ...@@ -8,7 +8,11 @@
.ci-variable-row { .ci-variable-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-start;
@media (max-width: $screen-xs-max) {
align-items: flex-end;
}
&:not(:last-child) { &:not(:last-child) {
margin-bottom: $gl-btn-padding; margin-bottom: $gl-btn-padding;
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
.ci-variable-row-body { .ci-variable-row-body {
display: flex; display: flex;
align-items: flex-start;
width: 100%; width: 100%;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -65,6 +70,8 @@ ...@@ -65,6 +70,8 @@
flex: 0 1 auto; flex: 0 1 auto;
display: flex; display: flex;
align-items: center; align-items: center;
padding-top: 5px;
padding-bottom: 5px;
} }
.ci-variable-row-remove-button { .ci-variable-row-remove-button {
...@@ -85,4 +92,8 @@ ...@@ -85,4 +92,8 @@
outline: none; outline: none;
color: $gl-text-color; color: $gl-text-color;
} }
&[disabled] {
color: $gl-text-color-disabled;
}
} }
module Groups module Groups
class VariablesController < Groups::ApplicationController class VariablesController < Groups::ApplicationController
before_action :variable, only: [:show, :update, :destroy]
before_action :authorize_admin_build! before_action :authorize_admin_build!
def index
redirect_to group_settings_ci_cd_path(group)
end
def show def show
respond_to do |format|
format.json do
render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) }
end
end
end end
def update def update
if variable.update(variable_params) if @group.update(group_variables_params)
redirect_to group_variables_path(group), respond_to do |format|
notice: 'Variable was successfully updated.' format.json { return render_group_variables }
end
else else
render "show" respond_to do |format|
format.json { render_error }
end
end end
end end
def create private
@variable = group.variables.create(variable_params)
.present(current_user: current_user)
if @variable.persisted? def render_group_variables
redirect_to group_settings_ci_cd_path(group), render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) }
notice: 'Variable was successfully created.'
else
render "show"
end
end end
def destroy def render_error
if variable.destroy render status: :bad_request, json: @group.errors.full_messages
redirect_to group_settings_ci_cd_path(group),
status: 302,
notice: 'Variable was successfully removed.'
else
redirect_to group_settings_ci_cd_path(group),
status: 302,
notice: 'Failed to remove the variable.'
end
end end
private def group_variables_params
params.permit(variables_attributes: [*variable_params_attributes])
def variable_params
params.require(:variable).permit(*variable_params_attributes)
end end
def variable_params_attributes def variable_params_attributes
%i[key value protected] %i[id key value protected _destroy]
end
def variable
@variable ||= group.variables.find(params[:id]).present(current_user: current_user)
end end
def authorize_admin_build! def authorize_admin_build!
......
class Projects::VariablesController < Projects::ApplicationController class Projects::VariablesController < Projects::ApplicationController
before_action :variable, only: [:show, :update, :destroy]
before_action :authorize_admin_build! before_action :authorize_admin_build!
layout 'project_settings'
def index
redirect_to project_settings_ci_cd_path(@project)
end
def show def show
respond_to do |format|
format.json do
render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) }
end
end
end end
def update def update
if variable.update(variable_params) if @project.update(variables_params)
redirect_to project_variables_path(project), respond_to do |format|
notice: 'Variable was successfully updated.' format.json { return render_variables }
end
else else
render "show" respond_to do |format|
format.json { render_error }
end
end end
end end
def create private
@variable = project.variables.create(variable_params)
.present(current_user: current_user)
if @variable.persisted? def render_variables
redirect_to project_settings_ci_cd_path(project), render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) }
notice: 'Variable was successfully created.'
else
render "show"
end
end end
def destroy def render_error
if variable.destroy render status: :bad_request, json: @project.errors.full_messages
redirect_to project_settings_ci_cd_path(project),
status: 302,
notice: 'Variable was successfully removed.'
else
redirect_to project_settings_ci_cd_path(project),
status: 302,
notice: 'Failed to remove the variable.'
end
end end
private def variables_params
params.permit(variables_attributes: [*variable_params_attributes])
def variable_params
params.require(:variable).permit(*variable_params_attributes)
end end
def variable_params_attributes def variable_params_attributes
%i[id key value protected _destroy] %i[id key value protected _destroy]
end end
def variable
@variable ||= project.variables.find(params[:id]).present(current_user: current_user)
end
end end
...@@ -31,9 +31,12 @@ class Group < Namespace ...@@ -31,9 +31,12 @@ class Group < Namespace
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent validate :visibility_level_allowed_by_parent
validates :variables, variable_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
......
...@@ -260,6 +260,7 @@ class Project < ActiveRecord::Base ...@@ -260,6 +260,7 @@ class Project < ActiveRecord::Base
validates :repository_storage, validates :repository_storage,
presence: true, presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: true
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -7,19 +7,15 @@ module Ci ...@@ -7,19 +7,15 @@ module Ci
end end
def form_path def form_path
if variable.persisted? group_settings_ci_cd_path(group)
group_variable_path(group, variable)
else
group_variables_path(group)
end
end end
def edit_path def edit_path
group_variable_path(group, variable) group_variables_path(group)
end end
def delete_path def delete_path
group_variable_path(group, variable) group_variables_path(group)
end end
end end
end end
...@@ -7,19 +7,15 @@ module Ci ...@@ -7,19 +7,15 @@ module Ci
end end
def form_path def form_path
if variable.persisted? project_settings_ci_cd_path(project)
project_variable_path(project, variable)
else
project_variables_path(project)
end
end end
def edit_path def edit_path
project_variable_path(project, variable) project_variables_path(project)
end end
def delete_path def delete_path
project_variable_path(project, variable) project_variables_path(project)
end end
end end
end end
class GroupVariableEntity < Grape::Entity
expose :id
expose :key
expose :value
expose :protected?, as: :protected
end
class GroupVariableSerializer < BaseSerializer
entity GroupVariableEntity
end
class VariableEntity < Grape::Entity
expose :id
expose :key
expose :value
expose :protected?, as: :protected
end
class VariableSerializer < BaseSerializer
entity VariableEntity
end
%p.append-bottom-default = _('Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want.')
Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags.
You can use variables for passwords, secret keys, or whatever you want.
= form_for @variable, as: :variable, url: @variable.form_path do |f|
= form_errors(@variable)
.form-group
= f.label :key, "Key", class: "label-light"
= f.text_field :key, class: "form-control", placeholder: @variable.placeholder, required: true
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: @variable.placeholder
.form-group
.checkbox
= f.label :protected do
= f.check_box :protected
%strong Protected
.help-block
This variable will be passed only to pipelines running on protected branches and tags
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'protected-secret-variables'), target: '_blank'
= f.submit btn_text, class: "btn btn-save"
.row.prepend-top-default.append-bottom-default - save_endpoint = local_assigns.fetch(:save_endpoint, nil)
.col-lg-12
%h5.prepend-top-0 .row
Add a variable .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } }
= render "ci/variables/form", btn_text: "Add new variable" .hide.alert.alert-danger.js-ci-variable-error-box
%hr
%h5.prepend-top-0 %ul.ci-variable-list
Your variables (#{@variables.size}) - @variables.each.each do |variable|
- if @variables.empty? = render 'ci/variables/variable_row', form_field: 'variables', variable: variable
%p.settings-message.text-center.append-bottom-0 = render 'ci/variables/variable_row', form_field: 'variables'
No variables found, add one with the form above. .prepend-top-20
- else %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' }
.js-secret-variable-table %span.hide.js-secret-variables-save-loading-icon
= render "ci/variables/table" = icon('spinner spin')
%button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } } = _('Save variables')
%button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } }
- if @variables.size == 0
= n_('Hide value', 'Hide values', @variables.size)
- else
= n_('Reveal value', 'Reveal values', @variables.size) = n_('Reveal value', 'Reveal values', @variables.size)
- page_title "Variables"
.row.prepend-top-default.append-bottom-default
.col-lg-3
= render "ci/variables/content"
.col-lg-9
%h4.prepend-top-0
Update variable
= render "ci/variables/form", btn_text: "Save variable"
.table-responsive.variables-table
%table.table
%colgroup
%col
%col
%col
%col{ width: 100 }
%thead
%th Key
%th Value
%th Protected
%th
%tbody
- @variables.each do |variable|
- if variable.id?
%tr
%td.variable-key= variable.key
%td.variable-value
%span.js-secret-value-placeholder
= '*' * 6
%span.hide.js-secret-value
= variable.value
%td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
Update
= icon("pencil")
= link_to variable.delete_path, class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
%span.sr-only
Remove
= icon("trash")
- breadcrumb_title "CI / CD Settings" - breadcrumb_title "CI / CD Settings"
- page_title "CI / CD" - page_title "CI / CD"
= render 'ci/variables/index' %h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%p
= render "ci/variables/content"
= render 'ci/variables/index', save_endpoint: group_variables_path
...@@ -29,14 +29,14 @@ ...@@ -29,14 +29,14 @@
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Secret variables = _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p.append-bottom-0
= render "ci/variables/content" = render "ci/variables/content"
.settings-content .settings-content
= render 'ci/variables/index' = render 'ci/variables/index', save_endpoint: project_variables_path(@project)
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
---
title: Update CI/CD secret variables list to be dynamic and save without reloading
the page
merge_request: 4110
author:
type: added
...@@ -28,7 +28,7 @@ constraints(GroupUrlConstrainer.new) do ...@@ -28,7 +28,7 @@ constraints(GroupUrlConstrainer.new) do
resource :ci_cd, only: [:show], controller: 'ci_cd' resource :ci_cd, only: [:show], controller: 'ci_cd'
end end
resources :variables, only: [:index, :show, :update, :create, :destroy] resource :variables, only: [:show, :update]
resources :children, only: [:index] resources :children, only: [:index]
......
...@@ -156,7 +156,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -156,7 +156,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :variables, only: [:index, :show, :update, :create, :destroy] resource :variables, only: [:show, :update]
resources :triggers, only: [:index, :create, :edit, :update, :destroy] do resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do member do
post :take_ownership post :take_ownership
......
...@@ -31,7 +31,7 @@ module QA ...@@ -31,7 +31,7 @@ module QA
page.fill_variable_key(key) page.fill_variable_key(key)
page.fill_variable_value(value) page.fill_variable_value(value)
page.add_variable page.save_variables
end end
end end
end end
......
...@@ -5,49 +5,40 @@ module QA ...@@ -5,49 +5,40 @@ module QA
class SecretVariables < Page::Base class SecretVariables < Page::Base
include Common include Common
view 'app/views/ci/variables/_table.html.haml' do view 'app/views/ci/variables/_variable_row.html.haml' do
element :variable_key, '.variable-key' element :variable_key, '.js-ci-variable-input-key'
element :variable_value, '.variable-value' element :variable_value, '.js-ci-variable-input-value'
end end
view 'app/views/ci/variables/_index.html.haml' do view 'app/views/ci/variables/_index.html.haml' do
element :add_new_variable, 'btn_text: "Add new variable"' element :save_variables, '.js-secret-variables-save-button'
end
view 'app/assets/javascripts/behaviors/secret_values.js' do
element :reveal_value, 'Reveal value'
element :hide_value, 'Hide value'
end end
def fill_variable_key(key) def fill_variable_key(key)
fill_in 'variable_key', with: key page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
page.find('.js-ci-variable-input-key').set(key)
end
end end
def fill_variable_value(value) def fill_variable_value(value)
fill_in 'variable_value', with: value page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
page.find('.js-ci-variable-input-value').set(value)
end
end end
def add_variable def save_variables
click_on 'Add new variable' click_button('Save variables')
end end
def variable_key def variable_key
page.find('.variable-key').text page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
end page.find('.js-ci-variable-input-key').value
def variable_value
reveal_value do
page.find('.variable-value').text
end end
end end
private def variable_value
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
def reveal_value page.find('.js-ci-variable-input-value').value
click_button('Reveal value')
yield.tap do
click_button('Hide value')
end end
end end
end end
......
...@@ -9,48 +9,27 @@ describe Groups::VariablesController do ...@@ -9,48 +9,27 @@ describe Groups::VariablesController do
group.add_master(user) group.add_master(user)
end end
describe 'POST #create' do describe 'GET #show' do
context 'variable is valid' do let!(:variable) { create(:ci_group_variable, group: group) }
it 'shows a success flash message' do
post :create, group_id: group, variable: { key: "one", value: "two" }
expect(flash[:notice]).to include 'Variable was successfully created.'
expect(response).to redirect_to(group_settings_ci_cd_path(group))
end
end
context 'variable is invalid' do
it 'renders show' do
post :create, group_id: group, variable: { key: "..one", value: "two" }
expect(response).to render_template("groups/variables/show") subject do
end get :show, group_id: group, format: :json
end end
end
describe 'POST #update' do
let(:variable) { create(:ci_group_variable) }
context 'updating a variable with valid characters' do include_examples 'GET #show lists all variables'
before do end
group.variables << variable
end
it 'shows a success flash message' do
post :update, group_id: group,
id: variable.id, variable: { key: variable.key, value: 'two' }
expect(flash[:notice]).to include 'Variable was successfully updated.'
expect(response).to redirect_to(group_variables_path(group))
end
it 'renders the action #show if the variable key is invalid' do describe 'PATCH #update' do
post :update, group_id: group, let!(:variable) { create(:ci_group_variable, group: group) }
id: variable.id, variable: { key: '?', value: variable.value } let(:owner) { group }
expect(response).to have_gitlab_http_status(200) subject do
expect(response).to render_template :show patch :update,
end group_id: group,
variables_attributes: variables_attributes,
format: :json
end end
include_examples 'PATCH #update updates variables'
end end
end end
...@@ -9,50 +9,28 @@ describe Projects::VariablesController do ...@@ -9,50 +9,28 @@ describe Projects::VariablesController do
project.add_master(user) project.add_master(user)
end end
describe 'POST #create' do describe 'GET #show' do
context 'variable is valid' do let!(:variable) { create(:ci_variable, project: project) }
it 'shows a success flash message' do
post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "one", value: "two" }
expect(flash[:notice]).to include 'Variable was successfully created.'
expect(response).to redirect_to(project_settings_ci_cd_path(project))
end
end
context 'variable is invalid' do
it 'renders show' do
post :create, namespace_id: project.namespace.to_param, project_id: project,
variable: { key: "..one", value: "two" }
expect(response).to render_template("projects/variables/show") subject do
end get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json
end end
end
describe 'POST #update' do
let(:variable) { create(:ci_variable) }
context 'updating a variable with valid characters' do include_examples 'GET #show lists all variables'
before do end
project.variables << variable
end
it 'shows a success flash message' do
post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: variable.key, value: 'two' }
expect(flash[:notice]).to include 'Variable was successfully updated.'
expect(response).to redirect_to(project_variables_path(project))
end
it 'renders the action #show if the variable key is invalid' do describe 'PATCH #update' do
post :update, namespace_id: project.namespace.to_param, project_id: project, let!(:variable) { create(:ci_variable, project: project) }
id: variable.id, variable: { key: '?', value: variable.value } let(:owner) { project }
expect(response).to have_gitlab_http_status(200) subject do
expect(response).to render_template :show patch :update,
end namespace_id: project.namespace.to_param,
project_id: project,
variables_attributes: variables_attributes,
format: :json
end end
include_examples 'PATCH #update updates variables'
end end
end end
...@@ -3,76 +3,15 @@ require 'spec_helper' ...@@ -3,76 +3,15 @@ require 'spec_helper'
feature 'Group variables', :js do feature 'Group variables', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test value', group: group) }
let(:page_path) { group_settings_ci_cd_path(group) }
background do background do
group.add_master(user) group.add_master(user)
gitlab_sign_in(user) gitlab_sign_in(user)
end
context 'when user creates a new variable' do
background do
visit group_settings_ci_cd_path(group)
fill_in 'variable_key', with: 'AAA'
fill_in 'variable_value', with: 'AAA123'
find(:css, "#variable_protected").set(true)
click_on 'Add new variable'
end
scenario 'user sees the created variable' do
page.within('.variables-table') do
expect(find(".variable-key")).to have_content('AAA')
expect(find(".variable-value")).to have_content('******')
expect(find(".variable-protected")).to have_content('Yes')
end
click_on 'Reveal value'
page.within('.variables-table') do
expect(find(".variable-value")).to have_content('AAA123')
end
end
end
context 'when user edits a variable' do
background do
create(:ci_group_variable, key: 'AAA', value: 'AAA123', protected: true,
group: group)
visit group_settings_ci_cd_path(group)
page.within('.variable-menu') do visit page_path
click_on 'Update'
end
fill_in 'variable_key', with: 'BBB'
fill_in 'variable_value', with: 'BBB123'
find(:css, "#variable_protected").set(false)
click_on 'Save variable'
end
scenario 'user sees the updated variable' do
page.within('.variables-table') do
expect(find(".variable-key")).to have_content('BBB')
expect(find(".variable-value")).to have_content('******')
expect(find(".variable-protected")).to have_content('No')
end
end
end end
context 'when user deletes a variable' do it_behaves_like 'variable list'
background do
create(:ci_group_variable, key: 'BBB', value: 'BBB123', protected: false,
group: group)
visit group_settings_ci_cd_path(group)
page.within('.variable-menu') do
page.accept_alert 'Are you sure?' do
click_on 'Remove'
end
end
end
scenario 'user does not see the deleted variable' do
expect(page).to have_no_css('.variables-table')
end
end
end end
require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
let(:page_path) { project_settings_ci_cd_path(project) }
before do
sign_in(user)
project.add_master(user)
project.variables << variable
visit page_path
end
it_behaves_like 'variable list'
end
require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
before do
sign_in(user)
project.add_master(user)
project.variables << variable
visit project_settings_ci_cd_path(project)
end
it 'shows list of variables' do
page.within('.variables-table') do
expect(page).to have_content(variable.key)
end
end
it 'adds new secret variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('No')
end
end
it 'adds empty variable' do
fill_in('variable_key', with: 'new_key')
fill_in('variable_value', with: '')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('new_key')
end
end
it 'adds new protected variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'value')
check('Protected')
click_button('Add new variable')
expect(page).to have_content('Variable was successfully created.')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('Yes')
end
end
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('******')
end
click_button('Reveal values')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('key value')
end
click_button('Hide values')
page.within('.variables-table') do
expect(page).to have_content('key')
expect(page).to have_content('******')
end
end
it 'deletes variable' do
page.within('.variables-table') do
accept_confirm { click_on 'Remove' }
end
expect(page).not_to have_selector('variables-table')
end
it 'edits variable' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first.value).to eq('key value')
end
it 'edits variable with empty value' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
fill_in('variable_value', with: '')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first.value).to eq('')
end
it 'edits variable to be protected' do
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
check('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).to be_protected
end
it 'edits variable to be unprotected' do
project.variables.first.update(protected: true)
page.within('.variables-table') do
click_on 'Update'
end
expect(page).to have_content('Update variable')
uncheck('Protected')
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
expect(project.variables(true).first).not_to be_protected
end
end
{
"type": "object",
"required": [
"id",
"key",
"value",
"protected"
],
"properties": {
"id": { "type": "integer" },
"key": { "type": "string" },
"value": { "type": "string" },
"protected": { "type": "boolean" }
},
"additionalProperties": false
}
{
"type": "object",
"required": ["variables"],
"properties": {
"variables": {
"type": "array",
"items": { "$ref": "variable.json" }
}
},
"additionalProperties": false
}
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';
describe('AjaxFormVariableList', () => {
preloadFixtures('projects/ci_cd_settings.html.raw');
preloadFixtures('projects/ci_cd_settings_with_variables.html.raw');
let container;
let saveButton;
let errorBox;
let mock;
let ajaxVariableList;
beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw');
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-secret-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
formField: 'variables',
saveButton,
errorBox,
saveEndpoint: container.dataset.saveEndpoint,
});
spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough();
spyOn(ajaxVariableList.variableList, 'toggleEnableRow').and.callThrough();
});
afterEach(() => {
mock.restore();
});
describe('onSaveClicked', () => {
it('shows loading spinner while waiting for the request', (done) => {
const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(false);
return [200, {}];
});
expect(loadingIcon.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
it('calls `updateRowsWithPersistedVariables` with the persisted variables', (done) => {
const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }];
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {
variables: variablesResponse,
});
ajaxVariableList.onSaveClicked()
.then(() => {
expect(ajaxVariableList.updateRowsWithPersistedVariables)
.toHaveBeenCalledWith(variablesResponse);
})
.then(done)
.catch(done.fail);
});
it('hides any previous error box', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
it('disables remove buttons while waiting for the request', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false);
return [200, {}];
});
ajaxVariableList.onSaveClicked()
.then(() => {
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true);
})
.then(done)
.catch(done.fail);
});
it('shows error box with validation errors', (done) => {
const validationError = 'some validation error';
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [
validationError,
]);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(false);
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`);
})
.then(done)
.catch(done.fail);
});
it('shows flash message when request fails', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
expect(errorBox.classList.contains('hide')).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true);
})
.then(done)
.catch(done.fail);
});
});
describe('updateRowsWithPersistedVariables', () => {
beforeEach(() => {
loadFixtures('projects/ci_cd_settings_with_variables.html.raw');
container = document.querySelector('.js-ci-variable-list-section');
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
saveButton = ajaxVariableListEl.querySelector('.js-secret-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.dispatchEvent(new Event('input'));
valueInput.value = 'bar';
valueInput.dispatchEvent(new Event('input'));
expect(idInput.value).toEqual('');
ajaxVariableList.updateRowsWithPersistedVariables([{
id: 3,
key: 'foo',
value: 'bar',
}]);
expect(idInput.value).toEqual('3');
expect(row.dataset.isPersisted).toEqual('true');
});
});
});
...@@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; ...@@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
describe('VariableList', () => { describe('VariableList', () => {
preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit.html.raw');
preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
preloadFixtures('projects/ci_cd_settings.html.raw');
let $wrapper; let $wrapper;
let variableList; let variableList;
...@@ -105,37 +106,8 @@ describe('VariableList', () => { ...@@ -105,37 +106,8 @@ describe('VariableList', () => {
describe('with all inputs(key, value, protected)', () => { describe('with all inputs(key, value, protected)', () => {
beforeEach(() => { beforeEach(() => {
// This markup will be replaced with a fixture when we can render the loadFixtures('projects/ci_cd_settings.html.raw');
// CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 $wrapper = $('.js-ci-variable-list-section');
$wrapper = $(`<form class="js-variable-list">
<ul>
<li class="js-row">
<div class="ci-variable-body-item">
<input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]">
</div>
<div class="ci-variable-body-item">
<textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea>
</div>
<div class="ci-variable-body-item ci-variable-protected-item">
<button type="button" class="js-project-feature-toggle project-feature-toggle">
<input
type="hidden"
class="js-ci-variable-input-protected js-project-feature-toggle-input"
name="variables[variables_attributes][][protected]"
value="true"
/>
</button>
</div>
<button type="button" class="js-row-remove-button"></button>
</li>
</ul>
<button type="button" class="js-secret-value-reveal-button">
Reveal values
</button>
</form>`);
variableList = new VariableList({ variableList = new VariableList({
container: $wrapper, container: $wrapper,
...@@ -160,4 +132,51 @@ describe('VariableList', () => { ...@@ -160,4 +132,51 @@ describe('VariableList', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('toggleEnableRow method', () => {
beforeEach(() => {
loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
$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);
});
});
}); });
require 'spec_helper'
describe 'Groups (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:group) { create(:group, name: 'frontend-fixtures-group' )}
render_views
before(:all) do
clean_frontend_fixtures('groups/')
end
before do
group.add_master(admin)
sign_in(admin)
end
describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
it 'groups/ci_cd_settings.html.raw' do |example|
get :show,
group_id: group
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
end
require 'spec_helper' require 'spec_helper'
describe ProjectsController, '(JavaScript fixtures)', type: :controller do describe 'Projects (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'builds-project') } let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') }
let!(:variable1) { create(:ci_variable, project: project_variable_populated) }
let!(:variable2) { create(:ci_variable, project: project_variable_populated) }
render_views render_views
...@@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do ...@@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
end end
before do before do
# EE-specific start
# EE specific end
project.add_master(admin)
sign_in(admin) sign_in(admin)
end end
...@@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do ...@@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
remove_repository(project) remove_repository(project)
end end
it 'projects/dashboard.html.raw' do |example| describe ProjectsController, '(JavaScript fixtures)', type: :controller do
get :show, it 'projects/dashboard.html.raw' do |example|
namespace_id: project.namespace.to_param, get :show,
id: project namespace_id: project.namespace.to_param,
id: project
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end
it 'projects/edit.html.raw' do |example|
get :edit,
namespace_id: project.namespace.to_param,
id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
it 'projects/ci_cd_settings.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
it 'projects/ci_cd_settings_with_variables.html.raw' do |example|
get :show,
namespace_id: project_variable_populated.namespace.to_param,
project_id: project_variable_populated
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end end
end end
...@@ -2070,7 +2070,7 @@ describe Project do ...@@ -2070,7 +2070,7 @@ describe Project do
create(:ci_variable, :protected, value: 'protected', project: project) create(:ci_variable, :protected, value: 'protected', project: project)
end end
subject { project.secret_variables_for(ref: 'ref') } subject { project.reload.secret_variables_for(ref: 'ref') }
before do before do
stub_application_setting( stub_application_setting(
......
...@@ -35,29 +35,20 @@ describe Ci::GroupVariablePresenter do ...@@ -35,29 +35,20 @@ describe Ci::GroupVariablePresenter do
end end
describe '#form_path' do describe '#form_path' do
context 'when variable is persisted' do subject { described_class.new(variable).form_path }
subject { described_class.new(variable).form_path }
it { is_expected.to eq(group_variable_path(group, variable)) } it { is_expected.to eq(group_settings_ci_cd_path(group)) }
end
context 'when variable is not persisted' do
let(:variable) { build(:ci_group_variable, group: group) }
subject { described_class.new(variable).form_path }
it { is_expected.to eq(group_variables_path(group)) }
end
end end
describe '#edit_path' do describe '#edit_path' do
subject { described_class.new(variable).edit_path } subject { described_class.new(variable).edit_path }
it { is_expected.to eq(group_variable_path(group, variable)) } it { is_expected.to eq(group_variables_path(group)) }
end end
describe '#delete_path' do describe '#delete_path' do
subject { described_class.new(variable).delete_path } subject { described_class.new(variable).delete_path }
it { is_expected.to eq(group_variable_path(group, variable)) } it { is_expected.to eq(group_variables_path(group)) }
end end
end end
...@@ -35,29 +35,20 @@ describe Ci::VariablePresenter do ...@@ -35,29 +35,20 @@ describe Ci::VariablePresenter do
end end
describe '#form_path' do describe '#form_path' do
context 'when variable is persisted' do subject { described_class.new(variable).form_path }
subject { described_class.new(variable).form_path }
it { is_expected.to eq(project_variable_path(project, variable)) } it { is_expected.to eq(project_settings_ci_cd_path(project)) }
end
context 'when variable is not persisted' do
let(:variable) { build(:ci_variable, project: project) }
subject { described_class.new(variable).form_path }
it { is_expected.to eq(project_variables_path(project)) }
end
end end
describe '#edit_path' do describe '#edit_path' do
subject { described_class.new(variable).edit_path } subject { described_class.new(variable).edit_path }
it { is_expected.to eq(project_variable_path(project, variable)) } it { is_expected.to eq(project_variables_path(project)) }
end end
describe '#delete_path' do describe '#delete_path' do
subject { described_class.new(variable).delete_path } subject { described_class.new(variable).delete_path }
it { is_expected.to eq(project_variable_path(project, variable)) } it { is_expected.to eq(project_variables_path(project)) }
end end
end end
...@@ -142,12 +142,12 @@ describe API::GroupVariables do ...@@ -142,12 +142,12 @@ describe API::GroupVariables do
end end
it 'updates variable data' do it 'updates variable data' do
initial_variable = group.variables.first initial_variable = group.variables.reload.first
value_before = initial_variable.value value_before = initial_variable.value
put api("/groups/#{group.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true put api("/groups/#{group.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
updated_variable = group.variables.first updated_variable = group.variables.reload.first
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(value_before).to eq(variable.value) expect(value_before).to eq(variable.value)
......
...@@ -122,12 +122,12 @@ describe API::Variables do ...@@ -122,12 +122,12 @@ describe API::Variables do
describe 'PUT /projects/:id/variables/:key' do describe 'PUT /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do context 'authorized user with proper permissions' do
it 'updates variable data' do it 'updates variable data' do
initial_variable = project.variables.first initial_variable = project.variables.reload.first
value_before = initial_variable.value value_before = initial_variable.value
put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
updated_variable = project.variables.first updated_variable = project.variables.reload.first
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(value_before).to eq(variable.value) expect(value_before).to eq(variable.value)
......
require 'spec_helper'
describe GroupVariableEntity do
let(:variable) { create(:ci_group_variable) }
let(:entity) { described_class.new(variable) }
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:id, :key, :value, :protected)
end
end
end
require 'spec_helper'
describe VariableEntity do
let(:variable) { create(:ci_variable) }
let(:entity) { described_class.new(variable) }
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:id, :key, :value, :protected)
end
end
end
shared_examples 'variable list' do
it 'shows list of variables' do
page.within('.js-ci-variable-list-section') do
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
end
end
it 'adds new secret variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
end
end
it 'adds empty variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
end
end
it 'adds new unprotected variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
end
it 'reveals and hides variables' do
page.within('.js-ci-variable-list-section') do
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
expect(page).to have_content('*' * 20)
click_button('Reveal value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value').value).to eq(variable.value)
expect(page).not_to have_content('*' * 20)
click_button('Hide value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
expect(page).to have_content('*' * 20)
end
end
it 'deletes variable' do
page.within('.js-ci-variable-list-section') do
expect(page).to have_selector('.js-row', count: 2)
first('.js-row-remove-button').click
click_button('Save variables')
wait_for_requests
expect(page).to have_selector('.js-row', count: 1)
end
end
it 'edits variable' do
page.within('.js-ci-variable-list-section') do
click_button('Reveal value')
page.within('.js-row:nth-child(1)') do
find('.js-ci-variable-input-key').set('new_key')
find('.js-ci-variable-input-value').set('new_value')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value')
end
end
end
it 'edits variable with empty value' do
page.within('.js-ci-variable-list-section') do
click_button('Reveal value')
page.within('.js-row:nth-child(1)') do
find('.js-ci-variable-input-key').set('new_key')
find('.js-ci-variable-input-value').set('')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
end
end
end
it 'edits variable to be protected' do
# Create the unprotected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('unprotected_key')
find('.js-ci-variable-input-value').set('unprotected_value')
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
wait_for_requests
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
end
it 'edits variable to be unprotected' do
# Create the protected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('protected_key')
find('.js-ci-variable-input-value').set('protected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
click_button('Save variables')
wait_for_requests
visit page_path
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('protected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
end
it 'handles multiple edits and deletion in the middle' do
page.within('.js-ci-variable-list-section') do
# Create 2 variables
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('akey')
find('.js-ci-variable-input-value').set('akeyvalue')
end
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('zkey')
find('.js-ci-variable-input-value').set('zkeyvalue')
end
click_button('Save variables')
wait_for_requests
expect(page).to have_selector('.js-row', count: 4)
# Remove the `akey` variable
page.within('.js-row:nth-child(2)') do
first('.js-row-remove-button').click
end
# Add another variable
page.within('.js-row:last-child') do
find('.js-ci-variable-input-key').set('ckey')
find('.js-ci-variable-input-value').set('ckeyvalue')
end
click_button('Save variables')
wait_for_requests
visit page_path
# Expect to find 3 variables(4 rows) in alphbetical order
expect(page).to have_selector('.js-row', count: 4)
row_keys = all('.js-ci-variable-input-key')
expect(row_keys[0].value).to eq('ckey')
expect(row_keys[1].value).to eq('test_key')
expect(row_keys[2].value).to eq('zkey')
expect(row_keys[3].value).to eq('')
end
end
it 'shows validation error box about duplicate keys' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
find('.js-ci-variable-input-value').set('value1')
end
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
find('.js-ci-variable-input-value').set('value2')
end
click_button('Save variables')
wait_for_requests
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section') do
expect(find('.js-ci-variable-error-box')).to have_content('Validation failed Variables Duplicate variables: samekey')
end
end
end
shared_examples 'GET #show lists all variables' do
it 'renders the variables as json' do
subject
expect(response).to match_response_schema('variables')
end
it 'has only one variable' do
subject
expect(json_response['variables'].count).to eq(1)
end
end
shared_examples 'PATCH #update updates variables' do
let(:variable_attributes) do
{ id: variable.id,
key: variable.key,
value: variable.value,
protected: variable.protected?.to_s }
end
let(:new_variable_attributes) do
{ key: 'new_key',
value: 'dummy_value',
protected: 'false' }
end
context 'with invalid new variable parameters' do
let(:variables_attributes) do
[
variable_attributes.merge(value: 'other_value'),
new_variable_attributes.merge(key: '...?')
]
end
it 'does not update the existing variable' do
expect { subject }.not_to change { variable.reload.value }
end
it 'does not create the new variable' do
expect { subject }.not_to change { owner.variables.count }
end
it 'returns a bad request response' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with duplicate new variable parameters' do
let(:variables_attributes) do
[
new_variable_attributes,
new_variable_attributes.merge(value: 'other_value')
]
end
it 'does not update the existing variable' do
expect { subject }.not_to change { variable.reload.value }
end
it 'does not create the new variable' do
expect { subject }.not_to change { owner.variables.count }
end
it 'returns a bad request response' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with valid new variable parameters' do
let(:variables_attributes) do
[
variable_attributes.merge(value: 'other_value'),
new_variable_attributes
]
end
it 'updates the existing variable' do
expect { subject }.to change { variable.reload.value }.to('other_value')
end
it 'creates the new variable' do
expect { subject }.to change { owner.variables.count }.by(1)
end
it 'returns a successful response' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has all variables in response' do
subject
expect(response).to match_response_schema('variables')
end
end
context 'with a deleted variable' do
let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] }
it 'destroys the variable' do
expect { subject }.to change { owner.variables.count }.by(-1)
expect { variable.reload }.to raise_error ActiveRecord::RecordNotFound
end
it 'returns a successful response' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has all variables in response' do
subject
expect(response).to match_response_schema('variables')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment