diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 18:10:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 18:10:08 +0300 |
commit | 78a5f872de316860ccd7a983c10805bf6c6b771c (patch) | |
tree | 29c394a4114d012cf9dcef37037e1992ef15105d /spec | |
parent | 14c3ebc6364f7d5eb31cbf2e66a79ec574e88b70 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
10 files changed, 402 insertions, 33 deletions
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js index b19095cc686..48c01e3efad 100644 --- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js @@ -162,4 +162,26 @@ describe('Merge request merge checks component', () => { expect(wrapper.findByTestId('merge-checks-full').exists()).toBe(true); }); + + it('sorts merge checks', async () => { + mountComponent({ + mergeabilityChecks: [ + { identifier: 'discussions', status: 'SUCCESS' }, + { identifier: 'discussions', status: 'INACTIVE' }, + { identifier: 'rebase', status: 'FAILED' }, + ], + }); + + await waitForPromises(); + + await wrapper.findByTestId('widget-toggle').trigger('click'); + + const mergeChecks = wrapper.findAllByTestId('merge-check'); + + expect(mergeChecks.length).toBe(2); + expect(mergeChecks.at(0).props('check')).toEqual(expect.objectContaining({ status: 'FAILED' })); + expect(mergeChecks.at(1).props('check')).toEqual( + expect.objectContaining({ status: 'SUCCESS' }), + ); + }); }); diff --git a/spec/graphql/types/work_items/widgets/participants_type_spec.rb b/spec/graphql/types/work_items/widgets/participants_type_spec.rb new file mode 100644 index 00000000000..68d201cc52b --- /dev/null +++ b/spec/graphql/types/work_items/widgets/participants_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::Widgets::ParticipantsType, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[participants type] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb index 58a44fec3aa..57cdf0da516 100644 --- a/spec/models/concerns/participable_spec.rb +++ b/spec/models/concerns/participable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Participable do +RSpec.describe Participable, feature_category: :team_planning do let(:model) do Class.new do include Participable @@ -31,7 +31,7 @@ RSpec.describe Participable do expect(instance).to receive(:foo).and_return(user2) expect(instance).to receive(:bar).and_return(user3) - expect(instance).to receive(:project).thrice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) participants = instance.participants(user1) @@ -66,7 +66,7 @@ RSpec.describe Participable do expect(instance).to receive(:foo).and_return(other) expect(other).to receive(:bar).and_return(user2) - expect(instance).to receive(:project).thrice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) expect(instance.participants(user1)).to eq([user2]) end @@ -86,7 +86,7 @@ RSpec.describe Participable do instance = model.new - expect(instance).to receive(:project).thrice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) instance.participants(user1) @@ -115,6 +115,46 @@ RSpec.describe Participable do expect(participants).to contain_exactly(user1) end end + + context 'participable is a group level object' do + it 'returns the list of participants' do + model.participant(:foo) + model.participant(:bar) + + user1 = build(:user) + user2 = build(:user) + user3 = build(:user) + group = build(:group, :public) + instance = model.new + + expect(instance).to receive(:foo).and_return(user2) + expect(instance).to receive(:bar).and_return(user3) + expect(instance).to receive(:project).exactly(3).and_return(nil) + expect(instance).to receive(:namespace).exactly(2).and_return(group) + + participants = instance.participants(user1) + + expect(participants).not_to include(user1) + expect(participants).to include(user2) + expect(participants).to include(user3) + end + end + + context 'participable is neither a project nor a group level object' do + it 'returns no participants' do + model.participant(:foo) + + user = build(:user) + instance = model.new + + expect(instance).to receive(:foo).and_return(user) + expect(instance).to receive(:project).exactly(3).and_return(nil) + + participants = instance.participants(user) + + expect(participants).to be_empty + end + end end describe '#visible_participants' do @@ -138,7 +178,7 @@ RSpec.describe Participable do allow(instance).to receive_message_chain(:model_name, :element) { 'class' } expect(instance).to receive(:foo).and_return(user2) expect(instance).to receive(:bar).and_return(user3) - expect(instance).to receive(:project).thrice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) participants = instance.visible_participants(user1) @@ -158,17 +198,68 @@ RSpec.describe Participable do allow(instance).to receive_message_chain(:model_name, :element) { 'class' } allow(instance).to receive(:bar).and_return(user2) - expect(instance).to receive(:project).thrice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) expect(instance.visible_participants(user1)).to be_empty end end + context 'when participable is a group level object' do + let(:group) { create(:group, :private) } + + it 'returns the list of participants' do + model.participant(:foo) + model.participant(:bar) + + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + instance = model.new + + group.add_reporter(user1) + group.add_reporter(user3) + + allow(instance).to receive_message_chain(:model_name, :element) { 'class' } + expect(instance).to receive(:foo).and_return(user2) + expect(instance).to receive(:bar).and_return(user3) + expect(instance).to receive(:project).exactly(3).and_return(nil) + expect(instance).to receive(:namespace).exactly(2).and_return(group) + + participants = instance.visible_participants(user1) + + expect(participants).not_to include(user1) # not returned by participant attr + expect(participants).not_to include(user2) # not a member of group + expect(participants).to include(user3) # member of group + end + end + + context 'when participable is neither project nor group level object' do + let(:group) { create(:group, :private) } + + it 'returns no participants' do + model.participant(:foo) + + user = create(:user) + instance = model.new + + group.add_reporter(user) + + allow(instance).to receive_message_chain(:model_name, :element) { 'class' } + expect(instance).to receive(:foo).and_return(user) + expect(instance).to receive(:project).exactly(3).and_return(nil) + + # user is returned by participant attr and is a member of the group, + # but participable model is neither a group or project object + participants = instance.visible_participants(user) + expect(participants).to be_empty + end + end + context 'with multiple system notes from the same author and mentioned_users' do let!(:user1) { create(:user) } let!(:user2) { create(:user) } - it 'skips expensive checks if the author is aleady in participants list' do + it 'skips expensive checks if the author is already in participants list' do model.participant(:notes) instance = model.new @@ -215,7 +306,7 @@ RSpec.describe Participable do it 'caches the list of raw participants' do expect(instance).to receive(:raw_participants).once.and_return([]) - expect(instance).to receive(:project).twice.and_return(project) + expect(instance).to receive(:project).exactly(4).and_return(project) instance.participant?(user1) instance.participant?(user1) @@ -234,5 +325,41 @@ RSpec.describe Participable do expect(instance.participant?(user3)).to be false end end + + context 'when participable is a group level object' do + let(:group) { create(:group, :private) } + + before do + # we need users to be created to add them as members to the group + user1.save! + user2.save! + user3.save! + + group.add_reporter(user1) + group.add_reporter(user2) + end + + it 'returns whether the user is a participant' do + allow(instance).to receive(:foo).and_return(user1) + allow(instance).to receive(:bar).and_return(user3) + allow(instance).to receive(:project).and_return(nil) + allow(instance).to receive(:namespace).and_return(group) + + expect(instance.participant?(user1)).to be true # returned by participant attr and a member of group + expect(instance.participant?(user2)).to be false # returned by participant attr + expect(instance.participant?(user3)).to be false # not a member of group + end + + context 'when participable is neither project nor group level object' do + it 'returns whether the user is a participant' do + allow(instance).to receive(:foo).and_return(user1) + allow(instance).to receive(:project).and_return(nil) + + # user1 is returned by participant attr and is a member of group, + # but participable model is neither a group or project object + expect(instance.participant?(user1)).to be false + end + end + end end end diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 843e3d40dc2..eeb1e3699d8 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -6,6 +6,7 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do using RSpec::Parameterized::TableSyntax let_it_be(:reusable_project) { create(:project) } + let_it_be(:reusable_group) { create(:group) } describe 'associations' do it { is_expected.to belong_to(:namespace) } @@ -725,4 +726,22 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do expect(item4.linked_items_count).to eq(0) end end + + context 'work item participants' do + context 'project level work item' do + let_it_be(:work_item) { create(:work_item, project: reusable_project) } + + it 'has participants' do + expect(work_item.participants).to match_array([work_item.author]) + end + end + + context 'group level work item' do + let_it_be(:work_item) { create(:work_item, namespace: reusable_group) } + + it 'has participants' do + expect(work_item.participants).to match_array([work_item.author]) + end + end + end end diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb index 1540ee57ff4..f1f498cef88 100644 --- a/spec/models/work_items/widget_definition_spec.rb +++ b/spec/models/work_items/widget_definition_spec.rb @@ -15,7 +15,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do ::WorkItems::Widgets::Notifications, ::WorkItems::Widgets::CurrentUserTodos, ::WorkItems::Widgets::AwardEmoji, - ::WorkItems::Widgets::LinkedItems + ::WorkItems::Widgets::LinkedItems, + ::WorkItems::Widgets::Participants ] if Gitlab.ee? diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index d0f80bcfebe..982696593bb 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -279,14 +279,14 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team context 'when fetching work item notifications widget' do let(:fields) do <<~GRAPHQL - nodes { - widgets { - type - ... on WorkItemWidgetNotifications { - subscribed - } + nodes { + widgets { + type + ... on WorkItemWidgetNotifications { + subscribed } } + } GRAPHQL end @@ -307,22 +307,22 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team context 'when fetching work item award emoji widget' do let(:fields) do <<~GRAPHQL - nodes { - widgets { - type - ... on WorkItemWidgetAwardEmoji { - awardEmoji { - nodes { - name - emoji - user { id } - } + nodes { + widgets { + type + ... on WorkItemWidgetAwardEmoji { + awardEmoji { + nodes { + name + emoji + user { id } } - upvotes - downvotes } + upvotes + downvotes } } + } GRAPHQL end @@ -407,6 +407,54 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end + context 'when fetching work item participants widget' do + let_it_be(:other_project) { create(:project, group: group) } + let_it_be(:project) { other_project } + let_it_be(:users) { create_list(:user, 3) } + let_it_be(:work_items) { create_list(:work_item, 3, project: project, assignees: users) } + + let(:fields) do + <<~GRAPHQL + nodes { + id + widgets { + type + ... on WorkItemWidgetParticipants { + participants { + nodes { + id + username + } + } + } + } + } + GRAPHQL + end + + before do + project.add_guest(current_user) + end + + it 'returns participants' do + post_graphql(query, current_user: current_user) + + participants_usernames = graphql_dig_at(items_data, 'widgets', 'participants', 'nodes', 'username') + expect(participants_usernames).to match_array(work_items.flat_map(&:participants).map(&:username)) + end + + it 'executes limited number of N+1 queries', :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create_list(:work_item, 2, project: project, assignees: users) + + expect_graphql_errors_to_be_empty + expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control) + end + end + def item_ids graphql_dig_at(items_data, :id) end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index c6d44b057a7..e5d8131fc7e 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -650,7 +650,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do let(:first_param) { 1 } let(:all_records) { [link1, link2] } - let(:data_path) { ['workItem', 'widgets', "linkedItems", -1] } + let(:data_path) { ['workItem', 'widgets', 'linkedItems', -2] } def widget_fields(args) query_graphql_field( diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb index ef8cd0a861e..a048c21d39a 100644 --- a/spec/services/merge_requests/retarget_chain_service_spec.rb +++ b/spec/services/merge_requests/retarget_chain_service_spec.rb @@ -41,9 +41,13 @@ RSpec.describe MergeRequests::RetargetChainService, feature_category: :code_revi 'target', 'delete', merge_request.source_branch, merge_request.target_branch) + expect(another_merge_request.rebase_jid).to be_blank + expect { subject }.to change { another_merge_request.reload.target_branch } .from(merge_request.source_branch) .to(merge_request.target_branch) + + expect(another_merge_request.rebase_jid).to be_present end end @@ -132,9 +136,17 @@ RSpec.describe MergeRequests::RetargetChainService, feature_category: :code_revi merge_request.mark_as_merged end - it 'retargets only 4 of them' do + it 'retargets and rebases only 4 of them' do + expect(many_merge_requests.any? { |mr| mr.reload.rebase_jid.present? }).to be_falsey + subject + first_four = many_merge_requests.first(4) + others = many_merge_requests - first_four + + expect(others.any? { |mr| mr.reload.rebase_jid.present? }).to be_falsey + expect(first_four.all? { |mr| mr.reload.rebase_jid.present? }).to be_truthy + expect(many_merge_requests.each(&:reload).pluck(:target_branch).tally) .to eq( merge_request.source_branch => 6, diff --git a/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb index d4fa35b9b82..4d7e0e138e9 100644 --- a/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb +++ b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb @@ -73,10 +73,10 @@ RSpec.describe ClickHouse::EventAuthorsConsistencyCronWorker, feature_category: User.where(id: [user1.id, user2.id]).delete_all stub_const("#{described_class}::MAX_AUTHOR_DELETIONS", 2) - stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1) + stub_const("#{ClickHouse::Concerns::ConsistencyWorker}::POSTGRESQL_BATCH_SIZE", 1) expect(worker).to receive(:log_extra_metadata_on_done).with(:result, - { status: :deletion_limit_reached, deletions: 2 }) + { status: :limit_reached, modifications: 2 }) worker.perform @@ -87,13 +87,13 @@ RSpec.describe ClickHouse::EventAuthorsConsistencyCronWorker, feature_category: context 'when time limit is reached' do it 'stops the processing earlier' do - stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1) + stub_const("#{ClickHouse::Concerns::ConsistencyWorker}::POSTGRESQL_BATCH_SIZE", 1) # stop at the third author_id allow_next_instance_of(Analytics::CycleAnalytics::RuntimeLimiter) do |runtime_limiter| allow(runtime_limiter).to receive(:over_time?).and_return(false, false, true) end - expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :over_time, deletions: 1 }) + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :over_time, modifications: 1 }) worker.perform diff --git a/spec/workers/click_house/event_paths_consistency_cron_worker_spec.rb b/spec/workers/click_house/event_paths_consistency_cron_worker_spec.rb new file mode 100644 index 00000000000..76ce63ed2e4 --- /dev/null +++ b/spec/workers/click_house/event_paths_consistency_cron_worker_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ClickHouse::EventPathsConsistencyCronWorker, feature_category: :value_stream_management do + let(:worker) { described_class.new } + + context 'when ClickHouse is disabled' do + it 'does nothing' do + allow(ClickHouse::Client).to receive(:database_configured?).and_return(false) + + expect(worker).not_to receive(:log_extra_metadata_on_done) + + worker.perform + end + end + + context 'when the event_sync_worker_for_click_house feature flag is off' do + before do + stub_feature_flags(event_sync_worker_for_click_house: false) + end + + it 'does nothing' do + allow(ClickHouse::Client).to receive(:database_configured?).and_return(true) + + expect(worker).not_to receive(:log_extra_metadata_on_done) + + worker.perform + end + end + + context 'when ClickHouse is available', :click_house do + let_it_be(:connection) { ClickHouse::Connection.new(:main) } + let_it_be_with_reload(:namespace1) { create(:group) } + let_it_be_with_reload(:namespace2) { create(:project).project_namespace } + let_it_be_with_reload(:namespace_with_updated_parent) { create(:group, parent: create(:group)) } + + let(:leftover_paths) { connection.select('SELECT DISTINCT path FROM events FINAL').pluck('path') } + let(:deleted_namespace_id) { namespace_with_updated_parent.id + 1 } + + before do + insert_query = <<~SQL + INSERT INTO events (id, path) VALUES + (1, '#{namespace1.id}/'), + (2, '#{namespace2.traversal_ids.join('/')}/'), + (3, '#{namespace1.id}/#{namespace_with_updated_parent.id}/'), + (4, '#{deleted_namespace_id}/'), + (5, '#{deleted_namespace_id}/') + SQL + + connection.execute(insert_query) + end + + it 'fixes all inconsistent records in ClickHouse' do + worker.perform + + paths = [ + "#{namespace1.id}/", + "#{namespace2.traversal_ids.join('/')}/", + "#{namespace_with_updated_parent.traversal_ids.join('/')}/" + ] + + expect(leftover_paths).to match_array(paths) + + # the next job starts from the beginning of the table + expect(ClickHouse::SyncCursor.cursor_for(:event_namespace_paths_consistency_check)).to eq(0) + end + + context 'when the table is empty' do + it 'does not do anything' do + connection.execute('TRUNCATE TABLE event_namespace_paths') + + expect { worker.perform }.not_to change { connection.select('SELECT * FROM events FINAL ORDER BY id') } + end + end + + context 'when the previous job was not finished' do + it 'continues the processing from the cursor' do + ClickHouse::SyncCursor.update_cursor_for(:event_namespace_paths_consistency_check, deleted_namespace_id) + + worker.perform + + paths = [ + "#{namespace1.id}/", + "#{namespace2.traversal_ids.join('/')}/", + "#{namespace1.id}/#{namespace_with_updated_parent.id}/" + ] + # the previous records should remain + expect(leftover_paths).to match_array(paths) + end + end + + context 'when processing stops due to the record clean up limit' do + it 'stores the last processed id value' do + stub_const("#{described_class}::MAX_RECORD_MODIFICATIONS", 1) + stub_const("#{ClickHouse::Concerns::ConsistencyWorker}::POSTGRESQL_BATCH_SIZE", 1) + + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :modification_limit_reached, modifications: 2 }) + + worker.perform + + paths = [ + "#{namespace1.id}/", + "#{namespace2.traversal_ids.join('/')}/", + "#{namespace_with_updated_parent.traversal_ids.join('/')}/" + ] + + expect(leftover_paths).to match_array(paths) + expect(ClickHouse::SyncCursor.cursor_for(:event_namespace_paths_consistency_check)).to eq(deleted_namespace_id) + end + end + + context 'when the processing stops due to time limit' do + it 'returns over_time status' do + stub_const("#{ClickHouse::Concerns::ConsistencyWorker}::POSTGRESQL_BATCH_SIZE", 1) + + allow_next_instance_of(Analytics::CycleAnalytics::RuntimeLimiter) do |runtime_limiter| + allow(runtime_limiter).to receive(:over_time?).and_return(false, true) + end + + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :over_time, modifications: 1 }) + + worker.perform + end + end + end +end |