diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_variables_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_variables_form.vue
index 7389f6b773ee11c232c3a43fdb7b14439f981318..0466aefebfa3a96454bde762bbd5d10214f16033 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_variables_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_variables_form.vue
@@ -16,6 +16,7 @@ import { reportToSentry } from '~/ci/utils';
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
import { fetchPolicies } from '~/lib/graphql';
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
+import { createAlert } from '~/alert';
import filterVariables from '../utils/filter_variables';
import {
CI_VARIABLE_TYPE_FILE,
@@ -77,34 +78,10 @@ export default {
configVariablesWithDescription: {},
form: {},
maxPollTimeout: null,
+ pollingStartTime: null,
+ manualPollInterval: null,
};
},
- apollo: {
- ciConfigVariables: {
- fetchPolicy: fetchPolicies.NO_CACHE,
- query: ciConfigVariablesQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- ref: this.refParam,
- };
- },
- update({ project }) {
- return project?.ciConfigVariables;
- },
- result({ data }) {
- if (data?.project?.ciConfigVariables) {
- this.stopPollingAndPopulateForm();
- }
- },
- error(error) {
- reportToSentry(this.$options.name, error);
- this.ciConfigVariables = [];
- this.stopPollingAndPopulateForm();
- },
- pollInterval: CI_VARIABLES_POLLING_INTERVAL,
- },
- },
computed: {
descriptions() {
return this.form[this.refParam]?.descriptions ?? {};
@@ -141,12 +118,11 @@ export default {
refParam() {
this.ciConfigVariables = null;
this.clearTimeouts();
- this.$apollo.queries.ciConfigVariables.startPolling(CI_VARIABLES_POLLING_INTERVAL);
- this.startMaxPollTimer();
+ this.startManualPolling();
},
},
mounted() {
- this.startMaxPollTimer();
+ this.startManualPolling();
},
beforeDestroy() {
this.clearTimeouts();
@@ -175,6 +151,11 @@ export default {
clearTimeout(this.maxPollTimeout);
this.maxPollTimeout = null;
}
+ if (this.manualPollInterval) {
+ clearInterval(this.manualPollInterval);
+ this.manualPollInterval = null;
+ }
+ this.pollingStartTime = null;
},
createListItemsFromVariableOptions(key) {
const options = this.configVariablesWithDescription?.options?.[key] || [];
@@ -260,19 +241,66 @@ export default {
shouldShowValuesDropdown(key) {
return this.configVariablesWithDescription.options[key]?.length > 1;
},
- startMaxPollTimer() {
- this.maxPollTimeout = setTimeout(() => {
- if (this.ciConfigVariables === null) {
- this.ciConfigVariables = [];
- }
- this.stopPollingAndPopulateForm();
- }, CI_VARIABLES_MAX_POLLING_TIME);
- },
stopPollingAndPopulateForm() {
this.clearTimeouts();
- this.$apollo.queries.ciConfigVariables.stopPolling();
this.populateForm();
},
+ async executeQuery(failOnCacheMiss = false) {
+ try {
+ const result = await this.$apollo.query({
+ query: ciConfigVariablesQuery,
+ variables: {
+ fullPath: this.projectPath,
+ ref: this.refParam,
+ failOnCacheMiss,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ this.ciConfigVariables = result.data?.project?.ciConfigVariables;
+
+ if (this.ciConfigVariables) {
+ this.stopPollingAndPopulateForm();
+ return true;
+ }
+
+ return false;
+ } catch (error) {
+ this.handleQueryError(error);
+ return true;
+ }
+ },
+ handleQueryError(error) {
+ reportToSentry(this.$options.name, error);
+ createAlert({
+ message: error.message || s__('Pipeline|Failed to retrieve CI/CD variables.'),
+ });
+ this.ciConfigVariables = [];
+ this.stopPollingAndPopulateForm();
+ },
+ startManualPolling() {
+ const CI_VARIABLES_FINAL_ATTEMPT_THRESHOLD =
+ CI_VARIABLES_MAX_POLLING_TIME - CI_VARIABLES_POLLING_INTERVAL;
+ this.pollingStartTime = Date.now();
+
+ this.executeQuery(false);
+
+ this.manualPollInterval = setInterval(async () => {
+ const pollingDuration = Date.now() - this.pollingStartTime;
+ const isLastAttempt = pollingDuration >= CI_VARIABLES_FINAL_ATTEMPT_THRESHOLD;
+
+ const shouldStop = await this.executeQuery(isLastAttempt);
+
+ if (shouldStop) {
+ this.clearTimeouts();
+ }
+ }, CI_VARIABLES_POLLING_INTERVAL);
+
+ this.maxPollTimeout = setTimeout(() => {
+ this.ciConfigVariables = this.ciConfigVariables || [];
+ this.stopPollingAndPopulateForm();
+ }, CI_VARIABLES_MAX_POLLING_TIME);
+ },
},
};
diff --git a/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
index f93f5ad4f11a114b93653277ffc21f177c6e6b14..d6d1dea8498cd0cf2cbcafc21736c684d10556d8 100644
--- a/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
@@ -1,7 +1,7 @@
-query ciConfigVariables($fullPath: ID!, $ref: String!) {
+query ciConfigVariables($fullPath: ID!, $ref: String!, $failOnCacheMiss: Boolean = false) {
project(fullPath: $fullPath) {
id
- ciConfigVariables(ref: $ref) {
+ ciConfigVariables(ref: $ref, failOnCacheMiss: $failOnCacheMiss) {
description
key
value
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 14ebc91850492b14b8aa41781d8afd500090e3a2..0a790e60a91aa66714a6687c3c9adb0eec3c6577 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.'
@@ -1033,14 +1038,17 @@ def ci_pipeline_creation_inputs(ref:)
response.payload[:inputs].all_inputs
end
- def ci_config_variables(ref:)
- result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref)
+ def ci_config_variables(ref:, fail_on_cache_miss: false)
+ result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref,
+ fail_on_cache_miss: fail_on_cache_miss)
return if result.nil?
result.map do |var_key, var_config|
{ key: var_key, **var_config }
end
+ rescue ::Ci::ListConfigVariablesService::CacheNotReadyError => e
+ raise_resource_not_available_error! e.message
end
def job(id:)
diff --git a/app/services/ci/list_config_variables_service.rb b/app/services/ci/list_config_variables_service.rb
index 3e1c0ce3ff328ec0d1e02c2d4245c393c3867a63..7e46e425c5954bc4f9322559502d602724898ec5 100644
--- a/app/services/ci/list_config_variables_service.rb
+++ b/app/services/ci/list_config_variables_service.rb
@@ -4,6 +4,8 @@ module Ci
class ListConfigVariablesService < ::BaseService
include ReactiveCaching
+ CacheNotReadyError = Class.new(StandardError)
+
self.reactive_cache_key = ->(service) { [service.class.name, service.id] }
self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(id, *_args) { from_cache(id) }
@@ -17,11 +19,18 @@ def self.from_cache(id)
new(project, user)
end
- def execute(ref)
+ def execute(ref, fail_on_cache_miss: false)
# "ref" is not a enough for a cache key because the name is static but that branch can be changed any time
sha = project.commit(ref).try(:sha)
- with_reactive_cache(sha, ref) { |result| result }
+ result = with_reactive_cache(sha, ref) { |result| result }
+
+ if result.nil? && fail_on_cache_miss
+ raise CacheNotReadyError,
+ "Failed to retrieve CI/CD variables from cache."
+ end
+
+ result
end
def calculate_reactive_cache(sha, ref)
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index c4f293dda8a16211e689a62d8a99db89d3321751..86d411833c486cdee7697b2ddb0946d891517d7e 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -39851,6 +39851,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/locale/gitlab.pot b/locale/gitlab.pot
index c38132d48051deea4fe330daf4f790d95f009488..0d00c0865e23babfe7c646a5bc511a7b3eea4c81 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -48973,6 +48973,9 @@ msgstr ""
msgid "Pipeline|Failed"
msgstr ""
+msgid "Pipeline|Failed to retrieve CI/CD variables."
+msgstr ""
+
msgid "Pipeline|File"
msgstr ""
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_variables_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_variables_form_spec.js
index 689d10821f1636f2eecd78e0df9104a361a9f7a4..a2170d4c8cd218fa9aff5b0dd028e9ecb3cfd232 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_variables_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_variables_form_spec.js
@@ -5,13 +5,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { fetchPolicies } from '~/lib/graphql';
import { reportToSentry } from '~/ci/utils';
import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
import { VARIABLE_TYPE } from '~/ci/pipeline_new/constants';
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
import PipelineVariablesForm from '~/ci/pipeline_new/components/pipeline_variables_form.vue';
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+jest.useFakeTimers();
Vue.use(VueApollo);
jest.mock('~/ci/utils');
@@ -113,6 +116,8 @@ describe('PipelineVariablesForm', () => {
},
},
});
+
+ jest.clearAllMocks();
});
it('displays the inputs adoption banner', async () => {
@@ -219,68 +224,152 @@ describe('PipelineVariablesForm', () => {
});
describe('query configuration', () => {
- it('has correct apollo query configuration', async () => {
- await createComponent();
- const { apollo } = wrapper.vm.$options;
-
- expect(apollo.ciConfigVariables.fetchPolicy).toBe(fetchPolicies.NO_CACHE);
- expect(apollo.ciConfigVariables.query).toBe(ciConfigVariablesQuery);
- });
-
it('makes query with correct variables', async () => {
await createComponent();
expect(mockCiConfigVariables).toHaveBeenCalledWith({
fullPath: defaultProvide.projectPath,
ref: defaultProps.refParam,
+ failOnCacheMiss: false,
});
});
- it('reports to sentry when query fails', async () => {
+ it('handles query errors in executeQuery method', async () => {
const error = new Error('GraphQL error');
+ const mockQuery = jest.fn().mockRejectedValue(error);
+
await createComponent();
- wrapper.vm.$options.apollo.ciConfigVariables.error.call(wrapper.vm, error);
+ wrapper.vm.$apollo.query = mockQuery;
+
+ await wrapper.vm.executeQuery(false);
expect(reportToSentry).toHaveBeenCalledWith('PipelineVariablesForm', error);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'GraphQL error',
+ });
+ expect(wrapper.vm.ciConfigVariables).toEqual([]);
+ });
+
+ it('handles query errors with failOnCacheMiss flag in executeQuery method', async () => {
+ const error = new Error('GraphQL error');
+ const mockQuery = jest.fn().mockRejectedValue(error);
+
+ await createComponent();
+ wrapper.vm.$apollo.query = mockQuery;
+
+ const result = await wrapper.vm.executeQuery(true);
+
+ expect(result).toBe(true);
+ expect(reportToSentry).toHaveBeenCalledWith('PipelineVariablesForm', error);
+ });
+
+ it('returns false when no ciConfigVariables received', async () => {
+ const mockQuery = jest.fn().mockResolvedValue({
+ data: { project: { ciConfigVariables: null } },
+ });
+
+ await createComponent();
+ wrapper.vm.$apollo.query = mockQuery;
+
+ const result = await wrapper.vm.executeQuery(false);
+
+ expect(result).toBe(false);
+ });
+
+ it('returns true when ciConfigVariables received', async () => {
+ const mockQuery = jest.fn().mockResolvedValue({
+ data: { project: { ciConfigVariables: configVariablesWithOptions } },
+ });
+
+ await createComponent();
+ wrapper.vm.$apollo.query = mockQuery;
+
+ const result = await wrapper.vm.executeQuery(false);
+
+ expect(result).toBe(true);
});
});
describe('polling behavior', () => {
- it('configures Apollo with the correct polling interval', () => {
- expect(PipelineVariablesForm.apollo.ciConfigVariables.pollInterval).toBe(2000);
+ it('starts manual polling with correct interval', async () => {
+ jest.spyOn(global, 'setInterval');
+ jest.spyOn(global, 'setTimeout');
+
+ await createComponent();
+
+ wrapper.vm.startManualPolling();
+
+ expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 2000);
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 10000);
+ });
+
+ it('executes query immediately when starting manual polling', async () => {
+ const executeQuerySpy = jest.spyOn(PipelineVariablesForm.methods, 'executeQuery');
+
+ await createComponent();
+
+ expect(executeQuerySpy).toHaveBeenCalledWith(false);
});
it('refetches and updates on ref change', async () => {
await createComponent();
- wrapper.setProps({ refParam: 'new-ref-param' });
- await nextTick();
+ const startManualPollingSpy = jest.spyOn(wrapper.vm, 'startManualPolling');
+
+ await wrapper.setProps({ refParam: 'refs/heads/new-feature' });
+
+ expect(startManualPollingSpy).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ refParam: 'refs/heads/new-feature-1' });
- expect(wrapper.vm.ciConfigVariables).toBe(null);
+ expect(startManualPollingSpy).toHaveBeenCalledTimes(2);
});
- it('sets ciConfigVariables to empty array on query error', async () => {
+ it('stops polling when data is received', async () => {
+ jest.spyOn(global, 'clearInterval');
+ jest.spyOn(global, 'clearTimeout');
+
+ mockCiConfigVariables = jest
+ .fn()
+ .mockResolvedValueOnce({
+ data: { project: { ciConfigVariables: null } },
+ })
+ .mockResolvedValueOnce({
+ data: { project: { ciConfigVariables: configVariablesWithOptions } },
+ });
+
await createComponent();
- const error = new Error('GraphQL error');
- wrapper.vm.$options.apollo.ciConfigVariables.error.call(wrapper.vm, error);
+ jest.advanceTimersByTime(2000);
+ await waitForPromises();
- expect(wrapper.vm.ciConfigVariables).toEqual([]);
- expect(reportToSentry).toHaveBeenCalledWith('PipelineVariablesForm', error);
+ expect(clearInterval).toHaveBeenCalled();
+ expect(clearTimeout).toHaveBeenCalled();
});
- it('stops polling when data is received', async () => {
- await createComponent({ configVariables: configVariablesWithOptions });
+ it('clears all timeouts when polling stops due to successful query', async () => {
+ jest.spyOn(global, 'clearInterval');
+ jest.spyOn(global, 'clearTimeout');
- const stopPollingSpy = jest.spyOn(
- wrapper.vm.$apollo.queries.ciConfigVariables,
- 'stopPolling',
- );
+ await createComponent();
+ jest.spyOn(wrapper.vm, 'executeQuery').mockResolvedValue(true);
- const mockData = { data: { project: { ciConfigVariables: configVariablesWithOptions } } };
- wrapper.vm.$options.apollo.ciConfigVariables.result.call(wrapper.vm, mockData);
+ jest.advanceTimersByTime(2000);
+ await waitForPromises();
- expect(stopPollingSpy).toHaveBeenCalled();
+ expect(clearInterval).toHaveBeenCalled();
+ expect(clearTimeout).toHaveBeenCalled();
+ expect(wrapper.vm.manualPollInterval).toBe(null);
+ expect(wrapper.vm.pollingStartTime).toBe(null);
+ });
+
+ it('sets empty array when max poll timeout reached', async () => {
+ await createComponent();
+
+ jest.advanceTimersByTime(10000);
+ await waitForPromises();
+
+ expect(wrapper.vm.ciConfigVariables).toEqual([]);
});
});
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 1a98eeb3706baf54ab1ceebe054de4cfdeeb4d70..bdb844d5869e9fefc170fee630e2f13894aa262f 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -1785,4 +1785,141 @@
end
end
end
+
+ describe 'ci_config_variables field' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let(:ref) { project.default_branch }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciConfigVariables(ref: "#{ref}") {
+ key
+ value
+ description
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when cache is not ready' do
+ before do
+ allow_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ allow(service).to receive(:execute).and_raise(
+ ::Ci::ListConfigVariablesService::CacheNotReadyError,
+ 'Failed to retrieve CI configuration variables from cache.'
+ )
+ end
+ 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 configuration 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(:ci_config_variables) do
+ {
+ KEY1: { value: 'val 1', description: 'description 1' },
+ KEY2: { value: 'val 2', description: '' },
+ KEY3: { value: 'val 3' }
+ }
+ end
+
+ before do
+ allow_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ allow(service).to receive(:execute).and_return(ci_config_variables)
+ end
+ 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
+ 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
+
+ context 'when service is called with fail_on_cache_miss parameter' do
+ before do
+ allow_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ allow(service).to receive(:execute).and_return({})
+ end
+ end
+
+ context 'with default value (not specified)' do
+ it 'calls service with fail_on_cache_miss: false' do
+ expect_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ expect(service).to receive(:execute).with(ref, fail_on_cache_miss: false)
+ end
+
+ subject
+ end
+ end
+
+ context 'with fail_on_cache_miss: false' do
+ let(:fail_on_cache_miss) { false }
+
+ it 'calls service with fail_on_cache_miss: false' do
+ expect_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ expect(service).to receive(:execute).with(ref, fail_on_cache_miss: false)
+ end
+
+ subject
+ end
+ end
+
+ context 'with fail_on_cache_miss: true' do
+ let(:fail_on_cache_miss) { true }
+
+ it 'calls service with fail_on_cache_miss: true' do
+ expect_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ expect(service).to receive(:execute).with(ref, fail_on_cache_miss: true)
+ end
+
+ subject
+ end
+ 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..03916d8a81b707da674b67c81984b63c30fb5d85 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, fail_on_cache_miss: fail_on_cache_miss)
+ .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/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 4c363412f3dfb8886d9349ec05a03f9aeae5b68f..fc9f1f0b8607b1055cff6fda23bc6d07921d8bb0 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -306,5 +306,20 @@
expect(result).to be_nil
end
+
+ context 'when fail_on_cache_miss is true' do
+ subject(:result) { service.execute(ref, fail_on_cache_miss: true) }
+
+ it 'raises CacheNotReadyError' do
+ expect { result }.to raise_error(described_class::CacheNotReadyError)
+ end
+
+ it 'raises error with expected message' do
+ expect { result }.to raise_error(
+ described_class::CacheNotReadyError,
+ '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)