From 18e4038ddf9530bbfd0821bf4b7811186aefb551 Mon Sep 17 00:00:00 2001 From: Alex Buijs Date: Wed, 7 Feb 2024 12:59:03 +0100 Subject: [PATCH] Allow managing project CI/CD variables For users with the admin_cicd_variables custom role ability. EE: true --- .../ci_settings_pipeline_triggers/index.js | 4 + app/assets/javascripts/deploy_freeze/index.js | 4 + .../projects/settings/ci_cd/show/index.js | 4 +- .../projects/settings/ci_cd_controller.rb | 3 +- .../projects/variables_controller.rb | 3 +- app/graphql/types/project_type.rb | 4 +- app/policies/project_policy.rb | 1 + .../projects/settings/ci_cd/show.html.haml | 181 +++++++++--------- .../projects/settings/ci_cd/show.html.haml | 3 +- .../sidebars/projects/menus/settings_menu.rb | 5 + .../projects/menus/settings_menu_spec.rb | 16 ++ .../projects_request_spec.rb | 98 ++++++++++ 12 files changed, 231 insertions(+), 95 deletions(-) create mode 100644 ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js index 3ea8e0df02279c..40f7abaa914586 100644 --- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js +++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js @@ -13,6 +13,10 @@ const parseJsonArray = (triggers) => { export default (containerId = 'js-ci-pipeline-triggers-list') => { const containerEl = document.getElementById(containerId); + if (!containerEl) { + return null; + } + const triggers = parseJsonArray(containerEl.dataset.triggers); return new Vue({ diff --git a/app/assets/javascripts/deploy_freeze/index.js b/app/assets/javascripts/deploy_freeze/index.js index fd3f52b6da1b87..85e515a1493e4d 100644 --- a/app/assets/javascripts/deploy_freeze/index.js +++ b/app/assets/javascripts/deploy_freeze/index.js @@ -5,6 +5,10 @@ import createStore from './store'; export default () => { const el = document.getElementById('js-deploy-freeze-table'); + if (!el) { + return null; + } + const { projectId, timezoneData } = el.dataset; const store = createStore({ 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 ad6f84fbc07366..66845e03963eca 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 @@ -22,7 +22,9 @@ initInheritedGroupCiVariables(); // hide extra auto devops settings based checkbox state const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); const instanceDefaultBadge = document.querySelector('.js-instance-default-badge'); -document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => { +const extraSettingsToggle = document.querySelector('.js-toggle-extra-settings'); + +extraSettingsToggle?.addEventListener('click', (event) => { const { target } = event; if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none'; autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked); diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 9a128adb926e19..94fc5bf0c75e71 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -8,7 +8,8 @@ class CiCdController < Projects::ApplicationController NUMBER_OF_RUNNERS_PER_PAGE = 20 layout 'project_settings' - before_action :authorize_admin_pipeline! + before_action :authorize_admin_pipeline!, except: :show + before_action :authorize_admin_cicd_variables!, only: :show before_action :check_builds_available! before_action :define_variables diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 29ecca1b7e0060..217d00c7d547ea 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Projects::VariablesController < Projects::ApplicationController - before_action :authorize_admin_build! + before_action :authorize_admin_build!, except: :update + before_action :authorize_admin_cicd_variables!, only: :update feature_category :secrets_management diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index db850f60910450..abab206087230b 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -382,13 +382,13 @@ class ProjectType < BaseObject field :ci_variables, Types::Ci::ProjectVariableType.connection_type, null: true, description: "List of the project's CI/CD variables.", - authorize: :admin_build, + authorize: :admin_cicd_variables, resolver: Resolvers::Ci::VariablesResolver field :inherited_ci_variables, Types::Ci::InheritedCiVariableType.connection_type, null: true, description: "List of CI/CD variables the project inherited from its parent group and ancestors.", - authorize: :admin_build, + authorize: :admin_cicd_variables, resolver: Resolvers::Ci::InheritedVariablesResolver field :ci_cd_settings, Types::Ci::CiCdSettingType, diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index a686a94b7c66d9..bc162f1215795e 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -593,6 +593,7 @@ class ProjectPolicy < BasePolicy enable :admin_incident_management_timeline_event_tag enable :stop_environment enable :read_import_error + enable :admin_cicd_variables end rule { can?(:admin_build) }.enable :manage_trigger diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index ab052a73ce02b0..290756e5aad2c0 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -5,118 +5,121 @@ - expanded = expanded_by_default? - general_expanded = @project.errors.empty? ? expanded : true -%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("General pipelines") - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') - %p.gl-text-secondary - = _("Customize your pipeline configuration.") - .settings-content - = render 'form' - -%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'autodevops-settings-content' } } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = s_('CICD|Auto DevOps') - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') - %p.gl-text-secondary - - auto_devops_url = help_page_path('topics/autodevops/index') - - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke') - - auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer') - - quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer') - = safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end)) - .settings-content - = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled? - -= render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded - -- expand_runners = expanded || params[:expand_runners] -%section.settings.no-animate#js-runners-settings{ class: ('expanded' if expand_runners), data: { testid: 'runners-settings-content' } } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Runners") - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expand_runners ? _('Collapse') : _('Expand') - %p.gl-text-secondary - = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") - = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer' - .settings-content - = render 'projects/runners/settings' +- if can?(current_user, :admin_pipeline, @project) + %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("General pipelines") + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do + = expanded ? _('Collapse') : _('Expand') + %p.gl-text-secondary + = _("Customize your pipeline configuration.") + .settings-content + = render 'form' -- if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? - %section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) } + %section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'autodevops-settings-content' } } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Artifacts") + = s_('CICD|Auto DevOps') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') %p.gl-text-secondary - = _("A job artifact is an archive of files and directories saved by a job when it finishes.") + - auto_devops_url = help_page_path('topics/autodevops/index') + - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke') + - auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer') + - quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer') + = safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end)) .settings-content - #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/jobs/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } } + = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled? -%section.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { testid: 'variables-settings-content' } } - .settings-header - = render 'ci/variables/header', expanded: expanded - .settings-content - = render 'ci/variables/index', save_endpoint: project_variables_path(@project) + = render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded -%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Pipeline trigger tokens") - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') - %p.gl-text-secondary - = _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.") - = link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer' - .settings-content - = render 'projects/triggers/index' + - expand_runners = expanded || params[:expand_runners] + %section.settings.no-animate#js-runners-settings{ class: ('expanded' if expand_runners), data: { testid: 'runners-settings-content' } } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("Runners") + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do + = expand_runners ? _('Collapse') : _('Expand') + %p.gl-text-secondary + = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") + = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer' + .settings-content + = render 'projects/runners/settings' -= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded + - if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? + %section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("Artifacts") + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do + = expanded ? _('Collapse') : _('Expand') + %p.gl-text-secondary + = _("A job artifact is an archive of files and directories saved by a job when it finishes.") + .settings-content + #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/jobs/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } } + +- if can?(current_user, :admin_cicd_variables, @project) + %section.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { testid: 'variables-settings-content' } } + .settings-header + = render 'ci/variables/header', expanded: expanded + .settings-content + = render 'ci/variables/index', save_endpoint: project_variables_path(@project) -- if can?(current_user, :create_freeze_period, @project) - %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) } +- if can?(current_user, :admin_pipeline, @project) + %section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Deploy freezes") + = _("Pipeline trigger tokens") = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') %p.gl-text-secondary - - freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze') - - freeze_period_link_start = ''.html_safe % { url: freeze_period_docs } - = html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: ''.html_safe, filename: tag.code('.gitlab-ci.yml') } + = _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.") + = link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer' + .settings-content + = render 'projects/triggers/index' - - cron_syntax_url = 'https://crontab.guru/' - - cron_syntax_link_start = ''.html_safe % { url: cron_syntax_url } - = s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "".html_safe } + = render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded - .settings-content - = render 'ci/deploy_freeze/index' + - if can?(current_user, :create_freeze_period, @project) + %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("Deploy freezes") + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do + = expanded ? _('Collapse') : _('Expand') + %p.gl-text-secondary + - freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze') + - freeze_period_link_start = ''.html_safe % { url: freeze_period_docs } + = html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: ''.html_safe, filename: tag.code('.gitlab-ci.yml') } -%section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Token Access") - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') - %p.gl-text-secondary - = _("Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects.") - .settings-content - = render 'ci/token_access/index' + - cron_syntax_url = 'https://crontab.guru/' + - cron_syntax_link_start = ''.html_safe % { url: cron_syntax_url } + = s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "".html_safe } -- if show_secure_files_setting(@project, current_user) - %section.settings.no-animate#js-secure-files{ class: ('expanded' if expanded) } + .settings-content + = render 'ci/deploy_freeze/index' + + %section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Secure Files") + = _("Token Access") = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') %p.gl-text-secondary - = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.") - = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer' + = _("Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects.") .settings-content - #js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } } + = render 'ci/token_access/index' + + - if show_secure_files_setting(@project, current_user) + %section.settings.no-animate#js-secure-files{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("Secure Files") + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do + = expanded ? _('Collapse') : _('Expand') + %p.gl-text-secondary + = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.") + = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer' + .settings-content + #js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } } diff --git a/ee/app/views/projects/settings/ci_cd/show.html.haml b/ee/app/views/projects/settings/ci_cd/show.html.haml index a0987888cd8bf4..b68d488d5a8b65 100644 --- a/ee/app/views/projects/settings/ci_cd/show.html.haml +++ b/ee/app/views/projects/settings/ci_cd/show.html.haml @@ -1,3 +1,4 @@ = render_ce 'projects/settings/ci_cd/show' -= render 'projects/settings/ci_cd/pipeline_subscriptions' +- if can?(current_user, :admin_pipeline, @project) + = render 'projects/settings/ci_cd/pipeline_subscriptions' diff --git a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb index 429f0221d5ee79..725defa85b30de 100644 --- a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb +++ b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb @@ -44,6 +44,7 @@ def custom_roles_menu_items items << general_menu_item if custom_roles_general_menu_item? items << access_tokens_menu_item if custom_roles_access_token_menu_item? + items << ci_cd_menu_item if custom_roles_ci_cd_menu_item? items end @@ -55,6 +56,10 @@ def custom_roles_general_menu_item? def custom_roles_access_token_menu_item? can?(context.current_user, :manage_resource_access_tokens, context.project) end + + def custom_roles_ci_cd_menu_item? + can?(context.current_user, :admin_cicd_variables, context.project) + end end end end diff --git a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb index 65ce668661ff87..0f3bc0067a3ace 100644 --- a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb @@ -99,5 +99,21 @@ end end end + + describe 'CI/CD' do + let(:item_id) { :ci_cd } + + describe 'when the user is not an admin but has `admin_cicd_variables` custom ability' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :admin_cicd_variables, project).and_return(true) + end + + it 'includes CI/CD menu item' do + expect(subject.title).to eql('CI/CD') + end + end + end end end diff --git a/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb b/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb new file mode 100644 index 00000000000000..cb857bbb2c719e --- /dev/null +++ b/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User with admin_cicd_variables custom role', feature_category: :secrets_management do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_cicd_variables: true) } + let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) } + + before do + stub_licensed_features(custom_roles: true) + + sign_in(user) + end + + describe Projects::Settings::CiCdController do + describe '#show' do + it 'user can view CI/CD settings page' do + get project_settings_ci_cd_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include('CI/CD Settings') + end + end + end + + describe 'Querying CI Variables and environment scopes' do + include GraphqlHelpers + + let_it_be(:variable) { create(:ci_variable, project: project, key: 'new_key', value: 'dummy_value') } + let_it_be(:inherited_variable) { create(:ci_group_variable, group: group, key: 'my_key') } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciVariables { + nodes { + key + value + } + } + inheritedCiVariables { + nodes { + key + } + } + } + } + ) + end + + it 'returns variables and inherited variables for the project' do + result = GitlabSchema.execute(query, context: { current_user: user }).as_json + + project_data = result.dig('data', 'project') + + expect(project_data.dig('ciVariables', 'nodes').first).to eq('key' => variable.key, 'value' => variable.value) + expect(project_data.dig('inheritedCiVariables', 'nodes').first).to eq('key' => inherited_variable.key) + end + end + + describe Projects::VariablesController do + describe '#update' do + it 'user can create CI/CD variables' do + params = { variables_attributes: [{ key: 'new_key', secret_value: 'dummy_value' }] } + put project_variables_path(project, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)['variables'][0]) + .to include('key' => 'new_key', 'value' => 'dummy_value') + end + + it 'user can update CI/CD variables' do + var = create(:ci_variable, project: project) + + params = { variables_attributes: [{ id: var.id, key: 'new_key', secret_value: 'dummy_value' }] } + put project_variables_path(project, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)['variables'][0]) + .to include('key' => 'new_key', 'value' => 'dummy_value') + end + + it 'user can destroy CI/CD variables' do + var = create(:ci_variable, project: project) + + params = { variables_attributes: [{ id: var.id, _destroy: 'true' }] } + put project_variables_path(project, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect { var.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end -- GitLab