From fe96a1f268558526fd122a348866fbf513ac17e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 12 Jan 2018 22:39:42 +0100 Subject: [PATCH 01/34] Stub multiple variable controller method --- app/controllers/projects/variables_controller.rb | 4 ++++ config/routes/project.rb | 7 ++++++- spec/controllers/projects/variables_controller_spec.rb | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 6a825137564..b7b88830837 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -32,6 +32,10 @@ class Projects::VariablesController < Projects::ApplicationController end end + def save_multiple + head :ok + end + def destroy if variable.destroy redirect_to project_settings_ci_cd_path(project), diff --git a/config/routes/project.rb b/config/routes/project.rb index bcaa68c8ce5..8b65240ade4 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -156,7 +156,12 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :variables, only: [:index, :show, :update, :create, :destroy] + resources :variables, only: [:index, :show, :update, :create, :destroy] do + collection do + post :save_multiple + end + end + resources :triggers, only: [:index, :create, :edit, :update, :destroy] do member do post :take_ownership diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 9fde6544215..97284909b3c 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -55,4 +55,11 @@ describe Projects::VariablesController do end end end + + describe 'POST #save_multiple' do + it 'returns a successful response' do + post :save_multiple, namespace_id: project.namespace.to_param, project_id: project + expect(response).to have_gitlab_http_status(:ok) + end + end end From 121d84d774e18b27a8a4624f173e97cfad0d7f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Sat, 13 Jan 2018 01:46:43 +0100 Subject: [PATCH 02/34] Implement multiple variable handling action --- .../projects/variables_controller.rb | 19 ++++++- .../projects/variables_controller_spec.rb | 55 ++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index b7b88830837..f9d548a14f8 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -33,7 +33,20 @@ class Projects::VariablesController < Projects::ApplicationController end def save_multiple - head :ok + respond_to do |format| + format.json do + variables = [] + variables_params[:variables].each do |variable_hash| + variable = project.variables.where(key: variable_hash[:key]).first_or_initialize(variable_hash) + variable.assign_attributes(variable_hash) unless variable.new_record? + return head :bad_request unless variable.valid? + + variables << variable + end + variables.each { |variable| variable.save } + end + head :ok + end end def destroy @@ -54,6 +67,10 @@ class Projects::VariablesController < Projects::ApplicationController params.require(:variable).permit(*variable_params_attributes) end + def variables_params + params.permit(variables: [*variable_params_attributes]) + end + def variable_params_attributes %i[id key value protected _destroy] end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 97284909b3c..e0294fd4a46 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -57,9 +57,58 @@ describe Projects::VariablesController do end describe 'POST #save_multiple' do - it 'returns a successful response' do - post :save_multiple, namespace_id: project.namespace.to_param, project_id: project - expect(response).to have_gitlab_http_status(:ok) + let(:variable) { create(:ci_variable) } + + before do + project.variables << variable + end + + context 'with invalid new variable parameters' do + subject do + post :save_multiple, + namespace_id: project.namespace.to_param, project_id: project, + variables: [{ key: variable.key, value: 'other_value' }, + { key: '..?', value: 'dummy_value' }], + format: :json + 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 { project.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 + subject do + post :save_multiple, + namespace_id: project.namespace.to_param, project_id: project, + variables: [{ key: variable.key, value: 'other_value' }, + { key: 'new_key', value: 'dummy_value' }], + format: :json + 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 { project.variables.count }.by(1) + end + + it 'returns a successful response' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end end end end From b91539d68fa21979001e122c912fad1c31dfd5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Sat, 13 Jan 2018 02:10:04 +0100 Subject: [PATCH 03/34] Refactor VariablesController#save_multiple --- .../projects/variables_controller.rb | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index f9d548a14f8..9aacb53078a 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -35,17 +35,12 @@ class Projects::VariablesController < Projects::ApplicationController def save_multiple respond_to do |format| format.json do - variables = [] - variables_params[:variables].each do |variable_hash| - variable = project.variables.where(key: variable_hash[:key]).first_or_initialize(variable_hash) - variable.assign_attributes(variable_hash) unless variable.new_record? - return head :bad_request unless variable.valid? + variables = variables_from_params(variables_params) + return head :bad_request unless variables.all?(&:valid?) - variables << variable - end - variables.each { |variable| variable.save } + variables.each(&:save) + head :ok end - head :ok end end @@ -71,6 +66,15 @@ class Projects::VariablesController < Projects::ApplicationController params.permit(variables: [*variable_params_attributes]) end + def variables_from_params(params) + params[:variables].map do |variable_hash| + variable = project.variables.where(key: variable_hash[:key]) + .first_or_initialize(variable_hash) + variable.assign_attributes(variable_hash) unless variable.new_record? + variable + end + end + def variable_params_attributes %i[id key value protected _destroy] end From c64181ce4c57bc7ffbbcc51ee9bf0001bf83e1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Sat, 13 Jan 2018 02:52:49 +0100 Subject: [PATCH 04/34] Move variable loading into before_action --- .../projects/variables_controller.rb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 9aacb53078a..cd68018e033 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,5 +1,6 @@ class Projects::VariablesController < Projects::ApplicationController before_action :variable, only: [:show, :update, :destroy] + before_action :variables, only: [:save_multiple] before_action :authorize_admin_build! layout 'project_settings' @@ -35,10 +36,9 @@ class Projects::VariablesController < Projects::ApplicationController def save_multiple respond_to do |format| format.json do - variables = variables_from_params(variables_params) - return head :bad_request unless variables.all?(&:valid?) + return head :bad_request unless @variables.all?(&:valid?) - variables.each(&:save) + @variables.each(&:save) head :ok end end @@ -66,15 +66,6 @@ class Projects::VariablesController < Projects::ApplicationController params.permit(variables: [*variable_params_attributes]) end - def variables_from_params(params) - params[:variables].map do |variable_hash| - variable = project.variables.where(key: variable_hash[:key]) - .first_or_initialize(variable_hash) - variable.assign_attributes(variable_hash) unless variable.new_record? - variable - end - end - def variable_params_attributes %i[id key value protected _destroy] end @@ -82,4 +73,13 @@ class Projects::VariablesController < Projects::ApplicationController def variable @variable ||= project.variables.find(params[:id]).present(current_user: current_user) end + + def variables + @variables = variables_params[:variables].map do |variable_hash| + variable = project.variables.where(key: variable_hash[:key]) + .first_or_initialize(variable_hash).present(current_user: current_user) + variable.assign_attributes(variable_hash) unless variable.new_record? + variable + end + end end From ba077841922089c0eb2bbb48947de8828f891776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Sat, 13 Jan 2018 03:36:04 +0100 Subject: [PATCH 05/34] Add destroy functionality to save_multiple --- .../projects/variables_controller.rb | 11 ++++++++-- .../projects/variables_controller_spec.rb | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index cd68018e033..dd514d500d0 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -39,6 +39,7 @@ class Projects::VariablesController < Projects::ApplicationController return head :bad_request unless @variables.all?(&:valid?) @variables.each(&:save) + @variables_destroy.each(&:destroy) head :ok end end @@ -75,10 +76,16 @@ class Projects::VariablesController < Projects::ApplicationController end def variables - @variables = variables_params[:variables].map do |variable_hash| + destroy, edit = variables_params[:variables].partition { |hash| hash[:_destroy] == 'true' } + @variables = initialize_or_update_variables_from_hash(edit) + @variables_destroy = initialize_or_update_variables_from_hash(destroy) + end + + def initialize_or_update_variables_from_hash(hash) + hash.map do |variable_hash| variable = project.variables.where(key: variable_hash[:key]) .first_or_initialize(variable_hash).present(current_user: current_user) - variable.assign_attributes(variable_hash) unless variable.new_record? + variable.assign_attributes(variable_hash.except(:_destroy)) unless variable.new_record? variable end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index e0294fd4a46..87463f00b8f 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -110,5 +110,25 @@ describe Projects::VariablesController do expect(response).to have_gitlab_http_status(:ok) end end + + context 'with a deleted variable' do + subject do + post :save_multiple, + namespace_id: project.namespace.to_param, project_id: project, + variables: [{ key: variable.key, value: variable.value, _destroy: 'true' }], + format: :json + end + + it 'destroys the variable' do + expect { subject }.to change { project.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 + end end end From 5de85708ce17c1965581b8bf7563751dda77510d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 01:17:40 +0100 Subject: [PATCH 06/34] Use nested attributes for updating multiple variables --- .../projects/variables_controller.rb | 24 +++---------------- .../projects/variables_controller_spec.rb | 11 +++++---- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index dd514d500d0..f916c545fab 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,6 +1,5 @@ class Projects::VariablesController < Projects::ApplicationController before_action :variable, only: [:show, :update, :destroy] - before_action :variables, only: [:save_multiple] before_action :authorize_admin_build! layout 'project_settings' @@ -36,11 +35,9 @@ class Projects::VariablesController < Projects::ApplicationController def save_multiple respond_to do |format| format.json do - return head :bad_request unless @variables.all?(&:valid?) + return head :ok if @project.update(variables_params) - @variables.each(&:save) - @variables_destroy.each(&:destroy) - head :ok + head :bad_request end end end @@ -64,7 +61,7 @@ class Projects::VariablesController < Projects::ApplicationController end def variables_params - params.permit(variables: [*variable_params_attributes]) + params.permit(variables_attributes: [*variable_params_attributes]) end def variable_params_attributes @@ -74,19 +71,4 @@ class Projects::VariablesController < Projects::ApplicationController def variable @variable ||= project.variables.find(params[:id]).present(current_user: current_user) end - - def variables - destroy, edit = variables_params[:variables].partition { |hash| hash[:_destroy] == 'true' } - @variables = initialize_or_update_variables_from_hash(edit) - @variables_destroy = initialize_or_update_variables_from_hash(destroy) - end - - def initialize_or_update_variables_from_hash(hash) - hash.map do |variable_hash| - variable = project.variables.where(key: variable_hash[:key]) - .first_or_initialize(variable_hash).present(current_user: current_user) - variable.assign_attributes(variable_hash.except(:_destroy)) unless variable.new_record? - variable - end - end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 87463f00b8f..946110c0e64 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -67,8 +67,8 @@ describe Projects::VariablesController do subject do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, - variables: [{ key: variable.key, value: 'other_value' }, - { key: '..?', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, + { key: '..?', value: 'dummy_value' }], format: :json end @@ -91,8 +91,8 @@ describe Projects::VariablesController do subject do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, - variables: [{ key: variable.key, value: 'other_value' }, - { key: 'new_key', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, + { key: 'new_key', value: 'dummy_value' }], format: :json end @@ -115,7 +115,8 @@ describe Projects::VariablesController do subject do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, - variables: [{ key: variable.key, value: variable.value, _destroy: 'true' }], + variables_attributes: [{ id: variable.id, key: variable.key, + value: variable.value, _destroy: 'true' }], format: :json end From edbe911b04465f0e6c72e102d083d0e85848a552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 02:09:36 +0100 Subject: [PATCH 07/34] Remove redundant routes in VariablesController --- .../projects/variables_controller.rb | 51 ------------------- config/routes/project.rb | 6 +-- .../projects/variables_controller_spec.rb | 47 ----------------- 3 files changed, 2 insertions(+), 102 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index f916c545fab..8bee1b97d14 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,37 +1,6 @@ class Projects::VariablesController < Projects::ApplicationController - before_action :variable, only: [:show, :update, :destroy] before_action :authorize_admin_build! - layout 'project_settings' - - def index - redirect_to project_settings_ci_cd_path(@project) - end - - def show - end - - def update - if variable.update(variable_params) - redirect_to project_variables_path(project), - notice: 'Variable was successfully updated.' - else - render "show" - end - end - - def create - @variable = project.variables.create(variable_params) - .present(current_user: current_user) - - if @variable.persisted? - redirect_to project_settings_ci_cd_path(project), - notice: 'Variable was successfully created.' - else - render "show" - end - end - def save_multiple respond_to do |format| format.json do @@ -42,24 +11,8 @@ class Projects::VariablesController < Projects::ApplicationController end end - def destroy - if variable.destroy - 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 - private - def variable_params - params.require(:variable).permit(*variable_params_attributes) - end - def variables_params params.permit(variables_attributes: [*variable_params_attributes]) end @@ -67,8 +20,4 @@ class Projects::VariablesController < Projects::ApplicationController def variable_params_attributes %i[id key value protected _destroy] end - - def variable - @variable ||= project.variables.find(params[:id]).present(current_user: current_user) - end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 8b65240ade4..b8d09f01ae1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -156,10 +156,8 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :variables, only: [:index, :show, :update, :create, :destroy] do - collection do - post :save_multiple - end + namespace :variables do + post :save_multiple end resources :triggers, only: [:index, :create, :edit, :update, :destroy] do diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 946110c0e64..4cbc2fb443a 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -9,53 +9,6 @@ describe Projects::VariablesController do project.add_master(user) end - describe 'POST #create' do - context 'variable is valid' do - 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") - end - end - end - - describe 'POST #update' do - let(:variable) { create(:ci_variable) } - - context 'updating a variable with valid characters' do - before do - 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 - post :update, namespace_id: project.namespace.to_param, project_id: project, - id: variable.id, variable: { key: '?', value: variable.value } - - expect(response).to have_gitlab_http_status(200) - expect(response).to render_template :show - end - end - end - describe 'POST #save_multiple' do let(:variable) { create(:ci_variable) } From cc2bed9283039db726f42184ea635f057534205f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 19:24:55 +0100 Subject: [PATCH 08/34] Port #save_multiple to Groups::VariablesController --- .../groups/variables_controller.rb | 16 +++- app/models/group.rb | 2 + config/routes/group.rb | 6 +- .../groups/variables_controller_spec.rb | 79 +++++++++++++++++-- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 10038ff3ad9..3c303b64b65 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -31,6 +31,16 @@ module Groups end end + def save_multiple + respond_to do |format| + format.json do + return head :ok if @group.update(variables_params) + + head :bad_request + end + end + end + def destroy if variable.destroy redirect_to group_settings_ci_cd_path(group), @@ -49,8 +59,12 @@ module Groups params.require(:variable).permit(*variable_params_attributes) end + def variables_params + params.permit(variables_attributes: [*variable_params_attributes]) + end + def variable_params_attributes - %i[key value protected] + %i[id key value protected _destroy] end def variable diff --git a/app/models/group.rb b/app/models/group.rb index 5b7f1b38612..29df4144d03 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -31,6 +31,8 @@ class Group < Namespace 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_sub_groups validate :visibility_level_allowed_by_parent diff --git a/config/routes/group.rb b/config/routes/group.rb index 24c76bc55ab..cdf2647415d 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -27,7 +27,11 @@ constraints(GroupUrlConstrainer.new) do resource :ci_cd, only: [:show], controller: 'ci_cd' end - resources :variables, only: [:index, :show, :update, :create, :destroy] + resources :variables, only: [:index, :show, :update, :create, :destroy] do + collection do + post :save_multiple + end + end resources :children, only: [:index] diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 8ea98cd9e8f..294e712f45d 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -29,13 +29,9 @@ describe Groups::VariablesController do end describe 'POST #update' do - let(:variable) { create(:ci_group_variable) } + let!(:variable) { create(:ci_group_variable, group: group) } context 'updating a variable with valid characters' do - before do - 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' } @@ -53,4 +49,77 @@ describe Groups::VariablesController do end end end + + describe 'POST #save_multiple' do + let!(:variable) { create(:ci_group_variable, group: group) } + + context 'with invalid new variable parameters' do + subject do + post :save_multiple, + group_id: group, + variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, + { key: '..?', value: 'dummy_value' }], + format: :json + 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 { group.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 + subject do + post :save_multiple, + group_id: group, + variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, + { key: 'new_key', value: 'dummy_value' }], + format: :json + 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 { group.variables.count }.by(1) + end + + it 'returns a successful response' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with a deleted variable' do + subject do + post :save_multiple, + group_id: group, + variables_attributes: [{ id: variable.id, key: variable.key, + value: variable.value, _destroy: 'true' }], + format: :json + end + + it 'destroys the variable' do + expect { subject }.to change { group.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 + end + end end From dcc23530bd1e2bd1600bd523c90c040f84f8c354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 19:34:06 +0100 Subject: [PATCH 09/34] Remove redundant routes in Groups::VariablesController --- .../groups/variables_controller.rb | 49 ------------------- config/routes/group.rb | 6 +-- .../groups/variables_controller_spec.rb | 41 ---------------- 3 files changed, 2 insertions(+), 94 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 3c303b64b65..5fbf532b98e 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -1,36 +1,7 @@ module Groups class VariablesController < Groups::ApplicationController - before_action :variable, only: [:show, :update, :destroy] before_action :authorize_admin_build! - def index - redirect_to group_settings_ci_cd_path(group) - end - - def show - end - - def update - if variable.update(variable_params) - redirect_to group_variables_path(group), - notice: 'Variable was successfully updated.' - else - render "show" - end - end - - def create - @variable = group.variables.create(variable_params) - .present(current_user: current_user) - - if @variable.persisted? - redirect_to group_settings_ci_cd_path(group), - notice: 'Variable was successfully created.' - else - render "show" - end - end - def save_multiple respond_to do |format| format.json do @@ -41,24 +12,8 @@ module Groups end end - def destroy - if variable.destroy - 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 - private - def variable_params - params.require(:variable).permit(*variable_params_attributes) - end - def variables_params params.permit(variables_attributes: [*variable_params_attributes]) end @@ -67,10 +22,6 @@ module Groups %i[id key value protected _destroy] end - def variable - @variable ||= group.variables.find(params[:id]).present(current_user: current_user) - end - def authorize_admin_build! return render_404 unless can?(current_user, :admin_build, group) end diff --git a/config/routes/group.rb b/config/routes/group.rb index cdf2647415d..b3afbb4152f 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -27,10 +27,8 @@ constraints(GroupUrlConstrainer.new) do resource :ci_cd, only: [:show], controller: 'ci_cd' end - resources :variables, only: [:index, :show, :update, :create, :destroy] do - collection do - post :save_multiple - end + namespace :variables do + post :save_multiple end resources :children, only: [:index] diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 294e712f45d..8570df15f34 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -9,47 +9,6 @@ describe Groups::VariablesController do group.add_master(user) end - describe 'POST #create' do - context 'variable is valid' do - 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") - end - end - end - - describe 'POST #update' do - let!(:variable) { create(:ci_group_variable, group: group) } - - context 'updating a variable with valid characters' do - 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 - post :update, group_id: group, - id: variable.id, variable: { key: '?', value: variable.value } - - expect(response).to have_gitlab_http_status(200) - expect(response).to render_template :show - end - end - end - describe 'POST #save_multiple' do let!(:variable) { create(:ci_group_variable, group: group) } From 2592aec9e3a7a1b0d2689dd14cc6e0b9cea068cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 19:53:29 +0100 Subject: [PATCH 10/34] Use all parameters in VariablesController specs --- .../groups/variables_controller_spec.rb | 18 +++++++++++++----- .../projects/variables_controller_spec.rb | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 8570df15f34..4299c3b0040 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -16,8 +16,11 @@ describe Groups::VariablesController do subject do post :save_multiple, group_id: group, - variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, - { key: '..?', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, + value: 'other_value', + protected: variable.protected?.to_s }, + { key: '..?', value: 'dummy_value', + protected: 'false' }], format: :json end @@ -40,8 +43,11 @@ describe Groups::VariablesController do subject do post :save_multiple, group_id: group, - variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, - { key: 'new_key', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, + value: 'other_value', + protected: variable.protected?.to_s }, + { key: 'new_key', value: 'dummy_value', + protected: 'false' }], format: :json end @@ -65,7 +71,9 @@ describe Groups::VariablesController do post :save_multiple, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, - value: variable.value, _destroy: 'true' }], + value: variable.value, + protected: variable.protected?.to_s, + _destroy: 'true' }], format: :json end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 4cbc2fb443a..ac5c453a8ab 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -20,8 +20,11 @@ describe Projects::VariablesController do subject do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, - { key: '..?', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, + value: 'other_value', + protected: variable.protected?.to_s }, + { key: '..?', value: 'dummy_value', + protected: 'false' }], format: :json end @@ -44,8 +47,11 @@ describe Projects::VariablesController do subject do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value' }, - { key: 'new_key', value: 'dummy_value' }], + variables_attributes: [{ id: variable.id, key: variable.key, + value: 'other_value', + protected: variable.protected?.to_s }, + { key: 'new_key', value: 'dummy_value', + protected: 'false' }], format: :json end @@ -69,7 +75,9 @@ describe Projects::VariablesController do post :save_multiple, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, - value: variable.value, _destroy: 'true' }], + value: variable.value, + protected: variable.protected?.to_s, + _destroy: 'true' }], format: :json end From 1292c158ce909e9054f842a90095810c0d464e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 23 Jan 2018 21:26:30 +0100 Subject: [PATCH 11/34] Update CI Variable presenters paths --- app/presenters/ci/group_variable_presenter.rb | 10 +++------- app/presenters/ci/variable_presenter.rb | 10 +++------- .../ci/group_variable_presenter_spec.rb | 17 ++++------------- spec/presenters/ci/variable_presenter_spec.rb | 17 ++++------------- 4 files changed, 14 insertions(+), 40 deletions(-) diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb index 81fea106a5c..fb6dfcc1e4d 100644 --- a/app/presenters/ci/group_variable_presenter.rb +++ b/app/presenters/ci/group_variable_presenter.rb @@ -7,19 +7,15 @@ module Ci end def form_path - if variable.persisted? - group_variable_path(group, variable) - else - group_variables_path(group) - end + group_settings_ci_cd_path(group) end def edit_path - group_variable_path(group, variable) + group_variables_save_multiple_path(group) end def delete_path - group_variable_path(group, variable) + group_variables_save_multiple_path(group) end end end diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb index 5d7998393a6..2e8f069646a 100644 --- a/app/presenters/ci/variable_presenter.rb +++ b/app/presenters/ci/variable_presenter.rb @@ -7,19 +7,15 @@ module Ci end def form_path - if variable.persisted? - project_variable_path(project, variable) - else - project_variables_path(project) - end + project_settings_ci_cd_path(project) end def edit_path - project_variable_path(project, variable) + project_variables_save_multiple_path(project) end def delete_path - project_variable_path(project, variable) + project_variables_save_multiple_path(project) end end end diff --git a/spec/presenters/ci/group_variable_presenter_spec.rb b/spec/presenters/ci/group_variable_presenter_spec.rb index d404028405b..d20fae47939 100644 --- a/spec/presenters/ci/group_variable_presenter_spec.rb +++ b/spec/presenters/ci/group_variable_presenter_spec.rb @@ -35,29 +35,20 @@ describe Ci::GroupVariablePresenter do end 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)) } - 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 + it { is_expected.to eq(group_settings_ci_cd_path(group)) } end describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(group_variable_path(group, variable)) } + it { is_expected.to eq(group_variables_save_multiple_path(group)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(group_variable_path(group, variable)) } + it { is_expected.to eq(group_variables_save_multiple_path(group)) } end end diff --git a/spec/presenters/ci/variable_presenter_spec.rb b/spec/presenters/ci/variable_presenter_spec.rb index db62f86edb0..35ad49817b8 100644 --- a/spec/presenters/ci/variable_presenter_spec.rb +++ b/spec/presenters/ci/variable_presenter_spec.rb @@ -35,29 +35,20 @@ describe Ci::VariablePresenter do end 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)) } - 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 + it { is_expected.to eq(project_settings_ci_cd_path(project)) } end describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(project_variable_path(project, variable)) } + it { is_expected.to eq(project_variables_save_multiple_path(project)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(project_variable_path(project, variable)) } + it { is_expected.to eq(project_variables_save_multiple_path(project)) } end end From bf2a040cf9fb8e0eb3576732f3cda6fe6326e65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 25 Jan 2018 03:50:38 +0100 Subject: [PATCH 12/34] Pass validation errors in JSON endpoint --- app/controllers/groups/variables_controller.rb | 2 +- app/controllers/projects/variables_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 5fbf532b98e..3a832a7c005 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -7,7 +7,7 @@ module Groups format.json do return head :ok if @group.update(variables_params) - head :bad_request + render status: :bad_request, json: @group.errors.to_hash end end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 8bee1b97d14..9c0dad393c3 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -6,7 +6,7 @@ class Projects::VariablesController < Projects::ApplicationController format.json do return head :ok if @project.update(variables_params) - head :bad_request + render status: :bad_request, json: @project.errors.to_hash end end end From 6b82a9ef51f59d37975bd5de48142d1a0a8504de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 25 Jan 2018 12:59:52 +0100 Subject: [PATCH 13/34] Format validation errors as human readable messages --- app/controllers/groups/variables_controller.rb | 2 +- app/controllers/projects/variables_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 3a832a7c005..2f7058e95ea 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -7,7 +7,7 @@ module Groups format.json do return head :ok if @group.update(variables_params) - render status: :bad_request, json: @group.errors.to_hash + render status: :bad_request, json: @group.errors.full_messages end end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 9c0dad393c3..99dea65927d 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -6,7 +6,7 @@ class Projects::VariablesController < Projects::ApplicationController format.json do return head :ok if @project.update(variables_params) - render status: :bad_request, json: @project.errors.to_hash + render status: :bad_request, json: @project.errors.full_messages end end end From 0bfcdd66bf932c080398ff264323b5c0df17d05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 29 Jan 2018 18:39:06 +0100 Subject: [PATCH 14/34] Use `resource` in Project Variables routing scheme --- .../projects/variables_controller.rb | 13 ++++++++- app/presenters/ci/variable_presenter.rb | 4 +-- config/routes/project.rb | 4 +-- .../projects/variables_controller_spec.rb | 27 ++++++++++++++++--- spec/presenters/ci/variable_presenter_spec.rb | 4 +-- 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 99dea65927d..2f03603bd1d 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,7 +1,18 @@ class Projects::VariablesController < Projects::ApplicationController before_action :authorize_admin_build! - def save_multiple + def show + respond_to do |format| + format.json do + variables = @project.variables + .map { |variable| variable.present(current_user: current_user) } + + render status: :ok, json: { variables: variables } + end + end + end + + def update respond_to do |format| format.json do return head :ok if @project.update(variables_params) diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb index 2e8f069646a..96159f88c59 100644 --- a/app/presenters/ci/variable_presenter.rb +++ b/app/presenters/ci/variable_presenter.rb @@ -11,11 +11,11 @@ module Ci end def edit_path - project_variables_save_multiple_path(project) + project_variables_path(project) end def delete_path - project_variables_save_multiple_path(project) + project_variables_path(project) end end end diff --git a/config/routes/project.rb b/config/routes/project.rb index b8d09f01ae1..1912808f9c0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -156,9 +156,7 @@ constraints(ProjectUrlConstrainer.new) do end end - namespace :variables do - post :save_multiple - end + resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index ac5c453a8ab..f6e15ee6147 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -9,7 +9,26 @@ describe Projects::VariablesController do project.add_master(user) end - describe 'POST #save_multiple' do + describe 'GET #show' do + let(:variable) { create(:ci_variable) } + + before do + project.variables << variable + end + + subject do + get :show, namespace_id: project.namespace.to_param, project_id: project, + format: :json + end + + it 'renders the ci_variable as json' do + subject + + expect(response.body).to include(variable.to_json) + end + end + + describe 'POST #update' do let(:variable) { create(:ci_variable) } before do @@ -18,7 +37,7 @@ describe Projects::VariablesController do context 'with invalid new variable parameters' do subject do - post :save_multiple, + post :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -45,7 +64,7 @@ describe Projects::VariablesController do context 'with valid new variable parameters' do subject do - post :save_multiple, + post :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -72,7 +91,7 @@ describe Projects::VariablesController do context 'with a deleted variable' do subject do - post :save_multiple, + post :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: variable.value, diff --git a/spec/presenters/ci/variable_presenter_spec.rb b/spec/presenters/ci/variable_presenter_spec.rb index 35ad49817b8..e3ce88372ea 100644 --- a/spec/presenters/ci/variable_presenter_spec.rb +++ b/spec/presenters/ci/variable_presenter_spec.rb @@ -43,12 +43,12 @@ describe Ci::VariablePresenter do describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(project_variables_save_multiple_path(project)) } + it { is_expected.to eq(project_variables_path(project)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(project_variables_save_multiple_path(project)) } + it { is_expected.to eq(project_variables_path(project)) } end end From a8887a0d9c4a41a0707b92189572aeff10566af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 29 Jan 2018 18:54:16 +0100 Subject: [PATCH 15/34] Use `resource` in Group Variables routing scheme --- .../groups/variables_controller.rb | 13 ++++++++++- app/presenters/ci/group_variable_presenter.rb | 4 ++-- config/routes/group.rb | 4 +--- .../groups/variables_controller_spec.rb | 22 +++++++++++++++---- .../ci/group_variable_presenter_spec.rb | 4 ++-- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 2f7058e95ea..c0eff63da18 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -2,7 +2,18 @@ module Groups class VariablesController < Groups::ApplicationController before_action :authorize_admin_build! - def save_multiple + def show + respond_to do |format| + format.json do + variables = @group.variables + .map { |variable| variable.present(current_user: current_user) } + + render status: :ok, json: { variables: variables } + end + end + end + + def update respond_to do |format| format.json do return head :ok if @group.update(variables_params) diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb index fb6dfcc1e4d..98d68bc7a83 100644 --- a/app/presenters/ci/group_variable_presenter.rb +++ b/app/presenters/ci/group_variable_presenter.rb @@ -11,11 +11,11 @@ module Ci end def edit_path - group_variables_save_multiple_path(group) + group_variables_path(group) end def delete_path - group_variables_save_multiple_path(group) + group_variables_path(group) end end end diff --git a/config/routes/group.rb b/config/routes/group.rb index b3afbb4152f..ac22b636372 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -27,9 +27,7 @@ constraints(GroupUrlConstrainer.new) do resource :ci_cd, only: [:show], controller: 'ci_cd' end - namespace :variables do - post :save_multiple - end + resource :variables, only: [:show, :update] resources :children, only: [:index] diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 4299c3b0040..96f1dc4d0ce 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -9,12 +9,26 @@ describe Groups::VariablesController do group.add_master(user) end - describe 'POST #save_multiple' do + describe 'GET #show' do + let!(:variable) { create(:ci_group_variable, group: group) } + + subject do + get :show, group_id: group, format: :json + end + + it 'renders the ci_variable as json' do + subject + + expect(response.body).to include(variable.to_json) + end + end + + describe 'POST #update' do let!(:variable) { create(:ci_group_variable, group: group) } context 'with invalid new variable parameters' do subject do - post :save_multiple, + post :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -41,7 +55,7 @@ describe Groups::VariablesController do context 'with valid new variable parameters' do subject do - post :save_multiple, + post :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -68,7 +82,7 @@ describe Groups::VariablesController do context 'with a deleted variable' do subject do - post :save_multiple, + post :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: variable.value, diff --git a/spec/presenters/ci/group_variable_presenter_spec.rb b/spec/presenters/ci/group_variable_presenter_spec.rb index d20fae47939..cb58a757564 100644 --- a/spec/presenters/ci/group_variable_presenter_spec.rb +++ b/spec/presenters/ci/group_variable_presenter_spec.rb @@ -43,12 +43,12 @@ describe Ci::GroupVariablePresenter do describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(group_variables_save_multiple_path(group)) } + it { is_expected.to eq(group_variables_path(group)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(group_variables_save_multiple_path(group)) } + it { is_expected.to eq(group_variables_path(group)) } end end From 13f0e18d2f85478c1f3e6a2851701e88d6f8378c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 29 Jan 2018 19:01:21 +0100 Subject: [PATCH 16/34] Fix a typo in Groups::VariablesController spec --- spec/controllers/groups/variables_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 96f1dc4d0ce..72a489febdb 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -16,7 +16,7 @@ describe Groups::VariablesController do get :show, group_id: group, format: :json end - it 'renders the ci_variable as json' do + it 'renders the ci_group_variable as json' do subject expect(response.body).to include(variable.to_json) From 9eb3bb5cffbd18a744be2553ab2be7cb7843b029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 30 Jan 2018 00:29:13 +0100 Subject: [PATCH 17/34] Change POST to PATCH requests in the controller specs --- spec/controllers/groups/variables_controller_spec.rb | 6 +++--- spec/controllers/projects/variables_controller_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 72a489febdb..a462d64c0f5 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -28,7 +28,7 @@ describe Groups::VariablesController do context 'with invalid new variable parameters' do subject do - post :update, + patch :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -55,7 +55,7 @@ describe Groups::VariablesController do context 'with valid new variable parameters' do subject do - post :update, + patch :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -82,7 +82,7 @@ describe Groups::VariablesController do context 'with a deleted variable' do subject do - post :update, + patch :update, group_id: group, variables_attributes: [{ id: variable.id, key: variable.key, value: variable.value, diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index f6e15ee6147..ab8915c58d0 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -37,7 +37,7 @@ describe Projects::VariablesController do context 'with invalid new variable parameters' do subject do - post :update, + patch :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -64,7 +64,7 @@ describe Projects::VariablesController do context 'with valid new variable parameters' do subject do - post :update, + patch :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: 'other_value', @@ -91,7 +91,7 @@ describe Projects::VariablesController do context 'with a deleted variable' do subject do - post :update, + patch :update, namespace_id: project.namespace.to_param, project_id: project, variables_attributes: [{ id: variable.id, key: variable.key, value: variable.value, From b48d8c8ad0bb2874db6b4c9accb3bebd19e9f2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 30 Jan 2018 00:44:53 +0100 Subject: [PATCH 18/34] Return all variables after UPDATE --- app/controllers/groups/variables_controller.rb | 7 ++++++- app/controllers/projects/variables_controller.rb | 7 ++++++- spec/controllers/groups/variables_controller_spec.rb | 12 ++++++++++++ .../projects/variables_controller_spec.rb | 12 ++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index c0eff63da18..afa98aa8267 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -16,7 +16,12 @@ module Groups def update respond_to do |format| format.json do - return head :ok if @group.update(variables_params) + if @group.update(variables_params) + variables = @group.variables + .map { |variable| variable.present(current_user: current_user) } + + return render status: :ok, json: { variables: variables } + end render status: :bad_request, json: @group.errors.full_messages end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 2f03603bd1d..58fa600eb34 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -15,7 +15,12 @@ class Projects::VariablesController < Projects::ApplicationController def update respond_to do |format| format.json do - return head :ok if @project.update(variables_params) + if @project.update(variables_params) + variables = @project.variables + .map { |variable| variable.present(current_user: current_user) } + + return render status: :ok, json: { variables: variables } + end render status: :bad_request, json: @project.errors.full_messages end diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index a462d64c0f5..4927a9ee402 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -78,6 +78,12 @@ describe Groups::VariablesController do expect(response).to have_gitlab_http_status(:ok) end + + it 'has all variables in response' do + subject + + expect(response.body).to include(group.variables.reload.to_json) + end end context 'with a deleted variable' do @@ -101,6 +107,12 @@ describe Groups::VariablesController do expect(response).to have_gitlab_http_status(:ok) end + + it 'has all variables in response' do + subject + + expect(response.body).to include(group.variables.reload.to_json) + end end end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index ab8915c58d0..ddba6cd83f4 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -87,6 +87,12 @@ describe Projects::VariablesController do expect(response).to have_gitlab_http_status(:ok) end + + it 'has all variables in response' do + subject + + expect(response.body).to include(project.variables.reload.to_json) + end end context 'with a deleted variable' do @@ -110,6 +116,12 @@ describe Projects::VariablesController do expect(response).to have_gitlab_http_status(:ok) end + + it 'has all variables in response' do + subject + + expect(response.body).to include(project.variables.reload.to_json) + end end end end From 9be519c199b01d4b4b1c69ec4d74a1e99345eb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 30 Jan 2018 02:01:53 +0100 Subject: [PATCH 19/34] Add VariableSerializer for Ci::Variable --- app/controllers/projects/variables_controller.rb | 4 ++-- app/serializers/variable_entity.rb | 7 +++++++ app/serializers/variable_serializer.rb | 3 +++ .../projects/variables_controller_spec.rb | 6 +++--- spec/fixtures/api/schemas/variable.json | 6 ++++++ spec/serializers/variable_entity_spec.rb | 14 ++++++++++++++ 6 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 app/serializers/variable_entity.rb create mode 100644 app/serializers/variable_serializer.rb create mode 100644 spec/fixtures/api/schemas/variable.json create mode 100644 spec/serializers/variable_entity_spec.rb diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 58fa600eb34..b5635ca1b3b 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -7,7 +7,7 @@ class Projects::VariablesController < Projects::ApplicationController variables = @project.variables .map { |variable| variable.present(current_user: current_user) } - render status: :ok, json: { variables: variables } + render status: :ok, json: { variables: VariableSerializer.new.represent(variables) } end end end @@ -19,7 +19,7 @@ class Projects::VariablesController < Projects::ApplicationController variables = @project.variables .map { |variable| variable.present(current_user: current_user) } - return render status: :ok, json: { variables: variables } + return render status: :ok, json: { variables: VariableSerializer.new.represent(variables) } end render status: :bad_request, json: @project.errors.full_messages diff --git a/app/serializers/variable_entity.rb b/app/serializers/variable_entity.rb new file mode 100644 index 00000000000..d576745c073 --- /dev/null +++ b/app/serializers/variable_entity.rb @@ -0,0 +1,7 @@ +class VariableEntity < Grape::Entity + expose :id + expose :key + expose :value + + expose :protected?, as: :protected +end diff --git a/app/serializers/variable_serializer.rb b/app/serializers/variable_serializer.rb new file mode 100644 index 00000000000..32ae82ab51c --- /dev/null +++ b/app/serializers/variable_serializer.rb @@ -0,0 +1,3 @@ +class VariableSerializer < BaseSerializer + entity VariableEntity +end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index ddba6cd83f4..8e5d6023b79 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -24,7 +24,7 @@ describe Projects::VariablesController do it 'renders the ci_variable as json' do subject - expect(response.body).to include(variable.to_json) + expect(response).to match_response_schema('variable') end end @@ -91,7 +91,7 @@ describe Projects::VariablesController do it 'has all variables in response' do subject - expect(response.body).to include(project.variables.reload.to_json) + expect(response).to match_response_schema('variable') end end @@ -120,7 +120,7 @@ describe Projects::VariablesController do it 'has all variables in response' do subject - expect(response.body).to include(project.variables.reload.to_json) + expect(json_response['variables'].count).to eq(0) end end end diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json new file mode 100644 index 00000000000..422b4738418 --- /dev/null +++ b/spec/fixtures/api/schemas/variable.json @@ -0,0 +1,6 @@ +{ + "id": "string", + "key": "string", + "value": "string", + "protected": "boolean" +} diff --git a/spec/serializers/variable_entity_spec.rb b/spec/serializers/variable_entity_spec.rb new file mode 100644 index 00000000000..effc0022633 --- /dev/null +++ b/spec/serializers/variable_entity_spec.rb @@ -0,0 +1,14 @@ +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 From 04263b9f3be22bd97249162d9d3e27829c639d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 30 Jan 2018 02:11:17 +0100 Subject: [PATCH 20/34] Add GroupVariableSerializer for Ci::GroupVariable --- app/controllers/groups/variables_controller.rb | 4 ++-- app/serializers/group_variable_entity.rb | 7 +++++++ app/serializers/group_variable_serializer.rb | 3 +++ .../groups/variables_controller_spec.rb | 6 +++--- spec/serializers/group_variable_entity_spec.rb | 14 ++++++++++++++ 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 app/serializers/group_variable_entity.rb create mode 100644 app/serializers/group_variable_serializer.rb create mode 100644 spec/serializers/group_variable_entity_spec.rb diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index afa98aa8267..32c024d78fb 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -8,7 +8,7 @@ module Groups variables = @group.variables .map { |variable| variable.present(current_user: current_user) } - render status: :ok, json: { variables: variables } + render status: :ok, json: { variables: GroupVariableSerializer.new.represent(variables) } end end end @@ -20,7 +20,7 @@ module Groups variables = @group.variables .map { |variable| variable.present(current_user: current_user) } - return render status: :ok, json: { variables: variables } + return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(variables) } end render status: :bad_request, json: @group.errors.full_messages diff --git a/app/serializers/group_variable_entity.rb b/app/serializers/group_variable_entity.rb new file mode 100644 index 00000000000..62cf0b21e1e --- /dev/null +++ b/app/serializers/group_variable_entity.rb @@ -0,0 +1,7 @@ +class GroupVariableEntity < Grape::Entity + expose :id + expose :key + expose :value + + expose :protected?, as: :protected +end diff --git a/app/serializers/group_variable_serializer.rb b/app/serializers/group_variable_serializer.rb new file mode 100644 index 00000000000..8f8205924aa --- /dev/null +++ b/app/serializers/group_variable_serializer.rb @@ -0,0 +1,3 @@ +class GroupVariableSerializer < BaseSerializer + entity GroupVariableEntity +end diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 4927a9ee402..f6bcc68f9ad 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -19,7 +19,7 @@ describe Groups::VariablesController do it 'renders the ci_group_variable as json' do subject - expect(response.body).to include(variable.to_json) + expect(response).to match_response_schema('variable') end end @@ -82,7 +82,7 @@ describe Groups::VariablesController do it 'has all variables in response' do subject - expect(response.body).to include(group.variables.reload.to_json) + expect(response).to match_response_schema('variable') end end @@ -111,7 +111,7 @@ describe Groups::VariablesController do it 'has all variables in response' do subject - expect(response.body).to include(group.variables.reload.to_json) + expect(json_response['variables'].count).to eq(0) end end end diff --git a/spec/serializers/group_variable_entity_spec.rb b/spec/serializers/group_variable_entity_spec.rb new file mode 100644 index 00000000000..f6de7d01f98 --- /dev/null +++ b/spec/serializers/group_variable_entity_spec.rb @@ -0,0 +1,14 @@ +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 From e2c8a2231bcc00e5993411e4ad6c0c0db7e1a297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 30 Jan 2018 02:14:39 +0100 Subject: [PATCH 21/34] Remove usage of VariablePresenter in controller --- app/controllers/groups/variables_controller.rb | 10 ++-------- app/controllers/projects/variables_controller.rb | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 32c024d78fb..3e78711dabf 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -5,10 +5,7 @@ module Groups def show respond_to do |format| format.json do - variables = @group.variables - .map { |variable| variable.present(current_user: current_user) } - - render status: :ok, json: { variables: GroupVariableSerializer.new.represent(variables) } + render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } end end end @@ -17,10 +14,7 @@ module Groups respond_to do |format| format.json do if @group.update(variables_params) - variables = @group.variables - .map { |variable| variable.present(current_user: current_user) } - - return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(variables) } + return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } end render status: :bad_request, json: @group.errors.full_messages diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index b5635ca1b3b..18225218a1b 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -4,10 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController def show respond_to do |format| format.json do - variables = @project.variables - .map { |variable| variable.present(current_user: current_user) } - - render status: :ok, json: { variables: VariableSerializer.new.represent(variables) } + render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } end end end @@ -16,10 +13,7 @@ class Projects::VariablesController < Projects::ApplicationController respond_to do |format| format.json do if @project.update(variables_params) - variables = @project.variables - .map { |variable| variable.present(current_user: current_user) } - - return render status: :ok, json: { variables: VariableSerializer.new.represent(variables) } + return render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } end render status: :bad_request, json: @project.errors.full_messages From c95c3ffc9e4f1992929899bfc21440b621cc8daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 1 Feb 2018 21:38:41 +0100 Subject: [PATCH 22/34] Switch emphasis from controller format to update --- app/controllers/groups/variables_controller.rb | 14 +++++++------- app/controllers/projects/variables_controller.rb | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 3e78711dabf..0ebfebd6682 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -11,13 +11,13 @@ module Groups end def update - respond_to do |format| - format.json do - if @group.update(variables_params) - return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } - end - - render status: :bad_request, json: @group.errors.full_messages + if @group.update(variables_params) + respond_to do |format| + format.json { return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } } + end + else + respond_to do |format| + format.json { render status: :bad_request, json: @group.errors.full_messages } end end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 18225218a1b..329e1cdfef0 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -10,13 +10,13 @@ class Projects::VariablesController < Projects::ApplicationController end def update - respond_to do |format| - format.json do - if @project.update(variables_params) - return render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } - end - - render status: :bad_request, json: @project.errors.full_messages + if @project.update(variables_params) + respond_to do |format| + format.json { return render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } } + end + else + respond_to do |format| + format.json { render status: :bad_request, json: @project.errors.full_messages } end end end From 558057010f02fc931db08d8dedfc7cdeb6192ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 1 Feb 2018 21:58:05 +0100 Subject: [PATCH 23/34] Extract variable parameters in VariablesController specs --- .../groups/variables_controller_spec.rb | 28 +++++++++---------- .../projects/variables_controller_spec.rb | 28 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index f6bcc68f9ad..94d13b632cd 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -25,16 +25,22 @@ describe Groups::VariablesController do describe 'POST #update' do let!(:variable) { create(:ci_group_variable, group: group) } + 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 subject do patch :update, group_id: group, - variables_attributes: [{ id: variable.id, key: variable.key, - value: 'other_value', - protected: variable.protected?.to_s }, - { key: '..?', value: 'dummy_value', - protected: 'false' }], + variables_attributes: [variable_attributes.merge(value: 'other_value'), + new_variable_attributes.merge(key: '..?')], format: :json end @@ -57,11 +63,8 @@ describe Groups::VariablesController do subject do patch :update, group_id: group, - variables_attributes: [{ id: variable.id, key: variable.key, - value: 'other_value', - protected: variable.protected?.to_s }, - { key: 'new_key', value: 'dummy_value', - protected: 'false' }], + variables_attributes: [variable_attributes.merge(value: 'other_value'), + new_variable_attributes], format: :json end @@ -90,10 +93,7 @@ describe Groups::VariablesController do subject do patch :update, group_id: group, - variables_attributes: [{ id: variable.id, key: variable.key, - value: variable.value, - protected: variable.protected?.to_s, - _destroy: 'true' }], + variables_attributes: [variable_attributes.merge(_destroy: 'true')], format: :json end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 8e5d6023b79..4dbcfb125a2 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -30,6 +30,15 @@ describe Projects::VariablesController do describe 'POST #update' do let(:variable) { create(:ci_variable) } + 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 before do project.variables << variable @@ -39,11 +48,8 @@ describe Projects::VariablesController do subject do patch :update, namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [{ id: variable.id, key: variable.key, - value: 'other_value', - protected: variable.protected?.to_s }, - { key: '..?', value: 'dummy_value', - protected: 'false' }], + variables_attributes: [variable_attributes.merge(value: 'other_value'), + new_variable_attributes.merge(key: '..?')], format: :json end @@ -66,11 +72,8 @@ describe Projects::VariablesController do subject do patch :update, namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [{ id: variable.id, key: variable.key, - value: 'other_value', - protected: variable.protected?.to_s }, - { key: 'new_key', value: 'dummy_value', - protected: 'false' }], + variables_attributes: [variable_attributes.merge(value: 'other_value'), + new_variable_attributes], format: :json end @@ -99,10 +102,7 @@ describe Projects::VariablesController do subject do patch :update, namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [{ id: variable.id, key: variable.key, - value: variable.value, - protected: variable.protected?.to_s, - _destroy: 'true' }], + variables_attributes: [variable_attributes.merge(_destroy: 'true')], format: :json end From 434a6158ad08104662539d11a574f12668b3e6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 1 Feb 2018 22:18:03 +0100 Subject: [PATCH 24/34] Fix Variable JSON Schema --- spec/fixtures/api/schemas/variable.json | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json index 422b4738418..b91ab633d26 100644 --- a/spec/fixtures/api/schemas/variable.json +++ b/spec/fixtures/api/schemas/variable.json @@ -1,6 +1,24 @@ { - "id": "string", - "key": "string", - "value": "string", - "protected": "boolean" + "type": "object", + "required": ["variables"], + "properties": { + "variables": { + "type": "array", + "items": { + "required": [ + "id", + "key", + "value", + "protected" + ], + "properties": { + "id": { "type": "integer" }, + "key": { "type": "string" }, + "value": { "type": "string" }, + "protected": { "type": "boolean" } + } + } + } + }, + "additionalProperties": false } From 18232d7efb53381dea8727c36fc7a36dd2fd9d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 1 Feb 2018 22:35:00 +0100 Subject: [PATCH 25/34] Extract Variable into separate JSON Schema --- .../groups/variables_controller_spec.rb | 6 ++-- .../projects/variables_controller_spec.rb | 6 ++-- spec/fixtures/api/schemas/variable.json | 28 +++++++------------ spec/fixtures/api/schemas/variables.json | 11 ++++++++ 4 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 spec/fixtures/api/schemas/variables.json diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 94d13b632cd..80b6be39291 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -19,7 +19,7 @@ describe Groups::VariablesController do it 'renders the ci_group_variable as json' do subject - expect(response).to match_response_schema('variable') + expect(response).to match_response_schema('variables') end end @@ -85,7 +85,7 @@ describe Groups::VariablesController do it 'has all variables in response' do subject - expect(response).to match_response_schema('variable') + expect(response).to match_response_schema('variables') end end @@ -111,7 +111,7 @@ describe Groups::VariablesController do it 'has all variables in response' do subject - expect(json_response['variables'].count).to eq(0) + expect(response).to match_response_schema('variables') end end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 4dbcfb125a2..0082757a5c6 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -24,7 +24,7 @@ describe Projects::VariablesController do it 'renders the ci_variable as json' do subject - expect(response).to match_response_schema('variable') + expect(response).to match_response_schema('variables') end end @@ -94,7 +94,7 @@ describe Projects::VariablesController do it 'has all variables in response' do subject - expect(response).to match_response_schema('variable') + expect(response).to match_response_schema('variables') end end @@ -120,7 +120,7 @@ describe Projects::VariablesController do it 'has all variables in response' do subject - expect(json_response['variables'].count).to eq(0) + expect(response).to match_response_schema('variables') end end end diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json index b91ab633d26..78977118b0a 100644 --- a/spec/fixtures/api/schemas/variable.json +++ b/spec/fixtures/api/schemas/variable.json @@ -1,24 +1,16 @@ { "type": "object", - "required": ["variables"], + "required": [ + "id", + "key", + "value", + "protected" + ], "properties": { - "variables": { - "type": "array", - "items": { - "required": [ - "id", - "key", - "value", - "protected" - ], - "properties": { - "id": { "type": "integer" }, - "key": { "type": "string" }, - "value": { "type": "string" }, - "protected": { "type": "boolean" } - } - } - } + "id": { "type": "integer" }, + "key": { "type": "string" }, + "value": { "type": "string" }, + "protected": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/variables.json b/spec/fixtures/api/schemas/variables.json new file mode 100644 index 00000000000..8002f39a7b8 --- /dev/null +++ b/spec/fixtures/api/schemas/variables.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": ["variables"], + "properties": { + "variables": { + "type": "array", + "items": { "$ref": "variable.json" } + } + }, + "additionalProperties": false +} From 45a14b4f584dd1102bd81c560e4d2e7c4b34aea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 2 Feb 2018 18:54:13 +0100 Subject: [PATCH 26/34] Refactor Variable controllers specs --- .../groups/variables_controller_spec.rb | 43 +++++++++-------- .../projects/variables_controller_spec.rb | 46 ++++++++++--------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 80b6be39291..8abdc1cb5b1 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -25,23 +25,32 @@ describe Groups::VariablesController do describe 'POST #update' do let!(:variable) { create(:ci_group_variable, group: group) } + + subject do + patch :update, + group_id: group, + variables_attributes: variables_attributes, + format: :json + end + let(:variable_attributes) do - { id: variable.id, key: variable.key, + { 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', + { key: 'new_key', + value: 'dummy_value', protected: 'false' } end context 'with invalid new variable parameters' do - subject do - patch :update, - group_id: group, - variables_attributes: [variable_attributes.merge(value: 'other_value'), - new_variable_attributes.merge(key: '..?')], - format: :json + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes.merge(key: '...?') + ] end it 'does not update the existing variable' do @@ -60,12 +69,11 @@ describe Groups::VariablesController do end context 'with valid new variable parameters' do - subject do - patch :update, - group_id: group, - variables_attributes: [variable_attributes.merge(value: 'other_value'), - new_variable_attributes], - format: :json + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes + ] end it 'updates the existing variable' do @@ -90,12 +98,7 @@ describe Groups::VariablesController do end context 'with a deleted variable' do - subject do - patch :update, - group_id: group, - variables_attributes: [variable_attributes.merge(_destroy: 'true')], - format: :json - end + let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] } it 'destroys the variable' do expect { subject }.to change { group.variables.count }.by(-1) diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 0082757a5c6..d2cb3dd9453 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -17,8 +17,7 @@ describe Projects::VariablesController do end subject do - get :show, namespace_id: project.namespace.to_param, project_id: project, - format: :json + get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json end it 'renders the ci_variable as json' do @@ -30,13 +29,23 @@ describe Projects::VariablesController do describe 'POST #update' do let(:variable) { create(:ci_variable) } + + subject do + patch :update, + namespace_id: project.namespace.to_param, project_id: project, + variables_attributes: variables_attributes, + format: :json + end + let(:variable_attributes) do - { id: variable.id, key: variable.key, + { 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', + { key: 'new_key', + value: 'dummy_value', protected: 'false' } end @@ -45,12 +54,11 @@ describe Projects::VariablesController do end context 'with invalid new variable parameters' do - subject do - patch :update, - namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [variable_attributes.merge(value: 'other_value'), - new_variable_attributes.merge(key: '..?')], - format: :json + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes.merge(key: '...?') + ] end it 'does not update the existing variable' do @@ -69,12 +77,11 @@ describe Projects::VariablesController do end context 'with valid new variable parameters' do - subject do - patch :update, - namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [variable_attributes.merge(value: 'other_value'), - new_variable_attributes], - format: :json + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes + ] end it 'updates the existing variable' do @@ -99,12 +106,7 @@ describe Projects::VariablesController do end context 'with a deleted variable' do - subject do - patch :update, - namespace_id: project.namespace.to_param, project_id: project, - variables_attributes: [variable_attributes.merge(_destroy: 'true')], - format: :json - end + let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] } it 'destroys the variable' do expect { subject }.to change { project.variables.count }.by(-1) From f7ed096455c932a5ead8bafb8c937ff9cdb3070c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 2 Feb 2018 19:35:00 +0100 Subject: [PATCH 27/34] Extract Variables controllers specs to shared_examples --- .../groups/variables_controller_spec.rb | 94 +-------------- .../projects/variables_controller_spec.rb | 109 ++---------------- .../controllers/variables_shared_examples.rb | 100 ++++++++++++++++ 3 files changed, 112 insertions(+), 191 deletions(-) create mode 100644 spec/support/shared_examples/controllers/variables_shared_examples.rb diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 8abdc1cb5b1..39a36b92bb4 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -16,15 +16,12 @@ describe Groups::VariablesController do get :show, group_id: group, format: :json end - it 'renders the ci_group_variable as json' do - subject - - expect(response).to match_response_schema('variables') - end + include_examples 'GET #show lists all variables' end - describe 'POST #update' do + describe 'PATCH #update' do let!(:variable) { create(:ci_group_variable, group: group) } + let(:owner) { group } subject do patch :update, @@ -33,89 +30,6 @@ describe Groups::VariablesController do format: :json end - 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 { group.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 { group.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 { group.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 + include_examples 'PATCH #update updates variables' end end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index d2cb3dd9453..68019743be0 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -10,120 +10,27 @@ describe Projects::VariablesController do end describe 'GET #show' do - let(:variable) { create(:ci_variable) } - - before do - project.variables << variable - end + let!(:variable) { create(:ci_variable, project: project) } subject do get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json end - it 'renders the ci_variable as json' do - subject - - expect(response).to match_response_schema('variables') - end + include_examples 'GET #show lists all variables' end - describe 'POST #update' do - let(:variable) { create(:ci_variable) } + describe 'PATCH #update' do + let!(:variable) { create(:ci_variable, project: project) } + let(:owner) { project } subject do patch :update, - namespace_id: project.namespace.to_param, project_id: project, + namespace_id: project.namespace.to_param, + project_id: project, variables_attributes: variables_attributes, format: :json end - 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 - - before do - project.variables << variable - 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 { project.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 { project.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 { project.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 + include_examples 'PATCH #update updates variables' end end diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb new file mode 100644 index 00000000000..3f1690e71dd --- /dev/null +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -0,0 +1,100 @@ +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 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 From 79570ce24fa93709db7a7bdd4fae2532a7235486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 5 Feb 2018 15:23:32 +0100 Subject: [PATCH 28/34] Fix validation of duplicate new variables --- app/models/group.rb | 1 + app/models/project.rb | 1 + .../controllers/variables_shared_examples.rb | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/app/models/group.rb b/app/models/group.rb index 29df4144d03..75bf013ecd2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -36,6 +36,7 @@ class Group < Namespace validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_sub_groups 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 } diff --git a/app/models/project.rb b/app/models/project.rb index 12d5f28f5ea..7e0a10cb4cd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -261,6 +261,7 @@ class Project < ActiveRecord::Base validates :repository_storage, presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :variables, variable_duplicates: true has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb index 3f1690e71dd..d7acf8c0032 100644 --- a/spec/support/shared_examples/controllers/variables_shared_examples.rb +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -48,6 +48,29 @@ shared_examples 'PATCH #update updates variables' do 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 [ From 3be32027b6c543287b94b5be34bf53039d86f88c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 16 Jan 2018 15:13:14 -0600 Subject: [PATCH 29/34] Use dynamic variable list in scheduled pipelines and group/project CI secret variables See https://gitlab.com/gitlab-org/gitlab-ce/issues/39118 Conflicts: app/views/ci/variables/_form.html.haml app/views/ci/variables/_table.html.haml ee/app/views/ci/variables/_environment_scope.html.haml spec/javascripts/ci_variable_list/ci_variable_list_ee_spec.js spec/javascripts/fixtures/projects.rb --- .../ci_variable_list/ajax_variable_list.js | 116 ++++++++ .../ci_variable_list/ci_variable_list.js | 27 +- .../javascripts/commons/polyfills/element.js | 19 ++ .../javascripts/lib/utils/http_status.js | 2 + .../pages/groups/settings/ci_cd/show/index.js | 17 +- .../projects/settings/ci_cd/show/index.js | 17 +- .../framework/ci_variable_list.scss | 11 +- app/views/ci/variables/_content.html.haml | 4 +- app/views/ci/variables/_form.html.haml | 19 -- app/views/ci/variables/_index.html.haml | 34 ++- app/views/ci/variables/_show.html.haml | 9 - app/views/ci/variables/_table.html.haml | 32 --- .../groups/settings/ci_cd/show.html.haml | 9 +- app/views/groups/variables/show.html.haml | 1 - .../projects/settings/ci_cd/show.html.haml | 8 +- app/views/projects/variables/show.html.haml | 1 - .../39118-dynamic-pipeline-variables-fe.yml | 6 + doc/ci/variables/img/secret_variables.png | Bin 15658 -> 32886 bytes qa/qa/factory/resource/secret_variable.rb | 2 +- .../page/project/settings/secret_variables.rb | 55 ++-- spec/features/group_variables_spec.rb | 71 +---- spec/features/project_variables_spec.rb | 18 ++ spec/features/variables_spec.rb | 145 ---------- .../ajax_variable_list_spec.js | 189 ++++++++++++ .../ci_variable_list/ci_variable_list_spec.js | 81 ++++-- spec/javascripts/fixtures/groups.rb | 29 ++ spec/javascripts/fixtures/projects.rb | 51 +++- .../features/variable_list_shared_examples.rb | 269 ++++++++++++++++++ 28 files changed, 852 insertions(+), 390 deletions(-) create mode 100644 app/assets/javascripts/ci_variable_list/ajax_variable_list.js delete mode 100644 app/views/ci/variables/_form.html.haml delete mode 100644 app/views/ci/variables/_show.html.haml delete mode 100644 app/views/ci/variables/_table.html.haml delete mode 100644 app/views/groups/variables/show.html.haml delete mode 100644 app/views/projects/variables/show.html.haml create mode 100644 changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml create mode 100644 spec/features/project_variables_spec.rb delete mode 100644 spec/features/variables_spec.rb create mode 100644 spec/javascripts/ci_variable_list/ajax_variable_list_spec.js create mode 100644 spec/javascripts/fixtures/groups.rb create mode 100644 spec/support/features/variable_list_shared_examples.rb diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js new file mode 100644 index 00000000000..76f93e5c6bd --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -0,0 +1,116 @@ +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 => ` +
  • + ${_.escape(errorString)} +
  • + `); + + return ` +

    + ${s__('CiVariable|Validation failed')} +

    +
      + ${errorList.join('')} +
    + `; +} + +// 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'); + } + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index e46478ddb98..d91789c2192 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -11,7 +11,7 @@ function createEnvironmentItem(value) { return { title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, id: value, - text: value, + text: value === '*' ? s__('CiVariable|* (All environments)') : value, }; } @@ -41,11 +41,11 @@ export default class VariableList { selector: '.js-ci-variable-input-protected', default: 'true', }, - environment: { + environment_scope: { // We can't use a `.js-` class here because // gl_dropdown replaces the and doesn't copy over the class // 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: '*', }, _destroy: { @@ -104,12 +104,15 @@ export default class VariableList { setupToggleButtons($row[0]); + // Reset the resizable textarea + $row.find(this.inputMap.value.selector).css('height', ''); + const $environmentSelect = $row.find('.js-variable-environment-toggle'); if ($environmentSelect.length) { const createItemDropdown = new CreateItemDropdown({ $dropdown: $environmentSelect, defaultToggleLabel: ALL_ENVIRONMENTS_STRING, - fieldName: `${this.formField}[variables_attributes][][environment]`, + fieldName: `${this.formField}[variables_attributes][][environment_scope]`, getData: (term, callback) => callback(this.getEnvironmentValues()), createNewItemFromValue: createEnvironmentItem, onSelect: () => { @@ -117,7 +120,7 @@ export default class VariableList { // so they have the new value we just picked 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 { $row.after($rowClone); } - removeRow($row) { + removeRow(row) { + const $row = $(row); const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { @@ -155,6 +159,10 @@ export default class VariableList { } else { $row.remove(); } + + // Refresh the other dropdowns in the variable list + // so any value with the variable deleted is gone + this.refreshDropdownData(); } checkIfRowTouched($row) { @@ -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() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. @@ -185,7 +198,7 @@ export default class VariableList { } 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) => ({ ...prevValueMap, [envInput.value]: envInput.value, diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js index 9a1f73bf2ac..b593bde6aa2 100644 --- a/app/assets/javascripts/commons/polyfills/element.js +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches || while (i >= 0 && elms.item(i) !== this) { 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]); diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 625e53ee9de..bb151929431 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -6,4 +6,6 @@ export default { ABORTED: 0, NO_CONTENT: 204, OK: 200, + MULTIPLE_CHOICES: 300, + BAD_REQUEST: 400, }; diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index f26c7360fbe..ad79f7e09ac 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,11 +1,12 @@ -import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default () => { - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); }; diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 18dc1dc03a5..a563d0f9961 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -1,9 +1,11 @@ import initSettingsPanels from '~/settings_panels'; import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default function () { // Initialize expandable settings panels initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); if (runnerToken) { const runnerTokenSecretValue = new SecretValues({ @@ -12,11 +14,12 @@ export default function () { runnerTokenSecretValue.init(); } - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index 8f654ab363c..ccd36af071f 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -8,7 +8,11 @@ .ci-variable-row { display: flex; - align-items: flex-end; + align-items: flex-start; + + @media (max-width: $screen-xs-max) { + align-items: flex-end; + } &:not(:last-child) { margin-bottom: $gl-btn-padding; @@ -41,6 +45,7 @@ .ci-variable-row-body { display: flex; + align-items: flex-start; width: 100%; @media (max-width: $screen-xs-max) { @@ -85,4 +90,8 @@ outline: none; color: $gl-text-color; } + + &[disabled] { + color: $gl-text-color-disabled; + } } diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml index fbfe3e56588..d355e7799df 100644 --- a/app/views/ci/variables/_content.html.haml +++ b/app/views/ci/variables/_content.html.haml @@ -1,3 +1 @@ -%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.') diff --git a/app/views/ci/variables/_form.html.haml b/app/views/ci/variables/_form.html.haml deleted file mode 100644 index eebd0955c80..00000000000 --- a/app/views/ci/variables/_form.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= 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" diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index 6e399fc7392..e402801a776 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -1,16 +1,20 @@ -.row.prepend-top-default.append-bottom-default - .col-lg-12 - %h5.prepend-top-0 - Add a variable - = render "ci/variables/form", btn_text: "Add new variable" - %hr - %h5.prepend-top-0 - Your variables (#{@variables.size}) - - if @variables.empty? - %p.settings-message.text-center.append-bottom-0 - No variables found, add one with the form above. - - else - .js-secret-variable-table - = render "ci/variables/table" - %button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } } +- save_endpoint = local_assigns.fetch(:save_endpoint, nil) + +.row + .col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } } + .hide.alert.alert-danger.js-ci-variable-error-box + + %ul.ci-variable-list + - @variables.each.each do |variable| + = render 'ci/variables/variable_row', form_field: 'variables', variable: variable + = render 'ci/variables/variable_row', form_field: 'variables' + .prepend-top-20 + %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' } + %span.hide.js-secret-variables-save-loading-icon + = icon('spinner spin') + = _('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) diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml deleted file mode 100644 index 6d75ae96124..00000000000 --- a/app/views/ci/variables/_show.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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" diff --git a/app/views/ci/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml deleted file mode 100644 index 2298930d0c7..00000000000 --- a/app/views/ci/variables/_table.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.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") diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 472da2a6a72..dd82922ec55 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,4 +1,11 @@ - breadcrumb_title "CI / CD Settings" - 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 diff --git a/app/views/groups/variables/show.html.haml b/app/views/groups/variables/show.html.haml deleted file mode 100644 index df533952b76..00000000000 --- a/app/views/groups/variables/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'ci/variables/show' diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 664a4554692..756f31f91d9 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -29,14 +29,14 @@ %section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Secret variables - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' + = _('Secret variables') + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' - %p + %p.append-bottom-0 = render "ci/variables/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) } .settings-header diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml deleted file mode 100644 index df533952b76..00000000000 --- a/app/views/projects/variables/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'ci/variables/show' diff --git a/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml b/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml new file mode 100644 index 00000000000..a38b447e345 --- /dev/null +++ b/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml @@ -0,0 +1,6 @@ +--- +title: Update CI/CD secret variables list to be dynamic and save without reloading + the page +merge_request: 4110 +author: +type: added diff --git a/doc/ci/variables/img/secret_variables.png b/doc/ci/variables/img/secret_variables.png index f70935069d9604009d5e0458b63343da0dbf7e8c..3c1aa361dc2b38759fa6619dcd2c8ffc788b8929 100644 GIT binary patch literal 32886 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 literal 15658 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 { + 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'); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 0170ab458d4..6ab7b50e035 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; describe('VariableList', () => { preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + preloadFixtures('projects/ci_cd_settings.html.raw'); let $wrapper; let variableList; @@ -105,37 +106,8 @@ describe('VariableList', () => { describe('with all inputs(key, value, protected)', () => { beforeEach(() => { - // This markup will be replaced with a fixture when we can render the - // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 - $wrapper = $(`
    -
      -
    • -
      - -
      - -
      - -
      - -
      - -
      - - -
    • -
    - -
    `); + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ container: $wrapper, @@ -160,4 +132,51 @@ describe('VariableList', () => { .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); + }); + }); }); diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb new file mode 100644 index 00000000000..35be52fbf97 --- /dev/null +++ b/spec/javascripts/fixtures/groups.rb @@ -0,0 +1,29 @@ +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 diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 2a100e7fab5..b344b389241 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe ProjectsController, '(JavaScript fixtures)', type: :controller do +describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} 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 @@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do end before do + # EE-specific start + # EE specific end + project.add_master(admin) sign_in(admin) end @@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'projects/dashboard.html.raw' do |example| - get :show, - namespace_id: project.namespace.to_param, - id: project + describe ProjectsController, '(JavaScript fixtures)', type: :controller do + it 'projects/dashboard.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + id: project - expect(response).to be_success - store_frontend_fixture(response, example.description) + expect(response).to be_success + 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 diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb new file mode 100644 index 00000000000..ea4e8386eff --- /dev/null +++ b/spec/support/features/variable_list_shared_examples.rb @@ -0,0 +1,269 @@ +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('Variables key has already been taken') + end + end +end From 1098355e0203dbce05384c8c88228d8175dd9bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 5 Feb 2018 18:21:38 +0100 Subject: [PATCH 30/34] Add missing padding to CI variables protected item --- app/assets/stylesheets/framework/ci_variable_list.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index ccd36af071f..5fe835dd8f9 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -70,6 +70,8 @@ flex: 0 1 auto; display: flex; align-items: center; + padding-top: 5px; + padding-bottom: 5px; } .ci-variable-row-remove-button { From 0ccbc6515e4c08ce77af6c8a82ead52a961ce2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 5 Feb 2018 18:22:10 +0100 Subject: [PATCH 31/34] Fix duplicate CI variable feature spec failure --- spec/support/features/variable_list_shared_examples.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index ea4e8386eff..83bf06b6727 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -263,7 +263,7 @@ shared_examples 'variable list' do # 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('Variables key has already been taken') + expect(find('.js-ci-variable-error-box')).to have_content('Validation failed Variables Duplicate variables: samekey') end end end From 5f127babcc22651754b512fbb6f3ca4559c96225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 5 Feb 2018 18:42:58 +0100 Subject: [PATCH 32/34] Fix API variable specs --- spec/requests/api/group_variables_spec.rb | 4 ++-- spec/requests/api/variables_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index a4f198eb5c9..64fa7dc824c 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -142,12 +142,12 @@ describe API::GroupVariables do end it 'updates variable data' do - initial_variable = group.variables.first + initial_variable = group.variables.reload.first value_before = initial_variable.value 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(value_before).to eq(variable.value) diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 79ee6c126f6..62215ea3d7d 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -122,12 +122,12 @@ describe API::Variables do describe 'PUT /projects/:id/variables/:key' do context 'authorized user with proper permissions' do it 'updates variable data' do - initial_variable = project.variables.first + initial_variable = project.variables.reload.first value_before = initial_variable.value 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(value_before).to eq(variable.value) From d05cc9c6a6e46ed024a275240be0a5137622e474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 5 Feb 2018 21:24:20 +0100 Subject: [PATCH 33/34] Reload project before checking variables in project_spec --- spec/models/project_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index da940571bc1..9b5f7eb6453 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2071,7 +2071,7 @@ describe Project do create(:ci_variable, :protected, value: 'protected', project: project) end - subject { project.secret_variables_for(ref: 'ref') } + subject { project.reload.secret_variables_for(ref: 'ref') } before do stub_application_setting( From efcdc269e03836e78dcc8d460621c948ac02bc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 6 Feb 2018 17:56:54 +0100 Subject: [PATCH 34/34] Fix static_analysis failure --- app/controllers/groups/variables_controller.rb | 16 ++++++++++++---- app/controllers/projects/variables_controller.rb | 12 ++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 0ebfebd6682..913e13bf734 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -11,20 +11,28 @@ module Groups end def update - if @group.update(variables_params) + if @group.update(group_variables_params) respond_to do |format| - format.json { return render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } } + format.json { return render_group_variables } end else respond_to do |format| - format.json { render status: :bad_request, json: @group.errors.full_messages } + format.json { render_error } end end end private - def variables_params + def render_group_variables + render status: :ok, json: { variables: GroupVariableSerializer.new.represent(@group.variables) } + end + + def render_error + render status: :bad_request, json: @group.errors.full_messages + end + + def group_variables_params params.permit(variables_attributes: [*variable_params_attributes]) end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 329e1cdfef0..7eb509e2e64 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -12,17 +12,25 @@ class Projects::VariablesController < Projects::ApplicationController def update if @project.update(variables_params) respond_to do |format| - format.json { return render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } } + format.json { return render_variables } end else respond_to do |format| - format.json { render status: :bad_request, json: @project.errors.full_messages } + format.json { render_error } end end end private + def render_variables + render status: :ok, json: { variables: VariableSerializer.new.represent(@project.variables) } + end + + def render_error + render status: :bad_request, json: @project.errors.full_messages + end + def variables_params params.permit(variables_attributes: [*variable_params_attributes]) end