diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index af6cb74ca0d6334d0c00ad2e1115505fb7e7272e..8363de4247524d587c29d8c6e81a404b5cd3009a 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -5,6 +5,7 @@ class ProjectType < BaseObject graphql_name 'Project' include ::Namespaces::DeletableHelper + include Gitlab::Graphql::Authorize::AuthorizeResource connection_type_class Types::CountableConnectionType @@ -34,6 +35,10 @@ def self.authorization_scopes authorize: :create_pipeline, experiment: { milestone: '15.3' }, description: 'CI/CD config variable.' do + argument :fail_on_cache_miss, GraphQL::Types::Boolean, + required: false, + default_value: false, + description: 'Whether to throw an error if cache is not ready.' argument :ref, GraphQL::Types::String, required: true, description: 'Ref.' @@ -1027,9 +1032,13 @@ def ci_pipeline_creation_inputs(ref:) response.payload[:inputs].all_inputs end - def ci_config_variables(ref:) + def ci_config_variables(ref:, fail_on_cache_miss: false) result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref) + if result.nil? && fail_on_cache_miss + raise_resource_not_available_error! "Failed to retrieve CI/CD variables from cache." + end + return if result.nil? result.map do |var_key, var_config| diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4abf36ac582f61dbce75eeb6b2fd82b15019c780..5c385f7488ccedbc1de83aca5fba38258690cac6 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -39867,6 +39867,7 @@ Returns [`[CiConfigVariable!]`](#ciconfigvariable). | Name | Type | Description | | ---- | ---- | ----------- | +| `failOnCacheMiss` | [`Boolean`](#boolean) | Whether to throw an error if cache is not ready. | | `ref` | [`String!`](#string) | Ref. | ##### `Project.ciPipelineCreationInputs` diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 2c1e783cdbc12ce611082471a33efaf8549cda30..51451b053ae0dce1675c8d52025fd67c65c7f52c 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -1785,4 +1785,108 @@ end end end + + describe 'ci_config_variables field' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user, developer_of: project) } + let(:ref) { project.default_branch } + let(:fail_on_cache_miss) { nil } + + let(:query) do + fail_on_cache_miss_arg = fail_on_cache_miss.nil? ? '' : ", failOnCacheMiss: #{fail_on_cache_miss}" + %( + query { + project(fullPath: "#{project.full_path}") { + ciConfigVariables(ref: "#{ref}"#{fail_on_cache_miss_arg}) { + key + value + description + } + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + def mock_config_variables_service(return_value) + allow_next_instance_of(::Ci::ListConfigVariablesService) do |service| + allow(service).to receive(:execute).and_return(return_value) + end + end + + context 'when service returns nil and fail_on_cache_miss is enabled' do + let(:fail_on_cache_miss) { true } + + before do + mock_config_variables_service(nil) + end + + it 'returns GraphQL error with expected message' do + expect(subject['errors']).to be_present + expect(subject['errors'].first['message']).to eq('Failed to retrieve CI/CD variables from cache.') + expect(subject['data']['project']['ciConfigVariables']).to be_nil + end + + it 'includes error path information' do + expect(subject['errors'].first['path']).to eq(%w[project ciConfigVariables]) + end + end + + context 'when variables are successfully fetched' do + let_it_be(:ci_config_variables) do + { + KEY1: { value: 'val 1', description: 'description 1' }, + KEY2: { value: 'val 2', description: '' }, + KEY3: { value: 'val 3' } + } + end + + before do + mock_config_variables_service(ci_config_variables) + end + + it 'returns variables list' do + variables = subject.dig('data', 'project', 'ciConfigVariables') + + expect(variables).to contain_exactly( + { 'key' => 'KEY1', 'value' => 'val 1', 'description' => 'description 1' }, + { 'key' => 'KEY2', 'value' => 'val 2', 'description' => '' }, + { 'key' => 'KEY3', 'value' => 'val 3', 'description' => nil } + ) + end + + it 'does not return any errors' do + expect(subject['errors']).to be_nil + end + end + + context 'with fail_on_cache_miss argument' do + context 'when service is called with fail_on_cache_miss parameter' do + before do + mock_config_variables_service({}) + end + + shared_examples 'calls service with expected fail_on_cache_miss value' do |expected_value| + it "calls service with fail_on_cache_miss: #{expected_value}" do + expect_next_instance_of(::Ci::ListConfigVariablesService) do |service| + expect(service).to receive(:execute).with(ref) + end + + subject + end + end + + context 'with default value (not specified)' do + it_behaves_like 'calls service with expected fail_on_cache_miss value', false + end + + context 'with fail_on_cache_miss: true' do + let(:fail_on_cache_miss) { true } + + it_behaves_like 'calls service with expected fail_on_cache_miss value', true + end + end + end + end end diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb index 6a9dcfee9667ada0b38398c157414e7e7862c1d5..34bb4ba6261ffd770626f65a12461e92b3a658b3 100644 --- a/spec/requests/api/graphql/ci/config_variables_spec.rb +++ b/spec/requests/api/graphql/ci/config_variables_spec.rb @@ -15,12 +15,13 @@ let(:service) { Ci::ListConfigVariablesService.new(project, user) } let(:ref) { project.default_branch } + let(:fail_on_cache_miss) { false } let(:query) do %( query { project(fullPath: "#{project.full_path}") { - ciConfigVariables(ref: "#{ref}") { + ciConfigVariables(ref: "#{ref}", failOnCacheMiss: #{fail_on_cache_miss}) { key value valueOptions @@ -40,19 +41,8 @@ end context 'when the cache is not empty' do - before do - synchronous_reactive_cache(service) - end - - it 'returns the CI variables for the config' do - expect(service) - .to receive(:execute) - .with(ref) - .and_call_original - - post_graphql(query, current_user: user) - - expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly( + let(:expected_ci_variables) do + [ { 'key' => 'KEY_VALUE_VAR', 'value' => 'value x', @@ -71,7 +61,32 @@ 'valueOptions' => ['env var value', 'env var value2'], 'description' => 'env var description' } - ) + ] + end + + shared_examples 'returns CI variables' do + it 'returns the CI variables for the config' do + expect(service) + .to receive(:execute) + .with(ref) + .and_call_original + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to match_array(expected_ci_variables) + end + end + + before do + synchronous_reactive_cache(service) + end + + it_behaves_like 'returns CI variables' + + context 'when failOnCacheMiss is true' do + let(:fail_on_cache_miss) { true } + + it_behaves_like 'returns CI variables' end end @@ -81,6 +96,20 @@ expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil end + + context 'when failOnCacheMiss is true' do + let(:fail_on_cache_miss) { true } + + it 'returns an error' do + post_graphql(query, current_user: user) + + expect(graphql_errors).to include( + a_hash_including( + 'message' => 'Failed to retrieve CI/CD variables from cache.' + ) + ) + end + end end end diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index 604abbe2edfd8efa57839ce37dd8ceba4b80e4dc..b364772b58dffbac4ed83f52935ad6e61a17de96 100644 --- a/spec/support/helpers/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb @@ -22,6 +22,8 @@ def synchronous_reactive_cache(subject) allow(subject).to receive(:with_reactive_cache) do |*args, &block| block.call(subject.calculate_reactive_cache(*args)) end + + allow(subject).to receive(:within_reactive_cache_lifetime?).and_return(true) end def read_reactive_cache(subject, *qualifiers)