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)