diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-10 15:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-10 15:09:36 +0300 |
commit | 948023c9c900344aa1e2f334bcaae5a194873b0d (patch) | |
tree | 846c5dbcec70436bca337d970bd11082f91eeb66 /spec | |
parent | f42c4be1c0d5247fac0c059ec41c9a1961485aed (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
35 files changed, 740 insertions, 464 deletions
diff --git a/spec/components/pajamas/alert_component_spec.rb b/spec/components/pajamas/alert_component_spec.rb index e596f07a15a..db425fb2dce 100644 --- a/spec/components/pajamas/alert_component_spec.rb +++ b/spec/components/pajamas/alert_component_spec.rb @@ -138,5 +138,35 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do end end end + + context 'with alert_options' do + let(:options) { { alert_options: { id: 'test_id', class: 'baz', data: { foo: 'bar' } } } } + + before do + render_inline described_class.new(**options) + end + + it 'renders the extra options' do + expect(rendered_component).to have_css "#test_id.gl-alert.baz[data-foo='bar']" + end + + context 'with custom classes or data' do + let(:options) do + { + variant: :danger, + alert_class: 'custom', + alert_data: { foo: 'bar' }, + alert_options: { + class: 'extra special', + data: { foo: 'conflict' } + } + } + end + + it 'doesn\'t conflict with internal alert_class or alert_data' do + expect(rendered_component).to have_css ".extra.special.custom.gl-alert.gl-alert-danger[data-foo='bar']" + end + end + end end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 07874c8a8af..85e5de46afd 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -84,100 +84,6 @@ RSpec.describe Projects::NotesController do end end - context 'for multiple pages of notes', :aggregate_failures do - # 3 pages worth: 1 normal page, 1 oversized due to clashing updated_at, - # and a final, short page - let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 3.days.ago) } - let!(:page_2) { create_list(:note, 3, noteable: issue, project: project, updated_at: 2.days.ago) } - let!(:page_3) { create_list(:note, 2, noteable: issue, project: project, updated_at: 1.day.ago) } - - # Include a resource event in the middle page as well - let!(:resource_event) { create(:resource_state_event, issue: issue, user: user, created_at: 2.days.ago) } - - let(:page_1_boundary) { microseconds(page_1.last.updated_at + NotesFinder::FETCH_OVERLAP) } - let(:page_2_boundary) { microseconds(page_2.last.updated_at + NotesFinder::FETCH_OVERLAP) } - - around do |example| - freeze_time do - example.run - end - end - - before do - stub_const('Gitlab::UpdatedNotesPaginator::LIMIT', 2) - end - - context 'feature flag enabled' do - before do - stub_feature_flags(paginated_notes: true) - end - - it 'returns the first page of notes' do - expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) - - get :index, params: request_params - - expect(json_response['notes'].count).to eq(page_1.count) - expect(json_response['more']).to be_truthy - expect(json_response['last_fetched_at']).to eq(page_1_boundary) - expect(response.headers['Poll-Interval'].to_i).to eq(1) - end - - it 'returns the second page of notes' do - expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) - - request.headers['X-Last-Fetched-At'] = page_1_boundary - - get :index, params: request_params - - expect(json_response['notes'].count).to eq(page_2.count + 1) # resource event - expect(json_response['more']).to be_truthy - expect(json_response['last_fetched_at']).to eq(page_2_boundary) - expect(response.headers['Poll-Interval'].to_i).to eq(1) - end - - it 'returns the final page of notes' do - expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) - - request.headers['X-Last-Fetched-At'] = page_2_boundary - - get :index, params: request_params - - expect(json_response['notes'].count).to eq(page_3.count) - expect(json_response['more']).to be_falsy - expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) - expect(response.headers['Poll-Interval'].to_i).to be > 1 - end - - it 'returns an empty page of notes' do - expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!) - - request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now) - - get :index, params: request_params - - expect(json_response['notes']).to be_empty - expect(json_response['more']).to be_falsy - expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) - expect(response.headers['Poll-Interval'].to_i).to be > 1 - end - end - - context 'feature flag disabled' do - before do - stub_feature_flags(paginated_notes: false) - end - - it 'returns all notes' do - get :index, params: request_params - - expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1) - expect(json_response['more']).to be_falsy - expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now)) - end - end - end - context 'for a discussion note' do let(:project) { create(:project, :repository) } let!(:note) { create(:discussion_note_on_merge_request, project: project) } diff --git a/spec/factories/plan_limits.rb b/spec/factories/plan_limits.rb index ad10629af05..1e4f70cd925 100644 --- a/spec/factories/plan_limits.rb +++ b/spec/factories/plan_limits.rb @@ -6,8 +6,10 @@ FactoryBot.define do dast_profile_schedules { 50 } - trait :default_plan do - plan factory: :default_plan + Plan.all_plans.each do |plan| + trait :"#{plan}_plan" do + plan factory: :"#{plan}_plan" + end end trait :with_package_file_sizes do diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index 9b54d95be6b..9e59ba034d2 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -13,8 +13,6 @@ RSpec.describe 'Merge request > Batch comments', :js do end before do - stub_feature_flags(paginated_notes: false) - project.add_maintainer(user) sign_in(user) diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index 20bf1a2939c..f77a42ee506 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -26,8 +26,6 @@ RSpec.describe 'User comments on a diff', :js do let(:user) { create(:user) } before do - stub_feature_flags(paginated_notes: false) - project.add_maintainer(user) sign_in(user) diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js index aaabdbc82d9..d55ba318e46 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -29,7 +29,7 @@ describe('WorkItemDetailModal component', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); - const createComponent = ({ workItemId = '1', error = false } = {}) => { + const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => { const apolloProvider = createMockApollo([ [ deleteWorkItemFromTaskMutation, @@ -46,7 +46,7 @@ describe('WorkItemDetailModal component', () => { wrapper = shallowMount(WorkItemDetailModal, { apolloProvider, - propsData: { workItemId }, + propsData: { workItemId, issueGid }, data() { return { error, @@ -67,6 +67,7 @@ describe('WorkItemDetailModal component', () => { expect(findWorkItemDetail().props()).toEqual({ workItemId: '1', + workItemParentId: '2', }); }); @@ -97,13 +98,6 @@ describe('WorkItemDetailModal component', () => { expect(wrapper.emitted('close')).toBeTruthy(); }); - it('emits `workItemUpdated` event on updating work item', () => { - createComponent(); - findWorkItemDetail().vm.$emit('workItemUpdated'); - - expect(wrapper.emitted('workItemUpdated')).toBeTruthy(); - }); - describe('delete work item', () => { it('emits workItemDeleted and closes modal', async () => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index 0e4b73933b1..b379d1fc846 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -82,15 +82,6 @@ describe('WorkItemState component', () => { }); }); - it('emits updated event', async () => { - createComponent(); - - findItemState().vm.$emit('changed', STATE_CLOSED); - await waitForPromises(); - - expect(wrapper.emitted('updated')).toEqual([[]]); - }); - it('emits an error message when the mutation was unsuccessful', async () => { createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') }); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index 168a742090b..a48449bb636 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -8,6 +8,7 @@ import ItemTitle from '~/work_items/components/item_title.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data'; describe('WorkItemTitle component', () => { @@ -19,14 +20,18 @@ describe('WorkItemTitle component', () => { const findItemTitle = () => wrapper.findComponent(ItemTitle); - const createComponent = ({ mutationHandler = mutationSuccessHandler } = {}) => { + const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => { const { id, title, workItemType } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemTitle, { - apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), + apolloProvider: createMockApollo([ + [updateWorkItemMutation, mutationHandler], + [updateWorkItemTaskMutation, mutationHandler], + ]), propsData: { workItemId: id, workItemTitle: title, workItemType: workItemType.name, + workItemParentId, }, }); }; @@ -57,13 +62,25 @@ describe('WorkItemTitle component', () => { }); }); - it('emits updated event', async () => { - createComponent(); + it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => { + const title = 'new title!'; + const workItemParentId = '1234'; - findItemTitle().vm.$emit('title-changed', 'new title'); - await waitForPromises(); + createComponent({ + workItemParentId, + }); - expect(wrapper.emitted('updated')).toEqual([[]]); + findItemTitle().vm.$emit('title-changed', title); + + expect(mutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: workItemParentId, + taskData: { + id: workItemQueryResponse.data.workItem.id, + title, + }, + }, + }); }); it('does not call a mutation when the title has not changed', () => { diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 33cf2636dd5..b9724034cb4 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -141,20 +141,6 @@ describe('WorkItemDetail component', () => { }); }); - it('emits workItemUpdated event when fields updated', async () => { - createComponent(); - - await waitForPromises(); - - findWorkItemState().vm.$emit('updated'); - - expect(wrapper.emitted('workItemUpdated')).toEqual([[]]); - - findWorkItemTitle().vm.$emit('updated'); - - expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]); - }); - describe('when work_items_mvc_2 feature flag is enabled', () => { it('renders assignees component when assignees widget is returned from the API', async () => { createComponent({ diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index 85096392e84..61af6f316da 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -52,6 +52,7 @@ describe('Work items root component', () => { expect(findWorkItemDetail().props()).toEqual({ workItemId: 'gid://gitlab/WorkItem/1', + workItemParentId: null, }); }); diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 969ef6cae7f..1294bf0ebaf 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -227,13 +227,29 @@ RSpec.describe EmailsHelper do describe '#header_logo' do context 'there is a brand item with a logo' do - it 'returns the brand header logo' do - appearance = create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png') + let_it_be(:appearance) { create(:appearance) } + + let(:logo_path) { 'spec/fixtures/dk.png' } + before do + appearance.update!(header_logo: fixture_file_upload(logo_path)) + end + + it 'returns the brand header logo' do expect(header_logo).to eq( %{<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" />} ) end + + context 'that is a SVG file' do + let(:logo_path) { 'spec/fixtures/logo_sample.svg' } + + it 'returns the default header logo' do + expect(header_logo).to match( + %r{<img alt="GitLab" src="/images/mailers/gitlab_logo\.(?:gif|png)" width="\d+" height="\d+" />} + ) + end + end end context 'there is a brand item without a logo' do diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb index 7d5ecbf7a57..25dfa2251c3 100644 --- a/spec/helpers/form_helper_spec.rb +++ b/spec/helpers/form_helper_spec.rb @@ -10,11 +10,16 @@ RSpec.describe FormHelper do expect(helper.form_errors(model)).to be_nil end - it 'renders an alert div' do + it 'renders an appropriately styled alert div' do model = double(errors: errors_stub('Error 1')) - expect(helper.form_errors(model)) + expect(helper.form_errors(model, pajamas_alert: false)) .to include('<div class="alert alert-danger" id="error_explanation">') + + expect(helper.form_errors(model, pajamas_alert: true)) + .to include( + '<div class="gl-alert gl-alert-danger gl-alert-not-dismissible gl-mb-5" id="error_explanation" role="alert">' + ) end it 'contains a summary message' do @@ -22,9 +27,9 @@ RSpec.describe FormHelper do multi_errors = double(errors: errors_stub('A', 'B', 'C')) expect(helper.form_errors(single_error)) - .to include('<h4>The form contains the following error:') + .to include('The form contains the following error:') expect(helper.form_errors(multi_errors)) - .to include('<h4>The form contains the following errors:') + .to include('The form contains the following errors:') end it 'renders each message' do diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 913a38d353f..68a6b6293c8 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -329,10 +329,6 @@ RSpec.describe NotesHelper do allow(helper).to receive(:current_user).and_return(guest) end - it 'sets last_fetched_at to 0 when start_at_zero is true' do - expect(helper.notes_data(issue, true)[:lastFetchedAt]).to eq(0) - end - it 'includes the current notes filter for the user' do guest.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue) diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb new file mode 100644 index 00000000000..3ed4a9f263f --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do + let(:factory) do + Gitlab::Config::Entry::Factory.new(described_class) + .value(config) + end + + subject(:entry) { factory.create! } + + before do + entry.compose! + end + + describe '.new' do + context 'when using a string array' do + let(:config) { %w[app/ lib/ spec/ other/* paths/**/*.rb] } + + it { is_expected.to be_valid } + end + + context 'when using an integer array' do + let(:config) { [1, 2] } + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(entry.errors).to include(/changes config should be an array of strings/) + end + end + + context 'when using a string' do + let(:config) { 'a regular string' } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid policy' do + expect(entry.errors).to include(/should be an array of strings/) + end + end + + context 'when using a long array' do + let(:config) { ['app/'] * 51 } + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(entry.errors).to include(/has too many entries \(maximum 50\)/) + end + end + + context 'when clause is empty' do + let(:config) {} + + it { is_expected.to be_valid } + end + + context 'when policy strategy does not match' do + let(:config) { 'string strategy' } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be an array of strings/) + end + end + end + + describe '#value' do + subject(:value) { entry.value } + + context 'when using a string array' do + let(:config) { %w[app/ lib/ spec/ other/* paths/**/*.rb] } + + it { is_expected.to eq(config) } + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 86270788431..89d349efe8f 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -18,6 +18,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do let(:entry) { factory.create! } + before do + entry.compose! + end + describe '.new' do subject { entry } @@ -121,7 +125,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it { is_expected.not_to be_valid } it 'returns errors' do - expect(subject.errors).to include(/changes should be an array of strings/) + expect(subject.errors).to include(/changes config should be an array of strings/) end end @@ -131,7 +135,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it { is_expected.not_to be_valid } it 'returns errors' do - expect(subject.errors).to include(/changes is too long \(maximum is 50 characters\)/) + expect(subject.errors).to include(/changes config has too many entries \(maximum 50\)/) end end @@ -434,6 +438,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do end describe '.default' do + let(:config) {} + it 'does not have default value' do expect(described_class.default).to be_nil end diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb index 5b0917c5c6f..8f727749ee2 100644 --- a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb @@ -4,9 +4,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do let_it_be_with_refind(:namespace) { create(:namespace) } - let_it_be_with_reload(:default_plan) { create(:default_plan) } let_it_be_with_reload(:project) { create(:project, :repository, namespace: namespace) } - let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) } + let_it_be(:plan_limits) { create(:plan_limits, :default_plan) } let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb index 5e965f94347..3ae0e5d1f0e 100644 --- a/spec/lib/gitlab/ci/trace/archive_spec.rb +++ b/spec/lib/gitlab/ci/trace/archive_spec.rb @@ -29,35 +29,59 @@ RSpec.describe Gitlab::Ci::Trace::Archive do let(:stream) { StringIO.new(trace, 'rb') } let(:src_checksum) { Digest::MD5.hexdigest(trace) } - context 'when the object store is disabled' do - before do - stub_artifacts_object_storage(enabled: false) + shared_examples 'valid' do + it 'does not count as invalid' do + subject.execute!(stream) + + expect(metrics) + .not_to have_received(:increment_error_counter) + .with(error_reason: :archive_invalid_checksum) end + end - it 'skips validation' do + shared_examples 'local checksum only' do + it 'generates only local checksum' do subject.execute!(stream) + expect(trace_metadata.checksum).to eq(src_checksum) expect(trace_metadata.remote_checksum).to be_nil - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(error_reason: :archive_invalid_checksum) end end - context 'with background_upload enabled' do + shared_examples 'skips validations' do + it_behaves_like 'valid' + it_behaves_like 'local checksum only' + end + + shared_context 'with FIPS' do + context 'with FIPS enabled', :fips_mode do + it_behaves_like 'valid' + + it 'does not generate md5 checksums' do + subject.execute!(stream) + + expect(trace_metadata.checksum).to be_nil + expect(trace_metadata.remote_checksum).to be_nil + end + end + end + + context 'when the object store is disabled' do before do - stub_artifacts_object_storage(background_upload: true) + stub_artifacts_object_storage(enabled: false) end - it 'skips validation' do - subject.execute!(stream) + it_behaves_like 'skips validations' + include_context 'with FIPS' + end - expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.remote_checksum).to be_nil - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(error_reason: :archive_invalid_checksum) + context 'with background_upload enabled' do + before do + stub_artifacts_object_storage(background_upload: true) end + + it_behaves_like 'skips validations' + include_context 'with FIPS' end context 'with direct_upload enabled' do @@ -65,27 +89,26 @@ RSpec.describe Gitlab::Ci::Trace::Archive do stub_artifacts_object_storage(direct_upload: true) end - it 'validates the archived trace' do + it_behaves_like 'valid' + + it 'checksums match' do subject.execute!(stream) expect(trace_metadata.checksum).to eq(src_checksum) expect(trace_metadata.remote_checksum).to eq(src_checksum) - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(error_reason: :archive_invalid_checksum) end context 'when the checksum does not match' do let(:invalid_remote_checksum) { SecureRandom.hex } before do - expect(::Gitlab::Ci::Trace::RemoteChecksum) + allow(::Gitlab::Ci::Trace::RemoteChecksum) .to receive(:new) .with(an_instance_of(Ci::JobArtifact)) .and_return(double(md5_checksum: invalid_remote_checksum)) end - it 'validates the archived trace' do + it 'counts as invalid' do subject.execute!(stream) expect(trace_metadata.checksum).to eq(src_checksum) @@ -94,7 +117,11 @@ RSpec.describe Gitlab::Ci::Trace::Archive do .to have_received(:increment_error_counter) .with(error_reason: :archive_invalid_checksum) end + + include_context 'with FIPS' end + + include_context 'with FIPS' end end end diff --git a/spec/lib/gitlab/metrics/sli_spec.rb b/spec/lib/gitlab/metrics/sli_spec.rb index 9b776d6738d..102ea442b3a 100644 --- a/spec/lib/gitlab/metrics/sli_spec.rb +++ b/spec/lib/gitlab/metrics/sli_spec.rb @@ -17,13 +17,13 @@ RSpec.describe Gitlab::Metrics::Sli do it 'allows different SLIs to be defined on each subclass' do apdex_counters = [ - fake_total_counter('foo', 'apdex'), - fake_numerator_counter('foo', 'apdex', 'success') + fake_total_counter('foo_apdex'), + fake_numerator_counter('foo_apdex', 'success') ] error_rate_counters = [ - fake_total_counter('foo', 'error_rate'), - fake_numerator_counter('foo', 'error_rate', 'error') + fake_total_counter('foo'), + fake_numerator_counter('foo', 'error') ] apdex = described_class::Apdex.initialize_sli(:foo, [{ hello: :world }]) @@ -40,13 +40,17 @@ RSpec.describe Gitlab::Metrics::Sli do end subclasses = { - Gitlab::Metrics::Sli::Apdex => :success, - Gitlab::Metrics::Sli::ErrorRate => :error + Gitlab::Metrics::Sli::Apdex => { + suffix: '_apdex', + numerator: :success + }, + Gitlab::Metrics::Sli::ErrorRate => { + suffix: '', + numerator: :error + } } - subclasses.each do |subclass, numerator_type| - subclass_type = subclass.to_s.demodulize.underscore - + subclasses.each do |subclass, subclass_info| describe subclass do describe 'Class methods' do before do @@ -73,8 +77,8 @@ RSpec.describe Gitlab::Metrics::Sli do describe '.initialize_sli' do it 'returns and stores a new initialized SLI' do counters = [ - fake_total_counter(:bar, subclass_type), - fake_numerator_counter(:bar, subclass_type, numerator_type) + fake_total_counter("bar#{subclass_info[:suffix]}"), + fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator]) ] sli = described_class.initialize_sli(:bar, [{ hello: :world }]) @@ -86,8 +90,8 @@ RSpec.describe Gitlab::Metrics::Sli do it 'does not change labels for an already-initialized SLI' do counters = [ - fake_total_counter(:bar, subclass_type), - fake_numerator_counter(:bar, subclass_type, numerator_type) + fake_total_counter("bar#{subclass_info[:suffix]}"), + fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator]) ] sli = described_class.initialize_sli(:bar, [{ hello: :world }]) @@ -106,8 +110,8 @@ RSpec.describe Gitlab::Metrics::Sli do describe '.initialized?' do before do - fake_total_counter(:boom, subclass_type) - fake_numerator_counter(:boom, subclass_type, numerator_type) + fake_total_counter("boom#{subclass_info[:suffix]}") + fake_numerator_counter("boom#{subclass_info[:suffix]}", subclass_info[:numerator]) end it 'is true when an SLI was initialized with labels' do @@ -125,8 +129,8 @@ RSpec.describe Gitlab::Metrics::Sli do describe '#initialize_counters' do it 'initializes counters for the passed label combinations' do counters = [ - fake_total_counter(:hey, subclass_type), - fake_numerator_counter(:hey, subclass_type, numerator_type) + fake_total_counter("hey#{subclass_info[:suffix]}"), + fake_numerator_counter("hey#{subclass_info[:suffix]}", subclass_info[:numerator]) ] described_class.new(:hey).initialize_counters([{ foo: 'bar' }, { foo: 'baz' }]) @@ -138,18 +142,18 @@ RSpec.describe Gitlab::Metrics::Sli do describe "#increment" do let!(:sli) { described_class.new(:heyo) } - let!(:total_counter) { fake_total_counter(:heyo, subclass_type) } - let!(:numerator_counter) { fake_numerator_counter(:heyo, subclass_type, numerator_type) } + let!(:total_counter) { fake_total_counter("heyo#{subclass_info[:suffix]}") } + let!(:numerator_counter) { fake_numerator_counter("heyo#{subclass_info[:suffix]}", subclass_info[:numerator]) } - it "increments both counters for labels when #{numerator_type} is true" do - sli.increment(labels: { hello: "world" }, numerator_type => true) + it "increments both counters for labels when #{subclass_info[:numerator]} is true" do + sli.increment(labels: { hello: "world" }, subclass_info[:numerator] => true) expect(total_counter).to have_received(:increment).with({ hello: 'world' }) expect(numerator_counter).to have_received(:increment).with({ hello: 'world' }) end - it "only increments the total counters for labels when #{numerator_type} is false" do - sli.increment(labels: { hello: "world" }, numerator_type => false) + it "only increments the total counters for labels when #{subclass_info[:numerator]} is false" do + sli.increment(labels: { hello: "world" }, subclass_info[:numerator] => false) expect(total_counter).to have_received(:increment).with({ hello: 'world' }) expect(numerator_counter).not_to have_received(:increment).with({ hello: 'world' }) @@ -168,11 +172,11 @@ RSpec.describe Gitlab::Metrics::Sli do fake_counter end - def fake_total_counter(name, type) - fake_prometheus_counter("gitlab_sli:#{name}_#{type}:total") + def fake_total_counter(name) + fake_prometheus_counter("gitlab_sli:#{name}:total") end - def fake_numerator_counter(name, type, numerator_name) - fake_prometheus_counter("gitlab_sli:#{name}_#{type}:#{numerator_name}_total") + def fake_numerator_counter(name, numerator_name) + fake_prometheus_counter("gitlab_sli:#{name}:#{numerator_name}_total") end end diff --git a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb index 33f7391a836..53e3d73d17e 100644 --- a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb +++ b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb @@ -12,17 +12,11 @@ RSpec.describe Gitlab::Redis::DuplicateJobs do include_examples "redis_shared_examples" describe '#pool' do - let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } - let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } - subject { described_class.pool } before do redis_clear_raw_config!(Gitlab::Redis::SharedState) redis_clear_raw_config!(Gitlab::Redis::Queues) - - allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host) - allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket) end after do @@ -37,14 +31,46 @@ RSpec.describe Gitlab::Redis::DuplicateJobs do clear_pool end - it 'instantiates an instance of MultiStore' do - subject.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) + context 'store connection settings' do + let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } + let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } + + before do + allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host) + allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket) + end + + it 'instantiates an instance of MultiStore' do + subject.with do |redis_instance| + expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) + + expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") + expect(redis_instance.primary_store.connection[:namespace]).to be_nil + expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0") + expect(redis_instance.secondary_store.connection[:namespace]).to eq("resque:gitlab") + + expect(redis_instance.instance_name).to eq('DuplicateJobs') + end + end + end + + # Make sure they current namespace is respected for the secondary store but omitted from the primary + context 'key namespaces' do + let(:key) { 'key' } + let(:value) { '123' } + + it 'writes keys to SharedState with no prefix, and to Queues with the "resque:gitlab:" prefix' do + subject.with do |redis_instance| + redis_instance.set(key, value) + end - expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") - expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0") + Gitlab::Redis::SharedState.with do |redis_instance| + expect(redis_instance.get(key)).to eq(value) + end - expect(redis_instance.instance_name).to eq('DuplicateJobs') + Gitlab::Redis::Queues.with do |redis_instance| + expect(redis_instance.get("resque:gitlab:#{key}")).to eq(value) + end end end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index 70f28b38082..e127c89c303 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -65,6 +65,7 @@ RSpec.describe Gitlab::Redis::MultiStore do context 'when primary_store is not a ::Redis instance' do before do allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false) + allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false) end it 'fails with exception' do @@ -73,9 +74,21 @@ RSpec.describe Gitlab::Redis::MultiStore do end end + context 'when primary_store is a ::Redis::Namespace instance' do + before do + allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false) + allow(primary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true) + end + + it 'fails with exception' do + expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error + end + end + context 'when secondary_store is not a ::Redis instance' do before do allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false) + allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(false) end it 'fails with exception' do @@ -84,6 +97,17 @@ RSpec.describe Gitlab::Redis::MultiStore do end end + context 'when secondary_store is a ::Redis::Namespace instance' do + before do + allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false) + allow(secondary_store).to receive(:is_a?).with(::Redis::Namespace).and_return(true) + end + + it 'fails with exception' do + expect { described_class.new(primary_store, secondary_store, instance_name) }.not_to raise_error + end + end + context 'with READ redis commands' do let_it_be(:key1) { "redis:{1}:key_a" } let_it_be(:key2) { "redis:{1}:key_b" } @@ -145,7 +169,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'logs the ReadFromPrimaryError' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError), - hash_including(command_name: name, extra: hash_including(instance_name: instance_name)) + hash_including(command_name: name, instance_name: instance_name) ) subject @@ -222,8 +246,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'logs the exception' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), - hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name), - command_name: name)) + hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name)) subject end @@ -404,7 +427,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'logs the exception and execute on secondary instance', :aggregate_errors do expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), - hash_including(extra: hash_including(:multi_store_error_message), command_name: name)) + hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name)) expect(secondary_store).to receive(name).with(*expected_args).and_call_original subject @@ -525,7 +548,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'logs the exception and execute on secondary instance', :aggregate_errors do expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), - hash_including(extra: hash_including(:multi_store_error_message), command_name: name)) + hash_including(:multi_store_error_message, command_name: name)) expect(secondary_store).to receive(name).and_call_original subject @@ -563,7 +586,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'returns the value from the secondary store, logging an error' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( an_instance_of(Gitlab::Redis::MultiStore::PipelinedDiffError), - hash_including(command_name: name, extra: hash_including(instance_name: instance_name)) + hash_including(command_name: name, instance_name: instance_name) ).and_call_original expect(counter).to receive(:increment).with(command: name, instance_name: instance_name) @@ -579,7 +602,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'returns the value from the secondary store, logging an error' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( an_instance_of(Gitlab::Redis::MultiStore::PipelinedDiffError), - hash_including(command_name: name, extra: hash_including(instance_name: instance_name)) + hash_including(command_name: name, instance_name: instance_name) ) expect(counter).to receive(:increment).with(command: name, instance_name: instance_name) @@ -673,7 +696,7 @@ RSpec.describe Gitlab::Redis::MultiStore do it 'logs MethodMissingError' do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError), - hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)) + hash_including(command_name: :incr, instance_name: instance_name) ) subject diff --git a/spec/lib/gitlab/redis/sidekiq_status_spec.rb b/spec/lib/gitlab/redis/sidekiq_status_spec.rb new file mode 100644 index 00000000000..f641ea40efd --- /dev/null +++ b/spec/lib/gitlab/redis/sidekiq_status_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Redis::SidekiqStatus do + # Note: this is a pseudo-store in front of `SharedState`, meant only as a tool + # to move away from `Sidekiq.redis` for sidekiq status data. Thus, we use the + # same store configuration as the former. + let(:instance_specific_config_file) { "config/redis.shared_state.yml" } + let(:environment_config_file_name) { "GITLAB_REDIS_SHARED_STATE_CONFIG_FILE" } + + include_examples "redis_shared_examples" + + describe '#pool' do + let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } + let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } + + subject { described_class.pool } + + before do + redis_clear_raw_config!(Gitlab::Redis::SharedState) + redis_clear_raw_config!(Gitlab::Redis::Queues) + + allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host) + allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket) + end + + after do + redis_clear_raw_config!(Gitlab::Redis::SharedState) + redis_clear_raw_config!(Gitlab::Redis::Queues) + end + + around do |example| + clear_pool + example.run + ensure + clear_pool + end + + it 'instantiates an instance of MultiStore' do + subject.with do |redis_instance| + expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) + + expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") + expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0") + + expect(redis_instance.instance_name).to eq('SidekiqStatus') + end + end + + it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sidekiq_status, + :use_primary_store_as_default_for_sidekiq_status + end + + describe '#raw_config_hash' do + it 'has a legacy default URL' do + expect(subject).to receive(:fetch_config) { false } + + expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382') + end + end + + describe '#store_name' do + it 'returns the name of the SharedState store' do + expect(described_class.store_name).to eq('SharedState') + end + end +end diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index c94deb8e008..027697db7e1 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -3,138 +3,175 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqStatus, :clean_gitlab_redis_queues, :clean_gitlab_redis_shared_state do - describe '.set' do - it 'stores the job ID' do - described_class.set('123') + shared_examples 'tracking status in redis' do + describe '.set' do + it 'stores the job ID' do + described_class.set('123') + + key = described_class.key_for('123') + + with_redis do |redis| + expect(redis.exists(key)).to eq(true) + expect(redis.ttl(key) > 0).to eq(true) + expect(redis.get(key)).to eq('1') + end + end - key = described_class.key_for('123') + it 'allows overriding the expiration time' do + described_class.set('123', described_class::DEFAULT_EXPIRATION * 2) + + key = described_class.key_for('123') - Sidekiq.redis do |redis| - expect(redis.exists(key)).to eq(true) - expect(redis.ttl(key) > 0).to eq(true) - expect(redis.get(key)).to eq('1') + with_redis do |redis| + expect(redis.exists(key)).to eq(true) + expect(redis.ttl(key) > described_class::DEFAULT_EXPIRATION).to eq(true) + expect(redis.get(key)).to eq('1') + end end - end - it 'allows overriding the expiration time' do - described_class.set('123', described_class::DEFAULT_EXPIRATION * 2) + it 'does not store anything with a nil expiry' do + described_class.set('123', nil) - key = described_class.key_for('123') + key = described_class.key_for('123') - Sidekiq.redis do |redis| - expect(redis.exists(key)).to eq(true) - expect(redis.ttl(key) > described_class::DEFAULT_EXPIRATION).to eq(true) - expect(redis.get(key)).to eq('1') + with_redis do |redis| + expect(redis.exists(key)).to eq(false) + end end end - it 'does not store anything with a nil expiry' do - described_class.set('123', nil) + describe '.unset' do + it 'removes the job ID' do + described_class.set('123') + described_class.unset('123') - key = described_class.key_for('123') + key = described_class.key_for('123') - Sidekiq.redis do |redis| - expect(redis.exists(key)).to eq(false) + with_redis do |redis| + expect(redis.exists(key)).to eq(false) + end end end - end - describe '.unset' do - it 'removes the job ID' do - described_class.set('123') - described_class.unset('123') + describe '.all_completed?' do + it 'returns true if all jobs have been completed' do + expect(described_class.all_completed?(%w(123))).to eq(true) + end - key = described_class.key_for('123') + it 'returns false if a job has not yet been completed' do + described_class.set('123') - Sidekiq.redis do |redis| - expect(redis.exists(key)).to eq(false) + expect(described_class.all_completed?(%w(123 456))).to eq(false) end end - end - describe '.all_completed?' do - it 'returns true if all jobs have been completed' do - expect(described_class.all_completed?(%w(123))).to eq(true) - end + describe '.running?' do + it 'returns true if job is running' do + described_class.set('123') - it 'returns false if a job has not yet been completed' do - described_class.set('123') + expect(described_class.running?('123')).to be(true) + end - expect(described_class.all_completed?(%w(123 456))).to eq(false) + it 'returns false if job is not found' do + expect(described_class.running?('123')).to be(false) + end end - end - describe '.running?' do - it 'returns true if job is running' do - described_class.set('123') + describe '.num_running' do + it 'returns 0 if all jobs have been completed' do + expect(described_class.num_running(%w(123))).to eq(0) + end + + it 'returns 2 if two jobs are still running' do + described_class.set('123') + described_class.set('456') - expect(described_class.running?('123')).to be(true) + expect(described_class.num_running(%w(123 456 789))).to eq(2) + end end - it 'returns false if job is not found' do - expect(described_class.running?('123')).to be(false) + describe '.num_completed' do + it 'returns 1 if all jobs have been completed' do + expect(described_class.num_completed(%w(123))).to eq(1) + end + + it 'returns 1 if a job has not yet been completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.num_completed(%w(123 456 789))).to eq(1) + end end - end - describe '.num_running' do - it 'returns 0 if all jobs have been completed' do - expect(described_class.num_running(%w(123))).to eq(0) + describe '.completed_jids' do + it 'returns the completed job' do + expect(described_class.completed_jids(%w(123))).to eq(['123']) + end + + it 'returns only the jobs completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.completed_jids(%w(123 456 789))).to eq(['789']) + end end - it 'returns 2 if two jobs are still running' do - described_class.set('123') - described_class.set('456') + describe '.job_status' do + it 'returns an array of boolean values' do + described_class.set('123') + described_class.set('456') + described_class.unset('123') - expect(described_class.num_running(%w(123 456 789))).to eq(2) + expect(described_class.job_status(%w(123 456 789))).to eq([false, true, false]) + end + + it 'handles an empty array' do + expect(described_class.job_status([])).to eq([]) + end end end - describe '.num_completed' do - it 'returns 1 if all jobs have been completed' do - expect(described_class.num_completed(%w(123))).to eq(1) + context 'with multi-store feature flags turned on' do + def with_redis(&block) + Gitlab::Redis::SidekiqStatus.with(&block) end - it 'returns 1 if a job has not yet been completed' do - described_class.set('123') - described_class.set('456') + it 'uses Gitlab::Redis::SidekiqStatus.with' do + expect(Gitlab::Redis::SidekiqStatus).to receive(:with).and_call_original + expect(Sidekiq).not_to receive(:redis) - expect(described_class.num_completed(%w(123 456 789))).to eq(1) + described_class.job_status(%w(123 456 789)) end - end - describe '.key_for' do - it 'returns the key for a job ID' do - key = described_class.key_for('123') + it_behaves_like 'tracking status in redis' + end - expect(key).to be_an_instance_of(String) - expect(key).to include('123') + context 'when both multi-store feature flags are off' do + def with_redis(&block) + Sidekiq.redis(&block) end - end - describe '.completed_jids' do - it 'returns the completed job' do - expect(described_class.completed_jids(%w(123))).to eq(['123']) + before do + stub_feature_flags(use_primary_and_secondary_stores_for_sidekiq_status: false) + stub_feature_flags(use_primary_store_as_default_for_sidekiq_status: false) end - it 'returns only the jobs completed' do - described_class.set('123') - described_class.set('456') + it 'uses Sidekiq.redis' do + expect(Sidekiq).to receive(:redis).and_call_original + expect(Gitlab::Redis::SidekiqStatus).not_to receive(:with) - expect(described_class.completed_jids(%w(123 456 789))).to eq(['789']) + described_class.job_status(%w(123 456 789)) end - end - describe '.job_status' do - it 'returns an array of boolean values' do - described_class.set('123') - described_class.set('456') - described_class.unset('123') + it_behaves_like 'tracking status in redis' + end - expect(described_class.job_status(%w(123 456 789))).to eq([false, true, false]) - end + describe '.key_for' do + it 'returns the key for a job ID' do + key = described_class.key_for('123') - it 'handles an empty array' do - expect(described_class.job_status([])).to eq([]) + expect(key).to be_an_instance_of(String) + expect(key).to include('123') end end end diff --git a/spec/lib/gitlab/updated_notes_paginator_spec.rb b/spec/lib/gitlab/updated_notes_paginator_spec.rb deleted file mode 100644 index ce6a7719fb4..00000000000 --- a/spec/lib/gitlab/updated_notes_paginator_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::UpdatedNotesPaginator do - let(:issue) { create(:issue) } - - let(:project) { issue.project } - let(:finder) { NotesFinder.new(user, target: issue, last_fetched_at: last_fetched_at) } - let(:user) { issue.author } - - let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 2.days.ago) } - let!(:page_2) { [create(:note, noteable: issue, project: project, updated_at: 1.day.ago)] } - - let(:page_1_boundary) { page_1.last.updated_at + NotesFinder::FETCH_OVERLAP } - - around do |example| - freeze_time do - example.run - end - end - - before do - stub_const("Gitlab::UpdatedNotesPaginator::LIMIT", 2) - end - - subject(:paginator) { described_class.new(finder.execute, last_fetched_at: last_fetched_at) } - - describe 'last_fetched_at: start of time' do - let(:last_fetched_at) { Time.at(0) } - - it 'calculates the first page of notes', :aggregate_failures do - expect(paginator.notes).to match_array(page_1) - expect(paginator.metadata).to match( - more: true, - last_fetched_at: microseconds(page_1_boundary) - ) - end - end - - describe 'last_fetched_at: start of final page' do - let(:last_fetched_at) { page_1_boundary } - - it 'calculates a final page', :aggregate_failures do - expect(paginator.notes).to match_array(page_2) - expect(paginator.metadata).to match( - more: false, - last_fetched_at: microseconds(Time.zone.now) - ) - end - end - - # Convert a time to an integer number of microseconds - def microseconds(time) - (time.to_i * 1_000_000) + time.usec - end -end diff --git a/spec/models/clusters/integrations/prometheus_spec.rb b/spec/models/clusters/integrations/prometheus_spec.rb index e529c751889..d1e40fffee0 100644 --- a/spec/models/clusters/integrations/prometheus_spec.rb +++ b/spec/models/clusters/integrations/prometheus_spec.rb @@ -21,11 +21,24 @@ RSpec.describe Clusters::Integrations::Prometheus do let(:cluster) { create(:cluster, :with_installed_helm) } it 'deactivates prometheus_integration' do - expect(Clusters::Applications::DeactivateServiceWorker) + expect(Clusters::Applications::DeactivateIntegrationWorker) .to receive(:perform_async).with(cluster.id, 'prometheus') integration.destroy! end + + context 'when the FF :rename_integrations_workers is disabled' do + before do + stub_feature_flags(rename_integrations_workers: false) + end + + it 'uses the old worker' do + expect(Clusters::Applications::DeactivateServiceWorker) + .to receive(:perform_async).with(cluster.id, 'prometheus') + + integration.destroy! + end + end end describe 'after_save' do @@ -38,10 +51,10 @@ RSpec.describe Clusters::Integrations::Prometheus do it 'does not touch project integrations' do integration # ensure integration exists before we set the expectations - expect(Clusters::Applications::DeactivateServiceWorker) + expect(Clusters::Applications::DeactivateIntegrationWorker) .not_to receive(:perform_async) - expect(Clusters::Applications::ActivateServiceWorker) + expect(Clusters::Applications::ActivateIntegrationWorker) .not_to receive(:perform_async) integration.update!(enabled: enabled) @@ -51,19 +64,32 @@ RSpec.describe Clusters::Integrations::Prometheus do context 'when enabling' do let(:enabled) { false } - it 'deactivates prometheus_integration' do - expect(Clusters::Applications::ActivateServiceWorker) + it 'activates prometheus_integration' do + expect(Clusters::Applications::ActivateIntegrationWorker) .to receive(:perform_async).with(cluster.id, 'prometheus') integration.update!(enabled: true) end + + context 'when the FF :rename_integrations_workers is disabled' do + before do + stub_feature_flags(rename_integrations_workers: false) + end + + it 'uses the old worker' do + expect(Clusters::Applications::ActivateServiceWorker) + .to receive(:perform_async).with(cluster.id, 'prometheus') + + integration.update!(enabled: true) + end + end end context 'when disabling' do let(:enabled) { true } it 'activates prometheus_integration' do - expect(Clusters::Applications::DeactivateServiceWorker) + expect(Clusters::Applications::DeactivateIntegrationWorker) .to receive(:perform_async).with(cluster.id, 'prometheus') integration.update!(enabled: false) diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 28040dd0365..a58d32dfe5d 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -27,41 +27,21 @@ RSpec.describe Deployment do describe '#manual_actions' do let(:deployment) { create(:deployment) } - it 'delegates to environment_manual_actions when deployment_environment_manual_actions ff is enabled' do - stub_feature_flags(deployment_environment_manual_actions: true) - + it 'delegates to environment_manual_actions' do expect(deployment.deployable).to receive(:environment_manual_actions).and_call_original deployment.manual_actions end - - it 'delegates to other_manual_actions when deployment_environment_manual_actions ff is disabled' do - stub_feature_flags(deployment_environment_manual_actions: false) - - expect(deployment.deployable).to receive(:other_manual_actions).and_call_original - - deployment.manual_actions - end end describe '#scheduled_actions' do let(:deployment) { create(:deployment) } - it 'delegates to environment_scheduled_actions when deployment_environment_manual_actions ff is enabled' do - stub_feature_flags(deployment_environment_manual_actions: true) - + it 'delegates to environment_scheduled_actions' do expect(deployment.deployable).to receive(:environment_scheduled_actions).and_call_original deployment.scheduled_actions end - - it 'delegates to other_scheduled_actions when deployment_environment_manual_actions ff is disabled' do - stub_feature_flags(deployment_environment_manual_actions: false) - - expect(deployment.deployable).to receive(:other_scheduled_actions).and_call_original - - deployment.scheduled_actions - end end describe 'modules' do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index d665b92d80f..97c76c58560 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -455,7 +455,7 @@ RSpec.describe Issue do end end - describe '#related_issues' do + describe '#related_issues to relate incidents and issues' do let_it_be(:authorized_project) { create(:project) } let_it_be(:authorized_project2) { create(:project) } let_it_be(:unauthorized_project) { create(:project) } @@ -463,12 +463,14 @@ RSpec.describe Issue do let_it_be(:authorized_issue_a) { create(:issue, project: authorized_project) } let_it_be(:authorized_issue_b) { create(:issue, project: authorized_project) } let_it_be(:authorized_issue_c) { create(:issue, project: authorized_project2) } + let_it_be(:authorized_incident_a) { create(:incident, project: authorized_project )} let_it_be(:unauthorized_issue) { create(:issue, project: unauthorized_project) } let_it_be(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) } let_it_be(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) } let_it_be(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) } + let_it_be(:issue_incident_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_incident_a) } before_all do authorized_project.add_developer(user) @@ -477,7 +479,7 @@ RSpec.describe Issue do it 'returns only authorized related issues for given user' do expect(authorized_issue_a.related_issues(user)) - .to contain_exactly(authorized_issue_b, authorized_issue_c) + .to contain_exactly(authorized_issue_b, authorized_issue_c, authorized_incident_a) end it 'returns issues with valid issue_link_type' do @@ -507,7 +509,7 @@ RSpec.describe Issue do expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false) expect(authorized_issue_a.related_issues(user)) - .to contain_exactly(authorized_issue_b) + .to contain_exactly(authorized_issue_b, authorized_incident_a) end end end @@ -1581,4 +1583,28 @@ RSpec.describe Issue do expire_cache end end + + describe '#link_reference_pattern' do + let(:match_data) { described_class.link_reference_pattern.match(link_reference_url) } + + context 'with issue url' do + let(:link_reference_url) { 'http://localhost/namespace/project/-/issues/1' } + + it 'matches with expected attributes' do + expect(match_data['namespace']).to eq('namespace') + expect(match_data['project']).to eq('project') + expect(match_data['issue']).to eq('1') + end + end + + context 'with incident url' do + let(:link_reference_url) { 'http://localhost/namespace1/project1/-/issues/incident/2' } + + it 'matches with expected attributes' do + expect(match_data['namespace']).to eq('namespace1') + expect(match_data['project']).to eq('project1') + expect(match_data['issue']).to eq('2') + end + end + end end diff --git a/spec/presenters/releases/link_presenter_spec.rb b/spec/presenters/releases/link_presenter_spec.rb new file mode 100644 index 00000000000..e52c68ffb38 --- /dev/null +++ b/spec/presenters/releases/link_presenter_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Releases::LinkPresenter do + describe '#direct_asset_url' do + let_it_be(:release) { create(:release) } + + let(:link) { build(:release_link, release: release, url: url, filepath: filepath) } + let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' } + let(:presenter) { described_class.new(link) } + + subject { presenter.direct_asset_url } + + context 'when filepath is provided' do + let(:filepath) { '/bin/bigfile.exe' } + let(:expected_url) do + "http://localhost/#{release.project.namespace.path}/#{release.project.name}" \ + "/-/releases/#{release.tag}/downloads/bin/bigfile.exe" + end + + it { is_expected.to eq(expected_url) } + end + + context 'when filepath is not provided' do + let(:filepath) { nil } + + it { is_expected.to eq(url) } + end + end +end diff --git a/spec/serializers/service_event_entity_spec.rb b/spec/serializers/integrations/event_entity_spec.rb index db82e84fcf8..07281248f5b 100644 --- a/spec/serializers/service_event_entity_spec.rb +++ b/spec/serializers/integrations/event_entity_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' -RSpec.describe ServiceEventEntity do - let(:request) { double('request') } +RSpec.describe Integrations::EventEntity do + let(:request) { EntityRequest.new(integration: integration) } - subject { described_class.new(event, request: request, service: integration).as_json } + subject { described_class.new(event, request: request, integration: integration).as_json } before do - allow(request).to receive(:service).and_return(integration) + allow(request).to receive(:integration).and_return(integration) end describe '#as_json' do - context 'integration without fields' do + context 'with integration without fields' do let(:integration) { create(:emails_on_push_integration, push_events: true) } let(:event) { 'push' } @@ -24,7 +24,7 @@ RSpec.describe ServiceEventEntity do end end - context 'integration with fields' do + context 'with integration with fields' do let(:integration) { create(:integrations_slack, note_events: false, note_channel: 'note-channel') } let(:event) { 'note' } diff --git a/spec/serializers/service_field_entity_spec.rb b/spec/serializers/integrations/field_entity_spec.rb index 3a574c522b0..e75dc051f5e 100644 --- a/spec/serializers/service_field_entity_spec.rb +++ b/spec/serializers/integrations/field_entity_spec.rb @@ -2,20 +2,20 @@ require 'spec_helper' -RSpec.describe ServiceFieldEntity do - let(:request) { double('request') } +RSpec.describe Integrations::FieldEntity do + let(:request) { EntityRequest.new(integration: integration) } - subject { described_class.new(field, request: request, service: integration).as_json } + subject { described_class.new(field, request: request, integration: integration).as_json } before do - allow(request).to receive(:service).and_return(integration) + allow(request).to receive(:integration).and_return(integration) end describe '#as_json' do - context 'Jira Service' do + context 'with Jira integration' do let(:integration) { create(:jira_integration) } - context 'field with type text' do + context 'with field with type text' do let(:field) { integration_field('username') } it 'exposes correct attributes' do @@ -36,7 +36,7 @@ RSpec.describe ServiceFieldEntity do end end - context 'field with type password' do + context 'with field with type password' do let(:field) { integration_field('password') } it 'exposes correct attributes but hides password' do @@ -58,10 +58,10 @@ RSpec.describe ServiceFieldEntity do end end - context 'EmailsOnPush Service' do + context 'with EmailsOnPush integration' do let(:integration) { create(:emails_on_push_integration, send_from_committer_email: '1') } - context 'field with type checkbox' do + context 'with field with type checkbox' do let(:field) { integration_field('send_from_committer_email') } it 'exposes correct attributes and casts value to Boolean' do @@ -78,11 +78,14 @@ RSpec.describe ServiceFieldEntity do } is_expected.to include(expected_hash) - expect(subject[:help]).to include("Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance") + expect(subject[:help]).to include( + "Send notifications from the committer's email address if the domain " \ + "matches the domain used by your GitLab instance" + ) end end - context 'field with type select' do + context 'with field with type select' do let(:field) { integration_field('branches_to_be_notified') } it 'exposes correct attributes' do @@ -93,7 +96,12 @@ RSpec.describe ServiceFieldEntity do title: 'Branches for which notifications are to be sent', placeholder: nil, required: nil, - choices: [['All branches', 'all'], ['Default branch', 'default'], ['Protected branches', 'protected'], ['Default branch and protected branches', 'default_and_protected']], + choices: [ + ['All branches', 'all'], + ['Default branch', 'default'], + ['Protected branches', 'protected'], + ['Default branch and protected branches', 'default_and_protected'] + ], help: nil, value: nil, checkbox_label: nil diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index e767990d351..bfcaf9552b3 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -1,6 +1,68 @@ # frozen_string_literal: true module ExceedQueryLimitHelpers + class QueryDiff + def initialize(expected, actual, show_common_queries) + @expected = expected + @actual = actual + @show_common_queries = show_common_queries + end + + def diff + return combined_counts if @show_common_queries + + combined_counts + .transform_values { select_suffixes_with_diffs(_1) } + .reject { |_prefix, suffs| suffs.empty? } + end + + private + + def select_suffixes_with_diffs(suffs) + reject_groups_with_different_parameters(reject_suffixes_with_identical_counts(suffs)) + end + + def reject_suffixes_with_identical_counts(suffs) + suffs.reject { |_k, counts| counts.first == counts.second } + end + + # Eliminates groups that differ only in parameters, + # to make it easier to debug the output. + # + # For example, if we have a group `SELECT * FROM users...`, + # with the following suffixes + # `WHERE id = 1` (counts: N, 0) + # `WHERE id = 2` (counts: 0, N) + def reject_groups_with_different_parameters(suffs) + return suffs if suffs.size != 2 + + counts_a, counts_b = suffs.values + return {} if counts_a == counts_b.reverse && counts_a.include?(0) + + suffs + end + + def expected_counts + @expected.transform_values do |suffixes| + suffixes.transform_values { |n| [n, 0] } + end + end + + def recorded_counts + @actual.transform_values do |suffixes| + suffixes.transform_values { |n| [0, n] } + end + end + + def combined_counts + expected_counts.merge(recorded_counts) do |_k, exp, got| + exp.merge(got) do |_k, exp_counts, got_counts| + exp_counts.zip(got_counts).map { |a, b| a + b } + end + end + end + end + MARGINALIA_ANNOTATION_REGEX = %r{\s*\/\*.*\*\/}.freeze DB_QUERY_RE = Regexp.union([ @@ -108,40 +170,7 @@ module ExceedQueryLimitHelpers end def diff_query_counts(expected, actual) - expected_counts = expected.transform_values do |suffixes| - suffixes.transform_values { |n| [n, 0] } - end - recorded_counts = actual.transform_values do |suffixes| - suffixes.transform_values { |n| [0, n] } - end - - combined_counts = expected_counts.merge(recorded_counts) do |_k, exp, got| - exp.merge(got) do |_k, exp_counts, got_counts| - exp_counts.zip(got_counts).map { |a, b| a + b } - end - end - - reject_groups_with_matching_counts(combined_counts) - end - - def reject_groups_with_matching_counts(combined_counts) - return combined_counts if @show_common_queries - - combined_counts - .transform_values { select_suffixes_with_diffs(_1) } - .reject { |_prefix, suffs| suffs.empty? } - end - - def select_suffixes_with_diffs(suffs) - # reject when count in LHS is the same as count in RHS - suffs = suffs.reject { |_k, counts| counts.first == counts.second } - - # Reject common case of N queries on LHS and N on right, but with different parameters - # accepts as equivalent if a == [0, 1] and b == [1, 0], for example - keys = suffs.keys - return {} if keys.size == 2 && suffs[keys.first] == suffs[keys.second].reverse - - suffs + QueryDiff.new(expected, actual, @show_common_queries).diff end def diff_query_group_message(query, suffixes) diff --git a/spec/workers/clusters/applications/activate_service_worker_spec.rb b/spec/workers/clusters/applications/activate_integration_worker_spec.rb index d13ff76613c..ecb49be5a4b 100644 --- a/spec/workers/clusters/applications/activate_service_worker_spec.rb +++ b/spec/workers/clusters/applications/activate_integration_worker_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do - context 'cluster exists' do +RSpec.describe Clusters::Applications::ActivateIntegrationWorker, '#perform' do + context 'when cluster exists' do describe 'prometheus integration' do let(:integration_name) { 'prometheus' } @@ -11,7 +11,7 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do create(:clusters_integrations_prometheus, cluster: cluster) end - context 'cluster type: group' do + context 'with cluster type: group' do let(:group) { create(:group) } let(:project) { create(:project, group: group) } let(:cluster) { create(:cluster_for_group, groups: [group]) } @@ -22,7 +22,7 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do end end - context 'cluster type: project' do + context 'with cluster type: project' do let(:project) { create(:project) } let(:cluster) { create(:cluster, projects: [project]) } @@ -32,7 +32,7 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do end end - context 'cluster type: instance' do + context 'with cluster type: instance' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :instance) } @@ -40,11 +40,20 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do expect { described_class.new.perform(cluster.id, integration_name) } .to change { project.reload.prometheus_integration&.active }.from(nil).to(true) end + + context 'when using the old worker class' do + let(:described_class) { Clusters::Applications::ActivateServiceWorker } + + it 'ensures Prometheus integration is activated' do + expect { described_class.new.perform(cluster.id, integration_name) } + .to change { project.reload.prometheus_integration&.active }.from(nil).to(true) + end + end end end end - context 'cluster does not exist' do + context 'when cluster does not exist' do it 'does not raise Record Not Found error' do expect { described_class.new.perform(0, 'ignored in this context') }.not_to raise_error end diff --git a/spec/workers/clusters/applications/deactivate_service_worker_spec.rb b/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb index 77788cfa893..3f0188eee23 100644 --- a/spec/workers/clusters/applications/deactivate_service_worker_spec.rb +++ b/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb @@ -2,20 +2,22 @@ require 'spec_helper' -RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do - context 'cluster exists' do +RSpec.describe Clusters::Applications::DeactivateIntegrationWorker, '#perform' do + context 'when cluster exists' do describe 'prometheus integration' do let(:integration_name) { 'prometheus' } let!(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) } - context 'prometheus integration exists' do - let!(:prometheus_integration) { create(:prometheus_integration, project: project, manual_configuration: false, active: true) } + context 'when prometheus integration exists' do + let!(:prometheus_integration) do + create(:prometheus_integration, project: project, manual_configuration: false, active: true) + end before do integration.delete # prometheus integration before save synchronises active stated with integration existence. end - context 'cluster type: group' do + context 'with cluster type: group' do let(:group) { create(:group) } let(:project) { create(:project, group: group) } let(:cluster) { create(:cluster_for_group, groups: [group]) } @@ -26,7 +28,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do end end - context 'cluster type: project' do + context 'with cluster type: project' do let(:project) { create(:project) } let(:cluster) { create(:cluster, projects: [project]) } @@ -36,7 +38,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do end end - context 'cluster type: instance' do + context 'with cluster type: instance' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :instance) } @@ -44,11 +46,20 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do expect { described_class.new.perform(cluster.id, integration_name) } .to change { prometheus_integration.reload.active }.from(true).to(false) end + + context 'when using the old worker class' do + let(:described_class) { Clusters::Applications::ActivateServiceWorker } + + it 'ensures Prometheus integration is deactivated' do + expect { described_class.new.perform(cluster.id, integration_name) } + .to change { prometheus_integration.reload.active }.from(true).to(false) + end + end end end - context 'prometheus integration does not exist' do - context 'cluster type: project' do + context 'when prometheus integration does not exist' do + context 'with cluster type: project' do let(:project) { create(:project) } let(:cluster) { create(:cluster, projects: [project]) } @@ -60,7 +71,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do end end - context 'cluster does not exist' do + context 'when cluster does not exist' do it 'raises Record Not Found error' do expect { described_class.new.perform(0, 'ignored in this context') }.to raise_error(ActiveRecord::RecordNotFound) end diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb index 0191a2898b2..d1dd1cd738b 100644 --- a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb +++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do subject { described_class.new.perform(app_name, app_id) } - context 'app exists' do + context 'when app exists' do let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) } it 'calls the check service' do @@ -20,7 +20,7 @@ RSpec.describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do end end - context 'app does not exist' do + context 'when app does not exist' do let(:app_id) { 0 } it 'does not call the check service' do diff --git a/spec/workers/concerns/limited_capacity/job_tracker_spec.rb b/spec/workers/concerns/limited_capacity/job_tracker_spec.rb index f141a1ad7ad..eeccdbd0e2d 100644 --- a/spec/workers/concerns/limited_capacity/job_tracker_spec.rb +++ b/spec/workers/concerns/limited_capacity/job_tracker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe LimitedCapacity::JobTracker, :clean_gitlab_redis_queues do +RSpec.describe LimitedCapacity::JobTracker, :clean_gitlab_redis_shared_state do let(:job_tracker) do described_class.new('namespace') end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index fb8ff23f8d8..eaf75cccb3e 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -180,7 +180,9 @@ RSpec.describe 'Every Sidekiq worker' do 'ClusterWaitForAppInstallationWorker' => 3, 'ClusterWaitForAppUpdateWorker' => 3, 'ClusterWaitForIngressIpAddressWorker' => 3, + 'Clusters::Applications::ActivateIntegrationWorker' => 3, 'Clusters::Applications::ActivateServiceWorker' => 3, + 'Clusters::Applications::DeactivateIntegrationWorker' => 3, 'Clusters::Applications::DeactivateServiceWorker' => 3, 'Clusters::Applications::UninstallWorker' => 3, 'Clusters::Applications::WaitForUninstallAppWorker' => 3, |