From 4903f95b04db58edc1931ec917d2313c916a06b2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 6 Mar 2023 09:08:54 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../profiles/user_deletes_saved_reply_spec.rb | 27 ++++ .../projects/work_items/work_item_children_spec.rb | 179 +++++++++++++++++++++ .../features/projects/work_items/work_item_spec.rb | 37 +++++ .../features/work_items/work_item_children_spec.rb | 179 --------------------- spec/features/work_items/work_item_spec.rb | 37 ----- .../__snapshots__/list_item_spec.js.snap | 31 ++++ .../saved_replies/components/list_item_spec.js | 32 ++++ .../async_constraints/migration_helpers_spec.rb | 104 +++++++++++- .../postgres_async_constraint_validation_spec.rb | 10 ++ ...ial_index_on_vulnerability_report_types_spec.rb | 22 +++ spec/requests/users_controller_spec.rb | 100 +++++++++--- spec/serializers/profile/event_entity_spec.rb | 149 +++++++++++++++++ 12 files changed, 668 insertions(+), 239 deletions(-) create mode 100644 spec/features/profiles/user_deletes_saved_reply_spec.rb create mode 100644 spec/features/projects/work_items/work_item_children_spec.rb create mode 100644 spec/features/projects/work_items/work_item_spec.rb delete mode 100644 spec/features/work_items/work_item_children_spec.rb delete mode 100644 spec/features/work_items/work_item_spec.rb create mode 100644 spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb create mode 100644 spec/serializers/profile/event_entity_spec.rb (limited to 'spec') diff --git a/spec/features/profiles/user_deletes_saved_reply_spec.rb b/spec/features/profiles/user_deletes_saved_reply_spec.rb new file mode 100644 index 00000000000..35bd6018ee3 --- /dev/null +++ b/spec/features/profiles/user_deletes_saved_reply_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Profile > Saved replies > User deletes saved reply', :js, + feature_category: :user_profile do + let_it_be(:user) { create(:user) } + let_it_be(:saved_reply) { create(:saved_reply, user: user) } + + before do + sign_in(user) + end + + it 'shows the user a list of their saved replies' do + visit profile_saved_replies_path + + find('[data-testid="saved-reply-delete-btn"]').click + + page.within('.gl-modal') do + click_button 'Delete' + end + + wait_for_requests + + expect(page).not_to have_content(saved_reply.name) + end +end diff --git a/spec/features/projects/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb new file mode 100644 index 00000000000..43a6b2771f6 --- /dev/null +++ b/spec/features/projects/work_items/work_item_children_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Work item children', :js, feature_category: :team_planning do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue, project: project) } + + context 'for signed in user' do + before do + project.add_developer(user) + + sign_in(user) + + stub_feature_flags(work_items: true) + + visit project_issue_path(project, issue) + + wait_for_requests + end + + it 'are not displayed when issue does not have work item children', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + expect(find('[data-testid="links-empty"]')).to have_content(_('No tasks are currently assigned.')) + expect(page).not_to have_selector('[data-testid="add-links-form"]') + expect(page).not_to have_selector('[data-testid="links-child"]') + end + end + + it 'toggles widget body', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + expect(page).to have_selector('[data-testid="widget-body"]') + + click_button 'Collapse' + + expect(page).not_to have_selector('[data-testid="widget-body"]') + + click_button 'Expand' + + expect(page).to have_selector('[data-testid="widget-body"]') + end + end + + it 'toggles form', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + expect(page).not_to have_selector('[data-testid="add-links-form"]') + + click_button 'Add' + click_button 'New task' + + expect(page).to have_selector('[data-testid="add-links-form"]') + + click_button 'Cancel' + + expect(page).not_to have_selector('[data-testid="add-links-form"]') + end + end + + it 'adds a new child task', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + click_button 'Add' + click_button 'New task' + + expect(page).to have_button('Create task', disabled: true) + fill_in 'Add a title', with: 'Task 1' + + expect(page).to have_button('Create task', disabled: false) + + click_button 'Create task' + + wait_for_all_requests + + expect(find('[data-testid="links-child"]')).to have_content('Task 1') + end + end + + it 'removes a child task and undoing', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + click_button 'Add' + click_button 'New task' + fill_in 'Add a title', with: 'Task 1' + click_button 'Create task' + wait_for_all_requests + + expect(find('[data-testid="links-child"]')).to have_content('Task 1') + expect(find('[data-testid="children-count"]')).to have_content('1') + + find('[data-testid="links-menu"]').click + click_button 'Remove' + + wait_for_all_requests + + expect(page).not_to have_content('Task 1') + expect(find('[data-testid="children-count"]')).to have_content('0') + end + + page.within('.gl-toast') do + expect(find('.toast-body')).to have_content(_('Child removed')) + find('.b-toaster a', text: 'Undo').click + end + + wait_for_all_requests + + page.within('[data-testid="work-item-links"]') do + expect(find('[data-testid="links-child"]')).to have_content('Task 1') + expect(find('[data-testid="children-count"]')).to have_content('1') + end + end + + context 'with existing task' do + let_it_be(:task) { create(:work_item, :task, project: project) } + + it 'adds an existing child task', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + click_button 'Add' + click_button 'Existing task' + + expect(page).to have_button('Add task', disabled: true) + find('[data-testid="work-item-token-select-input"]').set(task.title) + wait_for_all_requests + click_button task.title + + expect(page).to have_button('Add task', disabled: false) + + click_button 'Add task' + + wait_for_all_requests + + expect(find('[data-testid="links-child"]')).to have_content(task.title) + end + end + end + + context 'in work item metadata' do + let_it_be(:label) { create(:label, title: 'Label 1', project: project) } + let_it_be(:milestone) { create(:milestone, project: project, title: 'v1') } + let_it_be(:task) do + create( + :work_item, + :task, + project: project, + labels: [label], + assignees: [user], + milestone: milestone + ) + end + + before do + visit project_issue_path(project, issue) + + wait_for_requests + end + + it 'displays labels, milestone and assignee for work item children', :aggregate_failures do + page.within('[data-testid="work-item-links"]') do + click_button 'Add' + click_button 'Existing task' + + find('[data-testid="work-item-token-select-input"]').set(task.title) + wait_for_all_requests + click_button task.title + + click_button 'Add task' + + wait_for_all_requests + end + + page.within('[data-testid="links-child"]') do + expect(page).to have_content(task.title) + expect(page).to have_content(label.title) + expect(page).to have_link(user.name) + expect(page).to have_content(milestone.title) + end + end + end + end +end diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb new file mode 100644 index 00000000000..3c71a27ff82 --- /dev/null +++ b/spec/features/projects/work_items/work_item_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Work item', :js, feature_category: :team_planning do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:work_item) { create(:work_item, project: project) } + + context 'for signed in user' do + before do + project.add_developer(user) + + sign_in(user) + + visit project_work_items_path(project, work_items_path: work_item.id) + end + + it_behaves_like 'work items status' + it_behaves_like 'work items assignees' + it_behaves_like 'work items labels' + it_behaves_like 'work items comments' + it_behaves_like 'work items description' + end + + context 'for signed in owner' do + before do + project.add_owner(user) + + sign_in(user) + + visit project_work_items_path(project, work_items_path: work_item.id) + end + + it_behaves_like 'work items invite members' + end +end diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb deleted file mode 100644 index 43a6b2771f6..00000000000 --- a/spec/features/work_items/work_item_children_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Work item children', :js, feature_category: :team_planning do - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, :public, namespace: group) } - let_it_be(:user) { create(:user) } - let_it_be(:issue) { create(:issue, project: project) } - - context 'for signed in user' do - before do - project.add_developer(user) - - sign_in(user) - - stub_feature_flags(work_items: true) - - visit project_issue_path(project, issue) - - wait_for_requests - end - - it 'are not displayed when issue does not have work item children', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - expect(find('[data-testid="links-empty"]')).to have_content(_('No tasks are currently assigned.')) - expect(page).not_to have_selector('[data-testid="add-links-form"]') - expect(page).not_to have_selector('[data-testid="links-child"]') - end - end - - it 'toggles widget body', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - expect(page).to have_selector('[data-testid="widget-body"]') - - click_button 'Collapse' - - expect(page).not_to have_selector('[data-testid="widget-body"]') - - click_button 'Expand' - - expect(page).to have_selector('[data-testid="widget-body"]') - end - end - - it 'toggles form', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - expect(page).not_to have_selector('[data-testid="add-links-form"]') - - click_button 'Add' - click_button 'New task' - - expect(page).to have_selector('[data-testid="add-links-form"]') - - click_button 'Cancel' - - expect(page).not_to have_selector('[data-testid="add-links-form"]') - end - end - - it 'adds a new child task', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - click_button 'Add' - click_button 'New task' - - expect(page).to have_button('Create task', disabled: true) - fill_in 'Add a title', with: 'Task 1' - - expect(page).to have_button('Create task', disabled: false) - - click_button 'Create task' - - wait_for_all_requests - - expect(find('[data-testid="links-child"]')).to have_content('Task 1') - end - end - - it 'removes a child task and undoing', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - click_button 'Add' - click_button 'New task' - fill_in 'Add a title', with: 'Task 1' - click_button 'Create task' - wait_for_all_requests - - expect(find('[data-testid="links-child"]')).to have_content('Task 1') - expect(find('[data-testid="children-count"]')).to have_content('1') - - find('[data-testid="links-menu"]').click - click_button 'Remove' - - wait_for_all_requests - - expect(page).not_to have_content('Task 1') - expect(find('[data-testid="children-count"]')).to have_content('0') - end - - page.within('.gl-toast') do - expect(find('.toast-body')).to have_content(_('Child removed')) - find('.b-toaster a', text: 'Undo').click - end - - wait_for_all_requests - - page.within('[data-testid="work-item-links"]') do - expect(find('[data-testid="links-child"]')).to have_content('Task 1') - expect(find('[data-testid="children-count"]')).to have_content('1') - end - end - - context 'with existing task' do - let_it_be(:task) { create(:work_item, :task, project: project) } - - it 'adds an existing child task', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - click_button 'Add' - click_button 'Existing task' - - expect(page).to have_button('Add task', disabled: true) - find('[data-testid="work-item-token-select-input"]').set(task.title) - wait_for_all_requests - click_button task.title - - expect(page).to have_button('Add task', disabled: false) - - click_button 'Add task' - - wait_for_all_requests - - expect(find('[data-testid="links-child"]')).to have_content(task.title) - end - end - end - - context 'in work item metadata' do - let_it_be(:label) { create(:label, title: 'Label 1', project: project) } - let_it_be(:milestone) { create(:milestone, project: project, title: 'v1') } - let_it_be(:task) do - create( - :work_item, - :task, - project: project, - labels: [label], - assignees: [user], - milestone: milestone - ) - end - - before do - visit project_issue_path(project, issue) - - wait_for_requests - end - - it 'displays labels, milestone and assignee for work item children', :aggregate_failures do - page.within('[data-testid="work-item-links"]') do - click_button 'Add' - click_button 'Existing task' - - find('[data-testid="work-item-token-select-input"]').set(task.title) - wait_for_all_requests - click_button task.title - - click_button 'Add task' - - wait_for_all_requests - end - - page.within('[data-testid="links-child"]') do - expect(page).to have_content(task.title) - expect(page).to have_content(label.title) - expect(page).to have_link(user.name) - expect(page).to have_content(milestone.title) - end - end - end - end -end diff --git a/spec/features/work_items/work_item_spec.rb b/spec/features/work_items/work_item_spec.rb deleted file mode 100644 index 3c71a27ff82..00000000000 --- a/spec/features/work_items/work_item_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Work item', :js, feature_category: :team_planning do - let_it_be(:project) { create(:project, :public) } - let_it_be(:user) { create(:user) } - let_it_be(:work_item) { create(:work_item, project: project) } - - context 'for signed in user' do - before do - project.add_developer(user) - - sign_in(user) - - visit project_work_items_path(project, work_items_path: work_item.id) - end - - it_behaves_like 'work items status' - it_behaves_like 'work items assignees' - it_behaves_like 'work items labels' - it_behaves_like 'work items comments' - it_behaves_like 'work items description' - end - - context 'for signed in owner' do - before do - project.add_owner(user) - - sign_in(user) - - visit project_work_items_path(project, work_items_path: work_item.id) - end - - it_behaves_like 'work items invite members' - end -end diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap index 3abdfcdaf20..154ce2bd089 100644 --- a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap +++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap @@ -10,6 +10,21 @@ exports[`Saved replies list item component renders list item 1`] = ` test + +
+ +
/assign_reviewer
+ + + + `; diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js index cad1000473b..1d80844f30a 100644 --- a/spec/frontend/saved_replies/components/list_item_spec.js +++ b/spec/frontend/saved_replies/components/list_item_spec.js @@ -1,11 +1,31 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { createMockDirective } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; import ListItem from '~/saved_replies/components/list_item.vue'; +import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql'; let wrapper; +let deleteSavedReplyMutationResponse; function createComponent(propsData = {}) { + Vue.use(VueApollo); + + deleteSavedReplyMutationResponse = jest + .fn() + .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } }); + return shallowMount(ListItem, { propsData, + directives: { + GlModal: createMockDirective('gl-modal'), + }, + apolloProvider: createMockApollo([ + [deleteSavedReplyMutation, deleteSavedReplyMutationResponse], + ]), }); } @@ -19,4 +39,16 @@ describe('Saved replies list item component', () => { expect(wrapper.element).toMatchSnapshot(); }); + + describe('delete button', () => { + it('calls Apollo mutate', async () => { + wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } }); + + wrapper.findComponent(GlModal).vm.$emit('primary'); + + await waitForPromises(); + + expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 }); + }); + }); }); diff --git a/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb index 08d255a4bb8..4dd510499ab 100644 --- a/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Database::AsyncConstraints::MigrationHelpers, feature_cat let(:column_name) { 'parent_id' } let(:fk_name) { nil } - context 'with regular tables' do + context 'with async FK validation on regular tables' do before do allow(migration).to receive(:puts) allow(migration.connection).to receive(:transaction_open?).and_return(false) @@ -183,4 +183,106 @@ RSpec.describe Gitlab::Database::AsyncConstraints::MigrationHelpers, feature_cat end end end + + context 'with async check constraint validations' do + let(:table_name) { '_test_async_check_constraints' } + let(:check_name) { 'partitioning_constraint' } + + before do + allow(migration).to receive(:puts) + allow(migration.connection).to receive(:transaction_open?).and_return(false) + + connection.create_table(table_name) do |t| + t.integer column_name + end + + migration.add_check_constraint( + table_name, "#{column_name} = 1", + check_name, validate: false) + end + + describe '#prepare_async_check_constraint_validation' do + it 'creates the record for async validation' do + expect do + migration.prepare_async_check_constraint_validation(table_name, name: check_name) + end.to change { constraint_model.where(name: check_name).count }.by(1) + + record = constraint_model.find_by(name: check_name) + + expect(record.table_name).to eq(table_name) + expect(record).to be_check_constraint + end + + context 'when the check constraint does not exist' do + it 'returns an error' do + expect do + migration.prepare_async_check_constraint_validation(table_name, name: 'missing') + end.to raise_error RuntimeError, /Could not find check constraint "missing" on table "#{table_name}"/ + end + end + + context 'when the record already exists' do + it 'does attempt to create the record' do + create(:postgres_async_constraint_validation, + table_name: table_name, + name: check_name, + constraint_type: :check_constraint) + + expect do + migration.prepare_async_check_constraint_validation(table_name, name: check_name) + end.not_to change { constraint_model.where(name: check_name).count } + end + end + + context 'when the async validation table does not exist' do + it 'does not raise an error' do + connection.drop_table(constraint_model.table_name) + + expect(constraint_model).not_to receive(:safe_find_or_create_by!) + + expect { migration.prepare_async_check_constraint_validation(table_name, name: check_name) } + .not_to raise_error + end + end + end + + describe '#unprepare_async_check_constraint_validation' do + context 'with check constraints' do + before do + migration.prepare_async_check_constraint_validation(table_name, name: check_name) + end + + it 'destroys the record' do + expect do + migration.unprepare_async_check_constraint_validation(table_name, name: check_name) + end.to change { constraint_model.where(name: check_name).count }.by(-1) + end + + context 'when the async validation table does not exist' do + it 'does not raise an error' do + connection.drop_table(constraint_model.table_name) + + expect(constraint_model).not_to receive(:find_by) + + expect { migration.unprepare_async_check_constraint_validation(table_name, name: check_name) } + .not_to raise_error + end + end + end + + context 'with other types of constraints' do + let(:constraint) { create(:postgres_async_constraint_validation, table_name: table_name, name: check_name) } + + it 'does not destroy the record' do + constraint.update_column(:constraint_type, 99) + + expect do + migration.unprepare_async_check_constraint_validation(table_name, name: check_name) + end.not_to change { constraint_model.where(name: check_name).count } + + expect(constraint).to be_present + end + end + end + end end diff --git a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb index 7fea2eb1bcc..da9cc18f7d7 100644 --- a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb +++ b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb @@ -48,6 +48,16 @@ RSpec.describe Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValida is_expected.to match_array([failed_validation, new_validation]) end end + + describe '.check_constraint_type' do + before do + new_validation.update!(constraint_type: :check_constraint) + end + + subject { described_class.check_constraint_type } + + it { is_expected.to eq([new_validation]) } + end end describe '.table_available?' do diff --git a/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb b/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb new file mode 100644 index 00000000000..cbf6b2882c4 --- /dev/null +++ b/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "spec_helper" + +require_migration! + +RSpec.describe AddTmpPartialIndexOnVulnerabilityReportTypes, feature_category: :vulnerability_management do + let(:async_index) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex } + let(:index_name) { described_class::INDEX_NAME } + + it "schedules the index" do + reversible_migration do |migration| + migration.before -> do + expect(async_index.where(name: index_name).count).to be(0) + end + + migration.after -> do + expect(async_index.where(name: index_name).count).to be(1) + end + end + end +end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 11d8be24e06..75ae0c970c0 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -174,39 +174,95 @@ RSpec.describe UsersController, feature_category: :user_management do end context 'requested in json format' do - let(:project) { create(:project) } + context 'when profile_tabs_vue feature flag is turned OFF' do + let(:project) { create(:project) } - before do - project.add_developer(user) - Gitlab::DataBuilder::Push.build_sample(project, user) + before do + project.add_developer(user) + Gitlab::DataBuilder::Push.build_sample(project, user) + stub_feature_flags(profile_tabs_vue: false) + sign_in(user) + end - sign_in(user) - end + it 'loads events' do + get user_activity_url user.username, format: :json - it 'loads events' do - get user_activity_url user.username, format: :json + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body)['count']).to eq(1) + end - expect(response.media_type).to eq('application/json') - expect(Gitlab::Json.parse(response.body)['count']).to eq(1) - end + it 'hides events if the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } - it 'hides events if the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + get user_activity_url user.username, format: :json - get user_activity_url user.username, format: :json + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body)['count']).to eq(0) + end - expect(response.media_type).to eq('application/json') - expect(Gitlab::Json.parse(response.body)['count']).to eq(0) + it 'hides events if the user has a private profile' do + Gitlab::DataBuilder::Push.build_sample(project, private_user) + + get user_activity_url private_user.username, format: :json + + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body)['count']).to eq(0) + end end - it 'hides events if the user has a private profile' do - Gitlab::DataBuilder::Push.build_sample(project, private_user) + context 'when profile_tabs_vue feature flag is turned ON' do + let(:project) { create(:project) } - get user_activity_url private_user.username, format: :json + before do + project.add_developer(user) + Gitlab::DataBuilder::Push.build_sample(project, user) + stub_feature_flags(profile_tabs_vue: true) + sign_in(user) + end - expect(response.media_type).to eq('application/json') - expect(Gitlab::Json.parse(response.body)['count']).to eq(0) + it 'loads events' do + get user_activity_url user.username, format: :json + + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body).count).to eq(1) + end + + it 'hides events if the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + get user_activity_url user.username, format: :json + + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body).count).to eq(0) + end + + it 'hides events if the user has a private profile' do + Gitlab::DataBuilder::Push.build_sample(project, private_user) + + get user_activity_url private_user.username, format: :json + + expect(response.media_type).to eq('application/json') + expect(Gitlab::Json.parse(response.body).count).to eq(0) + end + + it 'hides events if the user has a private profile' do + project = create(:project, :private) + private_event_user = create(:user, include_private_contributions: true) + push_data = Gitlab::DataBuilder::Push.build_sample(project, private_event_user) + EventCreateService.new.push(project, private_event_user, push_data) + + get user_activity_url private_event_user.username, format: :json + + response_body = Gitlab::Json.parse(response.body) + event = response_body.first + expect(response.media_type).to eq('application/json') + expect(response_body.count).to eq(1) + expect(event).to include('created_at', 'author', 'action') + expect(event['action']).to eq('private') + expect(event).not_to include('ref', 'commit', 'target', 'resource_parent') + end end end end diff --git a/spec/serializers/profile/event_entity_spec.rb b/spec/serializers/profile/event_entity_spec.rb new file mode 100644 index 00000000000..1551fc76466 --- /dev/null +++ b/spec/serializers/profile/event_entity_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Profile::EventEntity, feature_category: :user_profile do + let_it_be(:group) { create(:group) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:project) { build(:project_empty_repo, group: group) } + let_it_be(:user) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + + let(:target_user) { user } + let(:event) { build(:event, :merged, author: user, project: project, target: merge_request) } + let(:request) { double(described_class, current_user: user, target_user: target_user) } # rubocop:disable RSpec/VerifiedDoubles + let(:entity) { described_class.new(event, request: request) } + + subject { entity.as_json } + + before do + group.add_maintainer(user) + end + + it 'exposes fields', :aggregate_failures do + expect(subject[:created_at]).to eq(event.created_at) + expect(subject[:action]).to eq(event.action) + expect(subject[:author][:id]).to eq(target_user.id) + expect(subject[:author][:name]).to eq(target_user.name) + expect(subject[:author][:path]).to eq(target_user.username) + end + + context 'for push events' do + let_it_be(:commit_from) { Gitlab::Git::BLANK_SHA } + let_it_be(:commit_title) { 'My commit' } + let(:event) { build(:push_event, project: project, author: target_user) } + + it 'exposes ref fields' do + build(:push_event_payload, event: event, ref_count: 3) + + expect(subject[:ref][:type]).to eq(event.ref_type) + expect(subject[:ref][:count]).to eq(event.ref_count) + expect(subject[:ref][:name]).to eq(event.ref_name) + expect(subject[:ref][:path]).to be_nil + end + + shared_examples 'returns ref path' do + specify do + expect(subject[:ref][:path]).to be_present + end + end + + context 'with tag' do + before do + allow(project.repository).to receive(:tag_exists?).and_return(true) + build(:push_event_payload, event: event, ref_type: :tag) + end + + it_behaves_like 'returns ref path' + end + + context 'with branch' do + before do + allow(project.repository).to receive(:branch_exists?).and_return(true) + build(:push_event_payload, event: event, ref_type: :branch) + end + + it_behaves_like 'returns ref path' + end + + it 'exposes commit fields' do + build(:push_event_payload, event: event, commit_title: commit_title, commit_from: commit_from, commit_count: 2) + + compare_path = "/#{group.path}/#{project.path}/-/compare/#{commit_from}...#{event.commit_to}" + expect(subject[:commit][:compare_path]).to eq(compare_path) + expect(event.commit_id).to include(subject[:commit][:truncated_sha]) + expect(subject[:commit][:path]).to be_present + expect(subject[:commit][:title]).to eq(commit_title) + expect(subject[:commit][:count]).to eq(2) + expect(commit_from).to include(subject[:commit][:from_truncated_sha]) + expect(event.commit_to).to include(subject[:commit][:to_truncated_sha]) + expect(subject[:commit][:create_mr_path]).to be_nil + end + + it 'exposes create_mr_path' do + allow(project).to receive(:default_branch).and_return('main') + allow(project.repository).to receive(:branch_exists?).and_return(true) + build(:push_event_payload, event: event, action: :created, commit_from: commit_from, commit_count: 2) + + new_mr_path = "/#{group.path}/#{project.path}/-/merge_requests/new?" \ + "merge_request%5Bsource_branch%5D=#{event.branch_name}" + expect(subject[:commit][:create_mr_path]).to eq(new_mr_path) + end + end + + context 'with target' do + let_it_be(:note) { build(:note_on_merge_request, :with_attachment, noteable: merge_request, project: project) } + + context 'when target does not responds to :reference_link_text' do + let(:event) { build(:event, :commented, project: project, target: note, author: target_user) } + + it 'exposes target fields' do + expect(subject[:target]).not_to include(:reference_link_text) + expect(subject[:target][:target_type]).to eq(note.class.to_s) + expect(subject[:target][:target_url]).to be_present + expect(subject[:target][:title]).to eq(note.title) + expect(subject[:target][:first_line_in_markdown]).to be_present + expect(subject[:target][:attachment][:url]).to eq(note.attachment.url) + end + end + + context 'when target responds to :reference_link_text' do + it 'exposes reference_link_text' do + expect(subject[:target][:reference_link_text]).to eq(merge_request.reference_link_text) + end + end + end + + context 'with resource parent' do + it 'exposes resource parent fields' do + resource_parent = event.resource_parent + + expect(subject[:resource_parent][:type]).to eq('project') + expect(subject[:resource_parent][:full_name]).to eq(resource_parent.full_name) + expect(subject[:resource_parent][:full_path]).to eq(resource_parent.full_path) + end + end + + context 'for private events' do + let(:event) { build(:event, :merged, author: target_user) } + + context 'when include_private_contributions? is true' do + let(:target_user) { build(:user, include_private_contributions: true) } + + it 'exposes only created_at, action, and author', :aggregate_failures do + expect(subject[:created_at]).to eq(event.created_at) + expect(subject[:action]).to eq('private') + expect(subject[:author][:id]).to eq(target_user.id) + expect(subject[:author][:name]).to eq(target_user.name) + expect(subject[:author][:path]).to eq(target_user.username) + + is_expected.not_to include(:ref, :commit, :target, :resource_parent) + end + end + + context 'when include_private_contributions? is false' do + let(:target_user) { build(:user, include_private_contributions: false) } + + it { is_expected.to be_empty } + end + end +end -- cgit v1.2.3