diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-14 12:08:03 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-14 12:08:03 +0000 |
commit | 61a82b8ec062d6f122dadd38783c7754cef7ce2b (patch) | |
tree | 071d1ded4f507d77bac97156aa1fa85c95c0cba5 /spec | |
parent | 3ed578edf525bce3167860b84f6b43bab5065cf5 (diff) | |
download | gitlab-ce-61a82b8ec062d6f122dadd38783c7754cef7ce2b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
80 files changed, 1510 insertions, 1614 deletions
diff --git a/spec/controllers/concerns/issuable_actions_spec.rb b/spec/controllers/concerns/issuable_actions_spec.rb index c3fef591b91..37d9dc080e1 100644 --- a/spec/controllers/concerns/issuable_actions_spec.rb +++ b/spec/controllers/concerns/issuable_actions_spec.rb @@ -6,8 +6,8 @@ RSpec.describe IssuableActions do let(:project) { double('project') } let(:user) { double('user') } let(:issuable) { double('issuable') } - let(:finder_params_for_issuable) { {} } - let(:notes_result) { double('notes_result') } + let(:finder_params_for_issuable) { { project: project, target: issuable } } + let(:notes_result) { [] } let(:discussion_serializer) { double('discussion_serializer') } let(:controller) do @@ -55,13 +55,20 @@ RSpec.describe IssuableActions do end it 'instantiates and calls NotesFinder as expected' do + expect(issuable).to receive(:to_ability_name).and_return('issue') + expect(issuable).to receive(:project).and_return(project) + expect(Ability).to receive(:allowed?).at_least(1).and_return(true) expect(Discussion).to receive(:build_collection).and_return([]) expect(DiscussionSerializer).to receive(:new).and_return(discussion_serializer) expect(NotesFinder).to receive(:new).with(user, finder_params_for_issuable).and_call_original expect_any_instance_of(NotesFinder).to receive(:execute).and_return(notes_result) - expect(notes_result).to receive_messages(inc_relations_for_view: notes_result, includes: notes_result, fresh: notes_result) + expect(notes_result).to receive_messages( + with_web_entity_associations: notes_result, + inc_relations_for_view: notes_result, + fresh: notes_result + ) controller.discussions end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index ceff86456f7..026cf19bde5 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1780,7 +1780,7 @@ RSpec.describe Projects::MergeRequestsController do end it 'renders MergeRequest as JSON' do - expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user') + expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'current_user') end end @@ -1814,7 +1814,7 @@ RSpec.describe Projects::MergeRequestsController do it 'renders MergeRequest as JSON' do subject - expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user') + expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'current_user') end end diff --git a/spec/factories/experiment_subjects.rb b/spec/factories/experiment_subjects.rb deleted file mode 100644 index c35bc370bad..00000000000 --- a/spec/factories/experiment_subjects.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :experiment_subject do - experiment - user - variant { :control } - end -end diff --git a/spec/factories/experiments.rb b/spec/factories/experiments.rb deleted file mode 100644 index 2c51a6585f4..00000000000 --- a/spec/factories/experiments.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :experiment do - name { generate(:title) } - end -end diff --git a/spec/factories/projects/wiki_repositories.rb b/spec/factories/projects/wiki_repositories.rb new file mode 100644 index 00000000000..78e02ff297b --- /dev/null +++ b/spec/factories/projects/wiki_repositories.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_wiki_repository, class: 'Projects::WikiRepository' do + project + end +end diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 159306b28d8..b50e6779e07 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -18,6 +18,8 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli click_button('Merge') + puts merge_request.short_merged_commit_sha + expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb index 1ee0ea51e53..97b743b4d73 100644 --- a/spec/features/projects/network_graph_spec.rb +++ b/spec/features/projects/network_graph_spec.rb @@ -13,98 +13,110 @@ RSpec.describe 'Project Network Graph', :js do allow(Network::Graph).to receive(:max_count).and_return(10) end - context 'when branch is master' do - def switch_ref_to(ref_name) - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link ref_name + shared_examples 'network graph' do + context 'when branch is master' do + def switch_ref_to(ref_name) + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link ref_name + end end - end - def click_show_only_selected_branch_checkbox - find('#filter_ref').click - end + def click_show_only_selected_branch_checkbox + find('#filter_ref').click + end - before do - visit project_network_path(project, 'master') - end + before do + visit project_network_path(project, 'master') + end - it 'renders project network' do - expect(page).to have_selector ".network-graph" - expect(page).to have_selector '.dropdown-menu-toggle', text: "master" - page.within '.network-graph' do - expect(page).to have_content 'master' + it 'renders project network' do + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'master' + end end - end - it 'switches ref to branch' do - switch_ref_to('feature') + it 'switches ref to branch' do + switch_ref_to('feature') - expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature' - page.within '.network-graph' do - expect(page).to have_content 'feature' + expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' + end end - end - it 'switches ref to tag' do - switch_ref_to('v1.0.0') + it 'switches ref to tag' do + switch_ref_to('v1.0.0') - expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0' - page.within '.network-graph' do - expect(page).to have_content 'v1.0.0' + expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end end - end - it 'renders by commit sha of "v1.0.0"' do - page.within ".network-form" do - fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' - find('button').click + it 'renders by commit sha of "v1.0.0"' do + page.within ".network-form" do + fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + find('button').click + end + + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end end - expect(page).to have_selector ".network-graph" - expect(page).to have_selector '.dropdown-menu-toggle', text: "master" - page.within '.network-graph' do - expect(page).to have_content 'v1.0.0' - end - end + it 'filters select tag' do + switch_ref_to('v1.0.0') - it 'filters select tag' do - switch_ref_to('v1.0.0') + expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end - expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false - page.within '.network-graph' do - expect(page).to have_content 'Change some files' - end + click_show_only_selected_branch_checkbox - click_show_only_selected_branch_checkbox + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' + end - page.within '.network-graph' do - expect(page).not_to have_content 'Change some files' + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end end - click_show_only_selected_branch_checkbox + it 'renders error message when sha commit not exists' do + page.within ".network-form" do + fill_in 'extended_sha1', with: ';' + find('button').click + end - page.within '.network-graph' do - expect(page).to have_content 'Change some files' + expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist." end end - it 'renders error message when sha commit not exists' do - page.within ".network-form" do - fill_in 'extended_sha1', with: ';' - find('button').click - end + it 'renders project network with test branch' do + visit project_network_path(project, "'test'") - expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist." + page.within '.network-graph' do + expect(page).to have_content "'test'" + end end end - it 'renders project network with test branch' do - visit project_network_path(project, "'test'") + it_behaves_like 'network graph' - page.within '.network-graph' do - expect(page).to have_content "'test'" + context 'when disable_network_graph_notes_count is disabled' do + before do + stub_feature_flags(disable_network_graph_notes_count: false) end + + it_behaves_like 'network graph' end end diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb index c722a4ec05c..e2ee78a7cc5 100644 --- a/spec/features/users/active_sessions_spec.rb +++ b/spec/features/users/active_sessions_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions do user = create(:user) Gitlab::Redis::Sessions.with do |redis| - redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') + redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') end gitlab_sign_in(user) @@ -45,7 +45,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions do personal_access_token = create(:personal_access_token, user: user) Gitlab::Redis::Sessions.with do |redis| - redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') + redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') end visit user_path(user, :atom, private_token: personal_access_token.token) diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 9314f616c44..f14c60c4b8f 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -211,7 +211,7 @@ RSpec.describe BranchesFinder do it 'raises an error' do expect do subject - end.to raise_error(Gitlab::Git::CommandError, '13:could not find page token.') + end.to raise_error(Gitlab::Git::CommandError, /could not find page token/) end end diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index f1113ee3073..6f17e4193a3 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -6,6 +6,15 @@ import * as urlUtility from '~/lib/utils/url_utility'; import { TOKEN_TITLE_AUTHOR, TOKEN_TITLE_LABEL, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_HEALTH, + TOKEN_TYPE_ITERATION, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_TYPE, + TOKEN_TYPE_WEIGHT, } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; @@ -120,16 +129,16 @@ describe('BoardFilteredSearch', () => { it('sets the url params to the correct results', async () => { const mockFilters = [ - { type: 'author', value: { data: 'root', operator: '=' } }, - { type: 'assignee', value: { data: 'root', operator: '=' } }, - { type: 'label', value: { data: 'label', operator: '=' } }, - { type: 'label', value: { data: 'label&2', operator: '=' } }, - { type: 'milestone', value: { data: 'New Milestone', operator: '=' } }, - { type: 'type', value: { data: 'INCIDENT', operator: '=' } }, - { type: 'weight', value: { data: '2', operator: '=' } }, - { type: 'iteration', value: { data: 'Any&3', operator: '=' } }, - { type: 'release', value: { data: 'v1.0.0', operator: '=' } }, - { type: 'health_status', value: { data: 'onTrack', operator: '=' } }, + { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'root', operator: '=' } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'label&2', operator: '=' } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'New Milestone', operator: '=' } }, + { type: TOKEN_TYPE_TYPE, value: { data: 'INCIDENT', operator: '=' } }, + { type: TOKEN_TYPE_WEIGHT, value: { data: '2', operator: '=' } }, + { type: TOKEN_TYPE_ITERATION, value: { data: 'Any&3', operator: '=' } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'v1.0.0', operator: '=' } }, + { type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } }, ]; jest.spyOn(urlUtility, 'updateHistory'); findFilteredSearch().vm.$emit('onFilter', mockFilters); @@ -173,9 +182,9 @@ describe('BoardFilteredSearch', () => { it('passes the correct props to FilterSearchBar', () => { expect(findFilteredSearch().props('initialFilterValue')).toEqual([ - { type: 'author', value: { data: 'root', operator: '=' } }, - { type: 'label', value: { data: 'label', operator: '=' } }, - { type: 'health_status', value: { data: 'Any', operator: '=' } }, + { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } }, + { type: TOKEN_TYPE_HEALTH, value: { data: 'Any', operator: '=' } }, ]); }); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index a18b79d00a0..3c26fa97338 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -10,6 +10,12 @@ import { TOKEN_TITLE_MILESTONE, TOKEN_TITLE_RELEASE, TOKEN_TITLE_TYPE, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; @@ -752,7 +758,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI { icon: 'user', title: TOKEN_TITLE_ASSIGNEE, - type: 'assignee', + type: TOKEN_TYPE_ASSIGNEE, operators: OPERATOR_IS_AND_IS_NOT, token: AuthorToken, unique: true, @@ -762,7 +768,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI { icon: 'pencil', title: TOKEN_TITLE_AUTHOR, - type: 'author', + type: TOKEN_TYPE_AUTHOR, operators: OPERATOR_IS_AND_IS_NOT, symbol: '@', token: AuthorToken, @@ -773,7 +779,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI { icon: 'labels', title: TOKEN_TITLE_LABEL, - type: 'label', + type: TOKEN_TYPE_LABEL, operators: OPERATOR_IS_AND_IS_NOT, token: LabelToken, unique: false, @@ -785,7 +791,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI icon: 'clock', title: TOKEN_TITLE_MILESTONE, symbol: '%', - type: 'milestone', + type: TOKEN_TYPE_MILESTONE, shouldSkipSort: true, token: MilestoneToken, unique: true, @@ -794,7 +800,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI { icon: 'issues', title: TOKEN_TITLE_TYPE, - type: 'type', + type: TOKEN_TYPE_TYPE, token: GlFilteredSearchToken, unique: true, options: [ @@ -803,7 +809,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI ], }, { - type: 'release', + type: TOKEN_TYPE_RELEASE, title: TOKEN_TITLE_RELEASE, icon: 'rocket', token: ReleaseToken, diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index 4ab3d56a13f..18f89fbc5e5 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -147,6 +147,20 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: expect_graphql_errors_to_be_empty end end + + context 'merge request in state getState query' do + base_input_path = 'vue_merge_request_widget/queries/' + base_output_path = 'graphql/merge_requests/' + query_name = 'get_state.query.graphql' + + it "#{base_output_path}#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s }) + + expect_graphql_errors_to_be_empty + end + end end private diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index a5488941791..d0c93c896b3 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -33,16 +33,6 @@ import { CREATED_DESC, RELATIVE_POSITION, RELATIVE_POSITION_ASC, - TOKEN_TYPE_ASSIGNEE, - TOKEN_TYPE_AUTHOR, - TOKEN_TYPE_CONFIDENTIAL, - TOKEN_TYPE_CONTACT, - TOKEN_TYPE_LABEL, - TOKEN_TYPE_MILESTONE, - TOKEN_TYPE_MY_REACTION, - TOKEN_TYPE_ORGANIZATION, - TOKEN_TYPE_RELEASE, - TOKEN_TYPE_TYPE, urlSortParams, } from '~/issues/list/constants'; import eventHub from '~/issues/list/eventhub'; @@ -57,7 +47,19 @@ import { WORK_ITEM_TYPE_ENUM_TASK, WORK_ITEM_TYPE_ENUM_TEST_CASE, } from '~/work_items/constants'; -import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + FILTERED_SEARCH_TERM, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_CONFIDENTIAL, + TOKEN_TYPE_CONTACT, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_ORGANIZATION, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_TYPE, +} from '~/vue_shared/components/filtered_search_bar/constants'; import('~/issuable/bulk_update_sidebar'); import('~/users_select'); diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index e80191a95c8..62fcbf7aad0 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -1,7 +1,21 @@ import { + FILTERED_SEARCH_TERM, OPERATOR_IS, OPERATOR_IS_NOT, OPERATOR_OR, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_CONFIDENTIAL, + TOKEN_TYPE_CONTACT, + TOKEN_TYPE_EPIC, + TOKEN_TYPE_ITERATION, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_ORGANIZATION, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_TYPE, + TOKEN_TYPE_WEIGHT, } from '~/vue_shared/components/filtered_search_bar/constants'; export const getIssuesQueryResponse = { @@ -169,58 +183,58 @@ export const locationSearchWithSpecialValues = [ ].join('&'); export const filteredTokens = [ - { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } }, - { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } }, - { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } }, - { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } }, - { type: 'assignee_username', value: { data: '5', operator: OPERATOR_IS } }, - { type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } }, - { type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } }, - { type: 'assignee_username', value: { data: 'carl', operator: OPERATOR_OR } }, - { type: 'assignee_username', value: { data: 'lenny', operator: OPERATOR_OR } }, - { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } }, - { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } }, - { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } }, - { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } }, - { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } }, - { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } }, - { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } }, - { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } }, - { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } }, - { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } }, - { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } }, - { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } }, - { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } }, - { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } }, - { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } }, - { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } }, - { type: 'weight', value: { data: '1', operator: OPERATOR_IS } }, - { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } }, - { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } }, - { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } }, - { type: 'filtered-search-term', value: { data: 'find' } }, - { type: 'filtered-search-term', value: { data: 'issues' } }, + { type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_TYPE, value: { data: 'issue', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_IS_NOT } }, + { type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } }, + { type: FILTERED_SEARCH_TERM, value: { data: 'find' } }, + { type: FILTERED_SEARCH_TERM, value: { data: 'issues' } }, ]; export const filteredTokensWithSpecialValues = [ - { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } }, - { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } }, - { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } }, - { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'weight', value: { data: 'None', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: '123', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_MY_REACTION, value: { data: 'None', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_ITERATION, value: { data: 'Current', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_LABEL, value: { data: 'None', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_RELEASE, value: { data: 'None', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } }, + { type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } }, ]; export const apiParams = { diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js new file mode 100644 index 00000000000..f0b318e69ec --- /dev/null +++ b/spec/frontend/observability/observability_app_spec.js @@ -0,0 +1,73 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ObservabilityApp from '~/observability/components/observability_app.vue'; + +describe('Observability root app', () => { + let wrapper; + const replace = jest.fn(); + const $router = { + replace, + }; + const $route = { + pathname: 'https://gitlab.com/gitlab-org/', + query: { otherQuery: 100 }, + }; + + const findIframe = () => wrapper.findByTestId('observability-ui-iframe'); + + const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840'; + + const mountComponent = (route = $route) => { + wrapper = shallowMountExtended(ObservabilityApp, { + propsData: { + observabilityIframeSrc: TEST_IFRAME_SRC, + }, + mocks: { + $router, + $route: route, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render an iframe with observabilityIframeSrc as src', () => { + mountComponent(); + const iframe = findIframe(); + expect(iframe.exists()).toBe(true); + expect(iframe.attributes('src')).toBe(TEST_IFRAME_SRC); + }); + + it('should not call replace method from vue router if message event does not have url', () => { + mountComponent(); + wrapper.vm.messageHandler({ data: 'some other data' }); + expect(replace).not.toHaveBeenCalled(); + }); + + it.each` + condition | origin | observability_path | url + ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'} + ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'} + `( + 'should not call replace method from vue router if $condition', + async ({ origin, observability_path, url }) => { + mountComponent({ ...$route, query: { observability_path } }); + wrapper.vm.messageHandler({ data: { url }, origin }); + expect(replace).not.toHaveBeenCalled(); + }, + ); + + it('should call replace method from vue router on messageHandle call', () => { + mountComponent(); + wrapper.vm.messageHandler({ data: { url: '/explore' }, origin: 'https://observe.gitlab.com' }); + expect(replace).toHaveBeenCalled(); + expect(replace).toHaveBeenCalledWith({ + name: 'https://gitlab.com/gitlab-org/', + query: { + otherQuery: 100, + observability_path: '/explore', + }, + }); + }); +}); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js index 05c259de370..7b52773e92d 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js @@ -8,7 +8,7 @@ jest.mock('~/vue_shared/plugins/global_toast'); let wrapper; -function createWrapper(propsData, mergeRequestWidgetGraphql) { +function createWrapper(propsData) { wrapper = mount(WidgetRebase, { propsData, data() { @@ -22,7 +22,6 @@ function createWrapper(propsData, mergeRequestWidgetGraphql) { }, }; }, - provide: { glFeatures: { mergeRequestWidgetGraphql } }, mocks: { $apollo: { queries: { @@ -43,276 +42,244 @@ describe('Merge request widget rebase component', () => { wrapper.destroy(); wrapper = null; }); - - [true, false].forEach((mergeRequestWidgetGraphql) => { - describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => { - describe('while rebasing', () => { - it('should show progress message', () => { - createWrapper( - { - mr: { rebaseInProgress: true }, - service: {}, - }, - mergeRequestWidgetGraphql, - ); - - expect(findRebaseMessageText()).toContain('Rebase in progress'); - }); + describe('while rebasing', () => { + it('should show progress message', () => { + createWrapper({ + mr: { rebaseInProgress: true }, + service: {}, }); - describe('with permissions', () => { - const rebaseMock = jest.fn().mockResolvedValue(); - const pollMock = jest.fn().mockResolvedValue({}); + expect(findRebaseMessageText()).toContain('Rebase in progress'); + }); + }); + + describe('with permissions', () => { + const rebaseMock = jest.fn().mockResolvedValue(); + const pollMock = jest.fn().mockResolvedValue({}); - it('renders the warning message', () => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); + it('renders the warning message', () => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }); - const text = findRebaseMessageText(); + const text = findRebaseMessageText(); - expect(text).toContain('Merge blocked'); - expect(text.replace(/\s\s+/g, ' ')).toContain( - 'the source branch must be rebased onto the target branch', - ); - }); + expect(text).toContain('Merge blocked'); + expect(text.replace(/\s\s+/g, ' ')).toContain( + 'the source branch must be rebased onto the target branch', + ); + }); - it('renders an error message when rebasing has failed', async () => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); + it('renders an error message when rebasing has failed', async () => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }); + + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax + wrapper.setData({ rebasingError: 'Something went wrong!' }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ rebasingError: 'Something went wrong!' }); + await nextTick(); + expect(findRebaseMessageText()).toContain('Something went wrong!'); + }); - await nextTick(); - expect(findRebaseMessageText()).toContain('Something went wrong!'); + describe('Rebase buttons', () => { + beforeEach(() => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, }); + }); - describe('Rebase buttons', () => { - beforeEach(() => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); - }); + it('renders both buttons', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(true); + expect(findStandardRebaseButton().exists()).toBe(true); + }); - it('renders both buttons', () => { - expect(findRebaseWithoutCiButton().exists()).toBe(true); - expect(findStandardRebaseButton().exists()).toBe(true); - }); + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); - it('starts the rebase when clicking', async () => { - findStandardRebaseButton().vm.$emit('click'); + await nextTick(); - await nextTick(); + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); - }); + it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { + findRebaseWithoutCiButton().vm.$emit('click'); - it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { - findRebaseWithoutCiButton().vm.$emit('click'); + await nextTick(); - await nextTick(); + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); + }); + }); - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); - }); + describe('Rebase when pipelines must succeed is enabled', () => { + beforeEach(() => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + onlyAllowMergeIfPipelineSucceeds: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, }); + }); - describe('Rebase when pipelines must succeed is enabled', () => { - beforeEach(() => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - onlyAllowMergeIfPipelineSucceeds: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); - }); + it('renders only the rebase button', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(false); + expect(findStandardRebaseButton().exists()).toBe(true); + }); - it('renders only the rebase button', () => { - expect(findRebaseWithoutCiButton().exists()).toBe(false); - expect(findStandardRebaseButton().exists()).toBe(true); - }); + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); - it('starts the rebase when clicking', async () => { - findStandardRebaseButton().vm.$emit('click'); + await nextTick(); - await nextTick(); + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + }); - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); - }); + describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => { + beforeEach(() => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + onlyAllowMergeIfPipelineSucceeds: true, + allowMergeOnSkippedPipeline: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, }); + }); - describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => { - beforeEach(() => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: true, - onlyAllowMergeIfPipelineSucceeds: true, - allowMergeOnSkippedPipeline: true, - }, - service: { - rebase: rebaseMock, - poll: pollMock, - }, - }, - mergeRequestWidgetGraphql, - ); - }); + it('renders both rebase buttons', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(true); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); - it('renders both rebase buttons', () => { - expect(findRebaseWithoutCiButton().exists()).toBe(true); - expect(findStandardRebaseButton().exists()).toBe(true); - }); + await nextTick(); - it('starts the rebase when clicking', async () => { - findStandardRebaseButton().vm.$emit('click'); + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); - await nextTick(); + it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { + findRebaseWithoutCiButton().vm.$emit('click'); - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); - }); + await nextTick(); - it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { - findRebaseWithoutCiButton().vm.$emit('click'); + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); + }); + }); + }); - await nextTick(); + describe('without permissions', () => { + const exampleTargetBranch = 'fake-branch-to-test-with'; - expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); - }); + describe('UI text', () => { + beforeEach(() => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: exampleTargetBranch, + }, + service: {}, }); }); - describe('without permissions', () => { - const exampleTargetBranch = 'fake-branch-to-test-with'; - - describe('UI text', () => { - beforeEach(() => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: false, - targetBranch: exampleTargetBranch, - }, - service: {}, - }, - mergeRequestWidgetGraphql, - ); - }); - - it('renders a message explaining user does not have permissions', () => { - const text = findRebaseMessageText(); - - expect(text).toContain( - 'Merge blocked: the source branch must be rebased onto the target branch.', - ); - expect(text).toContain('the source branch must be rebased'); - }); - - it('renders the correct target branch name', () => { - const elem = findRebaseMessage(); - - expect(elem.text()).toContain( - 'Merge blocked: the source branch must be rebased onto the target branch.', - ); - }); - }); + it('renders a message explaining user does not have permissions', () => { + const text = findRebaseMessageText(); - it('does render the "Rebase without pipeline" button', () => { - createWrapper( - { - mr: { - rebaseInProgress: false, - canPushToSourceBranch: false, - targetBranch: exampleTargetBranch, - }, - service: {}, - }, - mergeRequestWidgetGraphql, - ); + expect(text).toContain( + 'Merge blocked: the source branch must be rebased onto the target branch.', + ); + expect(text).toContain('the source branch must be rebased'); + }); - expect(findRebaseWithoutCiButton().exists()).toBe(true); - }); + it('renders the correct target branch name', () => { + const elem = findRebaseMessage(); + + expect(elem.text()).toContain( + 'Merge blocked: the source branch must be rebased onto the target branch.', + ); + }); + }); + + it('does render the "Rebase without pipeline" button', () => { + createWrapper({ + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: exampleTargetBranch, + }, + service: {}, }); - describe('methods', () => { - it('checkRebaseStatus', async () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - createWrapper( - { - mr: {}, - service: { - rebase() { - return Promise.resolve(); - }, - poll() { - return Promise.resolve({ - data: { - rebase_in_progress: false, - should_be_rebased: false, - merge_error: null, - }, - }); - }, + expect(findRebaseWithoutCiButton().exists()).toBe(true); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', async () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + createWrapper({ + mr: {}, + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + should_be_rebased: false, + merge_error: null, }, - }, - mergeRequestWidgetGraphql, - ); + }); + }, + }, + }); - wrapper.vm.rebase(); + wrapper.vm.rebase(); - // Wait for the rebase request - await nextTick(); - // Wait for the polling request - await nextTick(); - // Wait for the eventHub to be called - await nextTick(); + // Wait for the rebase request + await nextTick(); + // Wait for the polling request + await nextTick(); + // Wait for the eventHub to be called + await nextTick(); - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess'); - expect(toast).toHaveBeenCalledWith('Rebase completed'); - }); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess'); + expect(toast).toHaveBeenCalledWith('Rebase completed'); }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap index 8c3a4978bb8..bd40a968392 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = ` +exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = ` <div class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal" > @@ -51,199 +51,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have class="gl-mr-3" data-testid="statusText" > - Set by - <a - class="author-link inline" - > - <img - class="avatar avatar-inline s16" - src="no_avatar.png" - /> - - <span - class="author" - > - - </span> - </a> - to be merged automatically when the pipeline succeeds - </h4> - - <div - class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3" - > - <div - class="gl-display-flex gl-align-items-flex-start" - > - <div - class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - lazy="" - no-caret="" - title="Options" - > - <!----> - <button - aria-expanded="false" - aria-haspopup="true" - class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" - type="button" - > - <!----> - - <svg - aria-hidden="true" - class="dropdown-icon gl-icon s16" - data-testid="ellipsis_v-icon" - role="img" - > - <use - href="#ellipsis_v" - /> - </svg> - - <span - class="gl-new-dropdown-button-text gl-sr-only" - > - - </span> - - <svg - aria-hidden="true" - class="gl-button-icon dropdown-chevron gl-icon s16" - data-testid="chevron-down-icon" - role="img" - > - <use - href="#chevron-down" - /> - </svg> - </button> - <ul - class="dropdown-menu dropdown-menu-right" - role="menu" - tabindex="-1" - > - <!----> - </ul> - </div> - - <button - class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - type="button" - > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Cancel auto-merge - - </span> - </button> - </div> - </div> - </div> - - <div - class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1" - > - <button - class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon" - title="Collapse merge details" - type="button" - > - <!----> - - <svg - aria-hidden="true" - class="gl-button-icon gl-icon s16" - data-testid="chevron-lg-up-icon" - role="img" - > - <use - href="#chevron-lg-up" - /> - </svg> - - <!----> - </button> - </div> - </div> -</div> -`; - -exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = ` -<div - class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal" -> - <div - class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3" - > - <div - class="gl-display-flex gl-m-auto" - > - <div - class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2" - > - <div - class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon" - > - <div - class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" - > - <div - class="gl-display-flex gl-m-auto gl-translate-y-n50" - > - <svg - aria-label="Scheduled " - class="gl-display-block gl-icon s12" - data-qa-selector="status_scheduled_icon" - data-testid="status-scheduled-icon" - role="img" - > - <use - href="#status-scheduled" - /> - </svg> - </div> - </div> - </div> - </div> - </div> - </div> - - <div - class="gl-display-flex gl-w-full" - > - <div - class="media-body gl-display-flex gl-align-items-center" - > - - <h4 - class="gl-mr-3" - data-testid="statusText" - > - Set by - <a - class="author-link inline" - > - <img - class="avatar avatar-inline s16" - src="no_avatar.png" - /> - - <span - class="author" - > - - </span> - </a> - to be merged automatically when the pipeline succeeds + Set by to be merged automatically when the pipeline succeeds </h4> <div diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js index 28182793683..5b9f30dfb86 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js @@ -9,7 +9,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; let wrapper; -let mergeRequestWidgetGraphqlEnabled = false; function convertPropsToGraphqlState(props) { return { @@ -30,12 +29,6 @@ function convertPropsToGraphqlState(props) { } function factory(propsData, stateOverride = {}) { - let state = {}; - - if (mergeRequestWidgetGraphqlEnabled) { - state = { ...convertPropsToGraphqlState(propsData), ...stateOverride }; - } - wrapper = extendedWrapper( mount(autoMergeEnabledComponent, { propsData: { @@ -43,9 +36,8 @@ function factory(propsData, stateOverride = {}) { service: new MRWidgetService({}), }, data() { - return { state }; + return { ...convertPropsToGraphqlState(propsData), ...stateOverride }; }, - provide: { glFeatures: { mergeRequestWidgetGraphql: mergeRequestWidgetGraphqlEnabled } }, mocks: { $apollo: { queries: { @@ -95,130 +87,88 @@ describe('MRWidgetAutoMergeEnabled', () => { wrapper = null; }); - [true, false].forEach((mergeRequestWidgetGraphql) => { - describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => { - beforeEach(() => { - mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql; + describe('computed', () => { + describe('cancelButtonText', () => { + it('should return "Cancel" if MWPS is selected', () => { + factory({ + ...defaultMrProps(), + autoMergeStrategy: MWPS_MERGE_STRATEGY, + }); + + expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe('Cancel auto-merge'); }); + }); + }); - describe('computed', () => { - describe('cancelButtonText', () => { - it('should return "Cancel" if MWPS is selected', () => { - factory({ - ...defaultMrProps(), - autoMergeStrategy: MWPS_MERGE_STRATEGY, + describe('methods', () => { + describe('cancelAutomaticMerge', () => { + it('should set flag and call service then tell main component to update the widget with data', async () => { + factory({ + ...defaultMrProps(), + }); + const mrObj = { + is_new_mr_data: true, + }; + jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue( + new Promise((resolve) => { + resolve({ + data: mrObj, }); + }), + ); - expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe( - 'Cancel auto-merge', - ); - }); - }); - }); + wrapper.vm.cancelAutomaticMerge(); - describe('methods', () => { - describe('cancelAutomaticMerge', () => { - it('should set flag and call service then tell main component to update the widget with data', async () => { - factory({ - ...defaultMrProps(), - }); - const mrObj = { - is_new_mr_data: true, - }; - jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue( - new Promise((resolve) => { - resolve({ - data: mrObj, - }); - }), - ); - - wrapper.vm.cancelAutomaticMerge(); - - await waitForPromises(); - - expect(wrapper.vm.isCancellingAutoMerge).toBe(true); - if (mergeRequestWidgetGraphql) { - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - } else { - expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); - } - }); - }); + await waitForPromises(); - describe('removeSourceBranch', () => { - it('should set flag and call service then request main component to update the widget', async () => { - factory({ - ...defaultMrProps(), - }); - jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue( - Promise.resolve({ - data: { - status: MWPS_MERGE_STRATEGY, - }, - }), - ); - - wrapper.vm.removeSourceBranch(); - - await waitForPromises(); - - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - expect(wrapper.vm.service.merge).toHaveBeenCalledWith({ - sha, - auto_merge_strategy: MWPS_MERGE_STRATEGY, - should_remove_source_branch: true, - }); - }); - }); + expect(wrapper.vm.isCancellingAutoMerge).toBe(true); + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); }); + }); + }); - describe('template', () => { - it('should have correct elements', () => { - factory({ - ...defaultMrProps(), - }); + describe('template', () => { + it('should have correct elements', () => { + factory({ + ...defaultMrProps(), + }); - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchSnapshot(); + }); - it('should disable cancel auto merge button when the action is in progress', async () => { - factory({ - ...defaultMrProps(), - }); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - isCancellingAutoMerge: true, - }); + it('should disable cancel auto merge button when the action is in progress', async () => { + factory({ + ...defaultMrProps(), + }); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax + wrapper.setData({ + isCancellingAutoMerge: true, + }); - await nextTick(); + await nextTick(); - expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true); - }); + expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true); + }); - it('should render the status text as "...to merged automatically" if MWPS is selected', () => { - factory({ - ...defaultMrProps(), - autoMergeStrategy: MWPS_MERGE_STRATEGY, - }); + it('should render the status text as "...to merged automatically" if MWPS is selected', () => { + factory({ + ...defaultMrProps(), + autoMergeStrategy: MWPS_MERGE_STRATEGY, + }); - expect(getStatusText()).toContain( - 'to be merged automatically when the pipeline succeeds', - ); - }); + expect(getStatusText()).toContain('to be merged automatically when the pipeline succeeds'); + }); - it('should render the cancel button as "Cancel" if MWPS is selected', () => { - factory({ - ...defaultMrProps(), - autoMergeStrategy: MWPS_MERGE_STRATEGY, - }); + it('should render the cancel button as "Cancel" if MWPS is selected', () => { + factory({ + ...defaultMrProps(), + autoMergeStrategy: MWPS_MERGE_STRATEGY, + }); - const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text()); + const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text()); - expect(cancelButtonText).toBe('Cancel auto-merge'); - }); - }); + expect(cancelButtonText).toBe('Cancel auto-merge'); }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js index 398a3912882..826f708069c 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -9,18 +9,11 @@ describe('MRWidgetAutoMergeFailed', () => { const mergeError = 'This is the merge error'; const findButton = () => wrapper.findComponent(GlButton); - const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => { + const createComponent = (props = {}) => { wrapper = mount(AutoMergeFailedComponent, { propsData: { ...props }, data() { - if (mergeRequestWidgetGraphql) { - return { mergeError: props.mr?.mergeError }; - } - - return {}; - }, - provide: { - glFeatures: { mergeRequestWidgetGraphql }, + return { mergeError: props.mr?.mergeError }; }, }); }; @@ -29,40 +22,33 @@ describe('MRWidgetAutoMergeFailed', () => { wrapper.destroy(); }); - [true, false].forEach((mergeRequestWidgetGraphql) => { - describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => { - beforeEach(() => { - createComponent( - { - mr: { mergeError }, - }, - mergeRequestWidgetGraphql, - ); - }); + beforeEach(() => { + createComponent({ + mr: { mergeError }, + }); + }); - it('renders failed message', () => { - expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); - }); + it('renders failed message', () => { + expect(wrapper.text()).toContain('This merge request failed to be merged automatically'); + }); - it('renders merge error provided', () => { - expect(wrapper.text()).toContain(mergeError); - }); + it('renders merge error provided', () => { + expect(wrapper.text()).toContain(mergeError); + }); - it('render refresh button', () => { - expect(findButton().text()).toBe('Refresh'); - }); + it('render refresh button', () => { + expect(findButton().text()).toBe('Refresh'); + }); - it('emits event and shows loading icon when button is clicked', async () => { - jest.spyOn(eventHub, '$emit'); - findButton().vm.$emit('click'); + it('emits event and shows loading icon when button is clicked', async () => { + jest.spyOn(eventHub, '$emit'); + findButton().vm.$emit('click'); - expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); + expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested'); - await nextTick(); + await nextTick(); - expect(findButton().attributes('disabled')).toBe('disabled'); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - }); + expect(findButton().attributes('disabled')).toBe('disabled'); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js index 7a9fd5b002d..a16e4d4a6ea 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js @@ -7,7 +7,6 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_ describe('MRWidgetConflicts', () => { let wrapper; - let mergeRequestWidgetGraphql = null; const path = '/conflicts'; const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button'); @@ -25,10 +24,17 @@ describe('MRWidgetConflicts', () => { wrapper = extendedWrapper( mount(ConflictsComponent, { propsData, - provide: { - glFeatures: { - mergeRequestWidgetGraphql, - }, + data() { + return { + userPermissions: { + canMerge: propsData.mr.canMerge, + pushToSourceBranch: propsData.mr.canPushToSourceBranch, + }, + state: { + shouldBeRebased: propsData.mr.shouldBeRebased, + sourceBranchProtected: propsData.mr.sourceBranchProtected, + }, + }; }, mocks: { $apollo: { @@ -41,212 +47,188 @@ describe('MRWidgetConflicts', () => { }), ); - if (mergeRequestWidgetGraphql) { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - userPermissions: { - canMerge: propsData.mr.canMerge, - pushToSourceBranch: propsData.mr.canPushToSourceBranch, - }, - stateData: { - shouldBeRebased: propsData.mr.shouldBeRebased, - sourceBranchProtected: propsData.mr.sourceBranchProtected, - }, - }); - } - await nextTick(); } afterEach(() => { - mergeRequestWidgetGraphql = null; wrapper.destroy(); }); - [false, true].forEach((featureEnabled) => { - describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => { - beforeEach(() => { - mergeRequestWidgetGraphql = featureEnabled; + // There are two permissions we need to consider: + // + // 1. Is the user allowed to merge to the target branch? + // 2. Is the user allowed to push to the source branch? + // + // This yields 4 possible permutations that we need to test, and + // we test them below. A user who can push to the source + // branch should be allowed to resolve conflicts. This is + // consistent with what the backend does. + describe('when allowed to merge but not allowed to push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: false, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, }); + }); - // There are two permissions we need to consider: - // - // 1. Is the user allowed to merge to the target branch? - // 2. Is the user allowed to push to the source branch? - // - // This yields 4 possible permutations that we need to test, and - // we test them below. A user who can push to the source - // branch should be allowed to resolve conflicts. This is - // consistent with what the backend does. - describe('when allowed to merge but not allowed to push to source branch', () => { - beforeEach(async () => { - await createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: false, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, - }); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(wrapper.text()).toContain(mergeConflictsText); + expect(wrapper.text()).not.toContain(userCannotMergeText); + }); - it('should tell you about conflicts without bothering other people', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).not.toContain(userCannotMergeText); - }); + it('should not allow you to resolve the conflicts', () => { + expect(wrapper.text()).not.toContain(resolveConflictsBtnText); + }); - it('should not allow you to resolve the conflicts', () => { - expect(wrapper.text()).not.toContain(resolveConflictsBtnText); - }); + it('should have merge buttons', () => { + expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText); + }); + }); - it('should have merge buttons', () => { - expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText); - }); + describe('when not allowed to merge but allowed to push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: true, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, }); + }); - describe('when not allowed to merge but allowed to push to source branch', () => { - beforeEach(async () => { - await createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: true, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, - }); - }); - - it('should tell you about conflicts', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).toContain(userCannotMergeText); - }); - - it('should allow you to resolve the conflicts', () => { - expect(findResolveButton().text()).toContain(resolveConflictsBtnText); - expect(findResolveButton().attributes('href')).toEqual(path); - }); - - it('should not have merge buttons', () => { - expect(wrapper.text()).not.toContain(mergeLocallyBtnText); - }); + it('should tell you about conflicts', () => { + expect(wrapper.text()).toContain(mergeConflictsText); + expect(wrapper.text()).toContain(userCannotMergeText); + }); + + it('should allow you to resolve the conflicts', () => { + expect(findResolveButton().text()).toContain(resolveConflictsBtnText); + expect(findResolveButton().attributes('href')).toEqual(path); + }); + + it('should not have merge buttons', () => { + expect(wrapper.text()).not.toContain(mergeLocallyBtnText); + }); + }); + + describe('when allowed to merge and push to source branch', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: path, + conflictsDocsPath: '', + }, }); + }); - describe('when allowed to merge and push to source branch', () => { - beforeEach(async () => { - await createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: path, - conflictsDocsPath: '', - }, - }); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).not.toContain(userCannotMergeText); - }); - - it('should allow you to resolve the conflicts', () => { - expect(findResolveButton().text()).toContain(resolveConflictsBtnText); - expect(findResolveButton().attributes('href')).toEqual(path); - }); - - it('should have merge buttons', () => { - expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(wrapper.text()).toContain(mergeConflictsText); + expect(wrapper.text()).not.toContain(userCannotMergeText); + }); + + it('should allow you to resolve the conflicts', () => { + expect(findResolveButton().text()).toContain(resolveConflictsBtnText); + expect(findResolveButton().attributes('href')).toEqual(path); + }); + + it('should have merge buttons', () => { + expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText); + }); + }); + + describe('when user does not have permission to push to source branch', () => { + it('should show proper message', async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: false, + conflictsDocsPath: '', + }, }); - describe('when user does not have permission to push to source branch', () => { - it('should show proper message', async () => { - await createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: false, - conflictsDocsPath: '', - }, - }); + expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText); + }); - expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText); - }); + it('should not have action buttons', async () => { + await createComponent({ + mr: { + canMerge: false, + canPushToSourceBranch: false, + conflictsDocsPath: '', + }, + }); - it('should not have action buttons', async () => { - await createComponent({ - mr: { - canMerge: false, - canPushToSourceBranch: false, - conflictsDocsPath: '', - }, - }); - - expect(findResolveButton().exists()).toBe(false); - expect(findMergeLocalButton().exists()).toBe(false); - }); - - it('should not have resolve button when no conflict resolution path', async () => { - await createComponent({ - mr: { - canMerge: true, - conflictResolutionPath: null, - conflictsDocsPath: '', - }, - }); + expect(findResolveButton().exists()).toBe(false); + expect(findMergeLocalButton().exists()).toBe(false); + }); - expect(findResolveButton().exists()).toBe(false); - }); + it('should not have resolve button when no conflict resolution path', async () => { + await createComponent({ + mr: { + canMerge: true, + conflictResolutionPath: null, + conflictsDocsPath: '', + }, }); - describe('when fast-forward or semi-linear merge enabled', () => { - it('should tell you to rebase locally', async () => { - await createComponent({ - mr: { - shouldBeRebased: true, - conflictsDocsPath: '', - }, - }); + expect(findResolveButton().exists()).toBe(false); + }); + }); - expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText); - }); + describe('when fast-forward or semi-linear merge enabled', () => { + it('should tell you to rebase locally', async () => { + await createComponent({ + mr: { + shouldBeRebased: true, + conflictsDocsPath: '', + }, }); - describe('when source branch protected', () => { - beforeEach(async () => { - await createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: TEST_HOST, - sourceBranchProtected: true, - conflictsDocsPath: '', - }, - }); - }); + expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText); + }); + }); - it('should allow you to resolve the conflicts', () => { - expect(findResolveButton().exists()).toBe(true); - }); + describe('when source branch protected', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: TEST_HOST, + sourceBranchProtected: true, + conflictsDocsPath: '', + }, }); + }); - describe('when source branch not protected', () => { - beforeEach(async () => { - await createComponent({ - mr: { - canMerge: true, - canPushToSourceBranch: true, - conflictResolutionPath: TEST_HOST, - sourceBranchProtected: false, - conflictsDocsPath: '', - }, - }); - }); + it('should not allow you to resolve the conflicts', () => { + expect(findResolveButton().exists()).toBe(false); + }); + }); - it('should allow you to resolve the conflicts', () => { - expect(findResolveButton().text()).toContain(resolveConflictsBtnText); - expect(findResolveButton().attributes('href')).toEqual(TEST_HOST); - }); + describe('when source branch not protected', () => { + beforeEach(async () => { + await createComponent({ + mr: { + canMerge: true, + canPushToSourceBranch: true, + conflictResolutionPath: TEST_HOST, + sourceBranchProtected: false, + conflictsDocsPath: '', + }, }); }); + + it('should allow you to resolve the conflicts', () => { + expect(findResolveButton().text()).toContain(resolveConflictsBtnText); + expect(findResolveButton().attributes('href')).toEqual(TEST_HOST); + }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js index ddce07954ab..f29cf55f7ce 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js @@ -1,26 +1,17 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue'; let wrapper; -async function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) { +function factory(sourceBranchRemoved) { wrapper = shallowMount(MissingBranchComponent, { propsData: { mr: { sourceBranchRemoved }, }, - provide: { - glFeatures: { mergeRequestWidgetGraphql }, + data() { + return { state: { sourceBranchExists: !sourceBranchRemoved } }; }, }); - - if (mergeRequestWidgetGraphql) { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } }); - } - - await nextTick(); } describe('MRWidgetMissingBranch', () => { @@ -28,22 +19,16 @@ describe('MRWidgetMissingBranch', () => { wrapper.destroy(); }); - [true, false].forEach((mergeRequestWidgetGraphql) => { - describe(`widget GraphQL feature flag is ${ - mergeRequestWidgetGraphql ? 'enabled' : 'disabled' - }`, () => { - it.each` - sourceBranchRemoved | branchName - ${true} | ${'source'} - ${false} | ${'target'} - `( - 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved', - async ({ sourceBranchRemoved, branchName }) => { - await factory(sourceBranchRemoved, mergeRequestWidgetGraphql); + it.each` + sourceBranchRemoved | branchName + ${true} | ${'source'} + ${false} | ${'target'} + `( + 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved', + ({ sourceBranchRemoved, branchName }) => { + factory(sourceBranchRemoved); - expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName); - }, - ); - }); - }); + expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName); + }, + ); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js index 48d3f15560b..407bd60b2b7 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -60,6 +60,11 @@ const createTestMr = (customConfig) => { translateStateToMachine: () => this.transitionStateMachine(), state: 'open', canMerge: true, + mergeable: true, + userPermissions: { + removeSourceBranch: true, + canMerge: true, + }, }; Object.assign(mr, customConfig.mr); @@ -68,7 +73,7 @@ const createTestMr = (customConfig) => { }; const createTestService = () => ({ - merge: jest.fn(), + merge: jest.fn().mockResolvedValue(), poll: jest.fn().mockResolvedValue(), }); @@ -87,21 +92,24 @@ const createReadyToMergeResponse = (customMr) => { }); }; -const createComponent = ( - customConfig = {}, - mergeRequestWidgetGraphql = false, - restructuredMrWidget = true, -) => { +const createComponent = (customConfig = {}, createState = true) => { wrapper = shallowMount(ReadyToMerge, { propsData: { mr: createTestMr(customConfig), service: createTestService(), }, - provide: { - glFeatures: { - mergeRequestWidgetGraphql, - restructuredMrWidget, - }, + data() { + if (createState) { + return { + loading: false, + state: { + ...createTestMr(customConfig), + }, + }; + } + return { + loading: true, + }; }, stubs: { CommitEdit, @@ -136,7 +144,7 @@ describe('ReadyToMerge', () => { describe('computed', () => { describe('isAutoMergeAvailable', () => { it('should return true when at least one merge strategy is available', () => { - createComponent(); + createComponent({}); expect(wrapper.vm.isAutoMergeAvailable).toBe(true); }); @@ -168,14 +176,14 @@ describe('ReadyToMerge', () => { }); it('returns pending when pipeline is active', () => { - createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); + createComponent({ mr: { pipeline: { active: true }, isPipelineActive: true } }); expect(wrapper.vm.status).toEqual('pending'); }); it('returns failed when pipeline is failed', () => { createComponent({ - mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] }, + mr: { pipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true }, }); expect(wrapper.vm.status).toEqual('failed'); @@ -185,7 +193,7 @@ describe('ReadyToMerge', () => { describe('Merge Button Variant', () => { it('defaults to confirm class', () => { createComponent({ - mr: { availableAutoMergeStrategies: [] }, + mr: { availableAutoMergeStrategies: [], mergeable: true }, }); expect(findMergeButton().attributes('variant')).toBe('confirm'); @@ -194,19 +202,19 @@ describe('ReadyToMerge', () => { describe('status icon', () => { it('defaults to tick icon', () => { - createComponent(); + createComponent({ mr: { mergeable: true } }); expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for success status', () => { - createComponent({ mr: { pipeline: true } }); + createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } }); expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for pending status', () => { - createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); + createComponent({ mr: { pipeline: { active: true }, mergeable: true } }); expect(wrapper.vm.iconClass).toEqual('success'); }); @@ -266,7 +274,7 @@ describe('ReadyToMerge', () => { describe('isMergeButtonDisabled', () => { it('should return false with initial data', () => { - createComponent({ mr: { isMergeAllowed: true } }); + createComponent({ mr: { isMergeAllowed: true, mergeable: false } }); expect(wrapper.vm.isMergeButtonDisabled).toBe(false); }); @@ -283,6 +291,7 @@ describe('ReadyToMerge', () => { isMergeAllowed: false, availableAutoMergeStrategies: [], onlyAllowMergeIfPipelineSucceeds: true, + mergeable: false, }, }); @@ -544,7 +553,15 @@ describe('ReadyToMerge', () => { describe('Remove source branch checkbox', () => { describe('when user can merge but cannot delete branch', () => { it('should be disabled in the rendered output', () => { - createComponent(); + createComponent({ + mr: { + mergeable: true, + userPermissions: { + removeSourceBranch: false, + canMerge: true, + }, + }, + }); expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false); }); @@ -553,7 +570,7 @@ describe('ReadyToMerge', () => { describe('when user can merge and can delete branch', () => { beforeEach(() => { createComponent({ - mr: { canRemoveSourceBranch: true }, + mr: { canRemoveSourceBranch: true, mergeable: true }, }); }); @@ -567,7 +584,7 @@ describe('ReadyToMerge', () => { describe('squash checkbox', () => { it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { createComponent({ - mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + mr: { commitsCount: 2, enableSquashBeforeMerge: true, mergeable: true }, }); expect(findCheckboxElement().exists()).toBe(true); @@ -665,6 +682,7 @@ describe('ReadyToMerge', () => { squashIsSelected: true, enableSquashBeforeMerge: true, commitsCount: 2, + mergeRequestsFfOnlyEnabled: true, }, }); @@ -795,7 +813,9 @@ describe('ReadyToMerge', () => { }); it('shows the diverged commits text when the source branch is behind the target', () => { - createComponent({ mr: { divergedCommitsCount: 9001, canMerge: false } }); + createComponent({ + mr: { divergedCommitsCount: 9001, userPermissions: { canMerge: false }, canMerge: false }, + }); expect(wrapper.text()).toEqual( expect.stringContaining('The source branch is 9001 commits behind the target branch'), @@ -807,7 +827,7 @@ describe('ReadyToMerge', () => { describe('Merge button when pipeline has failed', () => { beforeEach(() => { createComponent({ - mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] }, + mr: { headPipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true }, }); }); @@ -830,7 +850,7 @@ describe('ReadyToMerge', () => { const USER_COMMIT_MESSAGE = 'Merge message provided manually by user'; const createDefaultGqlComponent = () => - createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, true); + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, false); beforeEach(() => { readyToMergeResponseSpy = jest diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js index 7259f210b6e..82aeac1a47d 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js @@ -1,101 +1,42 @@ -import Vue, { nextTick } from 'vue'; -import waitForPromises from 'helpers/wait_for_promises'; +import { mount } from '@vue/test-utils'; import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue'; -import toast from '~/vue_shared/plugins/global_toast'; -import eventHub from '~/vue_merge_request_widget/event_hub'; -jest.mock('~/vue_shared/plugins/global_toast'); - -const createComponent = () => { - const Component = Vue.extend(WorkInProgress); - const mr = { - title: 'The best MR ever', - removeWIPPath: '/path/to/remove/wip', - }; - const service = { - removeWIP() {}, - }; - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, +let wrapper; + +const createComponent = (updateMergeRequest = true) => { + wrapper = mount(WorkInProgress, { + propsData: { + mr: {}, + }, + data() { + return { + userPermissions: { + updateMergeRequest, + }, + }; + }, }); }; -describe('Wip', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = WorkInProgress.props; - - expect(mr.type instanceof Object).toBe(true); - expect(mr.required).toBe(true); - - expect(service.type instanceof Object).toBe(true); - expect(service.required).toBe(true); - }); - }); - - describe('data', () => { - it('should have default data', () => { - const vm = createComponent(); - - expect(vm.isMakingRequest).toBe(false); - }); - }); - - describe('methods', () => { - const mrObj = { - is_new_mr_data: true, - }; - - describe('handleRemoveDraft', () => { - it('should make a request to service and handle response', async () => { - const vm = createComponent(); - - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'removeWIP').mockReturnValue( - new Promise((resolve) => { - resolve({ - data: mrObj, - }); - }), - ); - - vm.handleRemoveDraft(); - - await waitForPromises(); - - expect(vm.isMakingRequest).toBe(true); - expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); - expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.'); - }); - }); +describe('Merge request widget draft state component', () => { + afterEach(() => { + wrapper.destroy(); }); describe('template', () => { - let vm; - let el; - - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBe(true); - expect(el.innerText).toContain( + createComponent(true); + + expect(wrapper.text()).toContain( "Merge blocked: merge request must be marked as ready. It's still marked as draft.", ); - expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain( - 'Mark as ready', - ); + expect(wrapper.find('[data-testid="removeWipButton"]').text()).toContain('Mark as ready'); }); - it('should not show removeWIP button is user cannot update MR', async () => { - vm.mr.removeWIPPath = ''; - - await nextTick(); + it('should not show removeWIP button is user cannot update MR', () => { + createComponent(false); - expect(el.querySelector('.js-remove-draft')).toBeNull(); + expect(wrapper.find('[data-testid="removeWipButton"]').exists()).toBe(false); }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap index bf50ae42794..e9a34453930 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = ` -"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\"> +"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\" actionbuttons=\\"\\"> <div class=\\"gl-display-flex gl-flex-direction-column\\"> <div> <p class=\\"gl-mb-0\\">Main text for the row</p> @@ -15,7 +15,7 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render </div> <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\"> <li> - <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" data-qa-selector=\\"child_content\\"> + <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" actionbuttons=\\"\\" data-qa-selector=\\"child_content\\"> <div class=\\"gl-display-flex gl-flex-direction-column\\"> <div> <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p> diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js index 606f7696694..e4bee6b8652 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js @@ -1,6 +1,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue'; import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue'; +import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => { @@ -8,6 +9,7 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', const findStatusIcon = () => wrapper.findComponent(StatusIcon); const findHelpPopover = () => wrapper.findComponent(HelpPopover); + const findActionButtons = () => wrapper.findComponent(ActionButtons); const createComponent = ({ propsData, slots } = {}) => { wrapper = shallowMountExtended(WidgetContentRow, { @@ -84,5 +86,17 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', createComponent({}); expect(findHelpPopover().exists()).toBe(false); }); + + it('does not display action buttons if actionButtons is not provided', () => { + createComponent({}); + expect(findActionButtons().exists()).toBe(false); + }); + + it('does display action buttons if actionButtons is provided', () => { + const actionButtons = [{ text: 'click-me', href: '#' }]; + + createComponent({ propsData: { actionButtons } }); + expect(findActionButtons().props('tertiaryButtons')).toEqual(actionButtons); + }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js index 02454af7242..0f4637d18d9 100644 --- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js @@ -4,6 +4,8 @@ import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import * as Sentry from '@sentry/browser'; +import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json'; +import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data'; @@ -22,6 +24,10 @@ import eventHub from '~/vue_merge_request_widget/event_hub'; import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; +import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql'; +import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql'; +import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql'; +import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; import mockData from './mock_data'; import { @@ -83,7 +89,39 @@ describe('MrWidgetOptions', () => { propsData: { mrData: { ...mrData }, }, + data() { + return { loading: false }; + }, + ...options, + apolloProvider: createMockApollo([ + [ + getStateQuery, + jest.fn().mockResolvedValue({ + data: { + project: { + ...getStateQueryResponse.data.project, + mergeRequest: { + ...getStateQueryResponse.data.project.mergeRequest, + mergeError: mrData.mergeError || null, + }, + }, + }, + }), + ], + [readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)], + [ + userPermissionsQuery, + jest.fn().mockResolvedValue({ + data: { project: { mergeRequest: { userPermissions: {} } } }, + }), + ], + [ + conflictsStateQuery, + jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }), + ], + ...(options.apolloMock || []), + ]), }); return axios.waitForAll(); @@ -769,12 +807,12 @@ describe('MrWidgetOptions', () => { mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]); return createComponent(mrData, { - apolloProvider: createMockApollo([ + apolloMock: [ [ securityReportMergeRequestDownloadPathsQuery, async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }), ], - ]), + ], }); }; @@ -837,8 +875,10 @@ describe('MrWidgetOptions', () => { ${'closed'} | ${false} | ${'hides'} ${'merged'} | ${true} | ${'shows'} ${'open'} | ${true} | ${'shows'} - `('$showText merge error when state is $state', ({ state, show }) => { - createComponent({ ...mockData, state, merge_error: 'Error!' }); + `('$showText merge error when state is $state', async ({ state, show }) => { + createComponent({ ...mockData, state, mergeError: 'Error!' }); + + await waitForPromises(); expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show); }); @@ -1069,7 +1109,7 @@ describe('MrWidgetOptions', () => { await nextTick(); await waitForPromises(); - expect(Sentry.captureException).toHaveBeenCalledTimes(1); + expect(Sentry.captureException).toHaveBeenCalledTimes(2); expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error')); expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed'); }); diff --git a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js index 3cdb4265ef0..37df041210c 100644 --- a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js @@ -21,22 +21,9 @@ describe('MergeRequestStore', () => { }); describe('setData', () => { - it('should set isSHAMismatch when the diff SHA changes', () => { - store.setData({ ...mockData, diff_head_sha: 'a-different-string' }); - - expect(store.isSHAMismatch).toBe(true); - }); - - it('should not set isSHAMismatch when other data changes', () => { - store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); - - expect(store.isSHAMismatch).toBe(false); - }); - it('should update cached sha after rebasing', () => { store.setData({ ...mockData, diff_head_sha: 'abc123' }, true); - expect(store.isSHAMismatch).toBe(false); expect(store.sha).toBe('abc123'); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js index cfd493663b7..266782a7478 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js @@ -2,6 +2,7 @@ import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/uti import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker'; import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker'; import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker'; +import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker'; import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies'; import { PACKAGE_JSON_FILE_TYPE, @@ -9,12 +10,14 @@ import { GEMSPEC_FILE_TYPE, GODEPS_JSON_FILE_TYPE, GEMFILE_FILE_TYPE, + PODSPEC_JSON_FILE_TYPE, } from './mock_data'; jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker'); jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker'); jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker'); jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker'); +jest.mock('~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker'); describe('Highlight.js plugin for linking dependencies', () => { const hljsResultMock = { value: 'test' }; @@ -38,4 +41,9 @@ describe('Highlight.js plugin for linking dependencies', () => { linkDependencies(hljsResultMock, GEMFILE_FILE_TYPE); expect(gemfileLinker).toHaveBeenCalled(); }); + + it('calls podspecJsonLinker for podspec_json file types', () => { + linkDependencies(hljsResultMock, PODSPEC_JSON_FILE_TYPE); + expect(podspecJsonLinker).toHaveBeenCalled(); + }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js index 4f390cebd37..8f3ff9a15bb 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js @@ -6,3 +6,20 @@ export const GEMSPEC_FILE_TYPE = 'gemspec'; export const GODEPS_JSON_FILE_TYPE = 'godeps_json'; export const GEMFILE_FILE_TYPE = 'gemfile'; + +export const PODSPEC_JSON_FILE_TYPE = 'podspec_json'; + +export const PODSPEC_JSON_CONTENT = `{ + "dependencies": { + "MyCheckCore": [ + ] + }, + "subspecs": [ + { + "dependencies": { + "AFNetworking/Security": [ + ] + } + } + ] + }`; diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js index e1dbdf8a87d..66e2020da27 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js @@ -1,7 +1,9 @@ import { createLink, generateHLJSOpenTag, + getObjectKeysByKeyName, } from '~/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util'; +import { PODSPEC_JSON_CONTENT } from '../mock_data'; describe('createLink', () => { it('generates a link with the correct attributes', () => { @@ -32,3 +34,11 @@ describe('generateHLJSOpenTag', () => { expect(generateHLJSOpenTag(type)).toBe(result); }); }); + +describe('getObjectKeysByKeyName method', () => { + it('gets all object keys within specified key', () => { + const acc = []; + const keys = getObjectKeysByKeyName(JSON.parse(PODSPEC_JSON_CONTENT), 'dependencies', acc); + expect(keys).toEqual(['MyCheckCore', 'AFNetworking/Security']); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js new file mode 100644 index 00000000000..0ef63de68c6 --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js @@ -0,0 +1,14 @@ +import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker'; +import { PODSPEC_JSON_CONTENT } from '../mock_data'; + +describe('Highlight.js plugin for linking podspec_json dependencies', () => { + it('mutates the input value by wrapping dependency names in anchors', () => { + const inputValue = + '<span class="hljs-attr">"AFNetworking/Security"</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">['; + const outputValue = + '<span class="hljs-attr">"<a href="https://cocoapods.org/pods/AFNetworking" target="_blank" rel="nofollow noreferrer noopener">AFNetworking/Security</a>"</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">['; + const hljsResultMock = { value: inputValue }; + const output = podspecJsonLinker(hljsResultMock, PODSPEC_JSON_CONTENT); + expect(output).toBe(outputValue); + }); +}); diff --git a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb index 3f8ba5955d5..2eccfd3409f 100644 --- a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb +++ b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb @@ -19,8 +19,9 @@ RSpec.describe Mutations::Ci::Runner::BulkDelete do end context 'when user can delete runners' do + let_it_be(:group) { create(:group) } + let(:user) { admin_user } - let(:group) { create(:group) } let!(:runners) do create_list(:ci_runner, 2, :group, groups: [group]) end diff --git a/spec/graphql/types/issue_type_enum_spec.rb b/spec/graphql/types/issue_type_enum_spec.rb index d462c26c6ac..cd1737c3ebb 100644 --- a/spec/graphql/types/issue_type_enum_spec.rb +++ b/spec/graphql/types/issue_type_enum_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe Types::IssueTypeEnum do specify { expect(described_class.graphql_name).to eq('IssueType') } - it 'exposes all the existing issue type values except objective & key_result' do + it 'exposes all the existing issue type values except key_result' do expect(described_class.values.keys).to match_array( - %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK] + %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE] ) end end diff --git a/spec/helpers/groups/observability_helper_spec.rb b/spec/helpers/groups/observability_helper_spec.rb new file mode 100644 index 00000000000..4393f4e9bec --- /dev/null +++ b/spec/helpers/groups/observability_helper_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Groups::ObservabilityHelper do + let(:group) { build_stubbed(:group) } + let(:observability_url) { Gitlab::Observability.observability_url } + + describe '#observability_iframe_src' do + context 'if observability_path is missing from params' do + it 'returns the iframe src for action: dashboards' do + allow(helper).to receive(:params).and_return({ action: 'dashboards' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/") + end + + it 'returns the iframe src for action: manage' do + allow(helper).to receive(:params).and_return({ action: 'manage' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/dashboards") + end + + it 'returns the iframe src for action: explore' do + allow(helper).to receive(:params).and_return({ action: 'explore' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/explore") + end + end + + context 'if observability_path exists in params' do + context 'if observability_path is valid' do + it 'returns the iframe src by injecting the observability path' do + allow(helper).to receive(:params).and_return({ action: '/explore', observability_path: '/foo?bar=foobar' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/foo?bar=foobar") + end + end + + context 'if observability_path is not valid' do + it 'returns the iframe src by injecting the sanitised observability path' do + allow(helper).to receive(:params).and_return({ + action: '/explore', + observability_path: + "/test?groupId=<script>alert('attack!')</script>" + }) + expect(helper.observability_iframe_src(group)).to eq( + "#{observability_url}/#{group.id}/test?groupId=alert('attack!')" + ) + end + end + end + + context 'when observability ui is standalone' do + before do + stub_env('STANDALONE_OBSERVABILITY_UI', 'true') + end + + it 'returns the iframe src without group.id for action: dashboards' do + allow(helper).to receive(:params).and_return({ action: 'dashboards' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/") + end + + it 'returns the iframe src without group.id for action: manage' do + allow(helper).to receive(:params).and_return({ action: 'manage' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/dashboards") + end + + it 'returns the iframe src without group.id for action: explore' do + allow(helper).to receive(:params).and_return({ action: 'explore' }) + expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/explore") + end + end + end + + describe '#observability_page_title' do + it 'returns the title for action: dashboards' do + allow(helper).to receive(:params).and_return({ action: 'dashboards' }) + expect(helper.observability_page_title).to eq("Dashboards") + end + + it 'returns the title for action: manage' do + allow(helper).to receive(:params).and_return({ action: 'manage' }) + expect(helper.observability_page_title).to eq("Manage Dashboards") + end + + it 'returns the title for action: explore' do + allow(helper).to receive(:params).and_return({ action: 'explore' }) + expect(helper.observability_page_title).to eq("Explore") + end + + it 'returns the default title for unknown action' do + allow(helper).to receive(:params).and_return({ action: 'unknown' }) + expect(helper.observability_page_title).to eq("Dashboards") + end + end +end diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb index 5eea78acd98..45becb8370c 100644 --- a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb +++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb @@ -3,38 +3,55 @@ require 'spec_helper' RSpec.describe Gitlab::Cluster::LifecycleEvents do + using RSpec::Parameterized::TableSyntax + # we create a new instance to ensure that we do not touch existing hooks let(:replica) { Class.new(described_class) } - context 'hooks execution' do - using RSpec::Parameterized::TableSyntax + before do + # disable blackout period to speed-up tests + stub_config(shutdown: { blackout_seconds: 0 }) + end - where(:method, :hook_names) do - :do_worker_start | %i[worker_start_hooks] - :do_before_fork | %i[before_fork_hooks] - :do_before_graceful_shutdown | %i[master_blackout_period master_graceful_shutdown] - :do_before_master_restart | %i[master_restart_hooks] + context 'outside of clustered environments' do + where(:hook, :was_executed_immediately) do + :on_worker_start | true + :on_before_fork | false + :on_before_graceful_shutdown | false + :on_before_master_restart | false + :on_worker_stop | false end - before do - # disable blackout period to speed-up tests - stub_config(shutdown: { blackout_seconds: 0 }) + with_them do + it 'executes the given block immediately' do + was_executed = false + replica.public_send(hook, &proc { was_executed = true }) + + expect(was_executed).to eq(was_executed_immediately) + end end + end - with_them do - subject { replica.public_send(method) } + context 'in clustered environments' do + before do + allow(Gitlab::Runtime).to receive(:puma?).and_return(true) + replica.set_puma_options(workers: 2) + end - it 'executes all hooks' do - hook_names.each do |hook_name| - hook = double - replica.instance_variable_set(:"@#{hook_name}", [hook]) + where(:hook, :execution_helper) do + :on_worker_start | :do_worker_start + :on_before_fork | :do_before_fork + :on_before_graceful_shutdown | :do_before_graceful_shutdown + :on_before_master_restart | :do_before_master_restart + :on_worker_stop | :do_worker_stop + end - # ensure that proper hooks are called - expect(hook).to receive(:call) - expect(replica).to receive(:call).with(hook_name, anything).and_call_original - end + with_them do + it 'requires explicit execution via do_* helper' do + was_executed = false + replica.public_send(hook, &proc { was_executed = true }) - subject + expect { replica.public_send(execution_helper) }.to change { was_executed }.from(false).to(true) end end end diff --git a/spec/lib/gitlab/experimentation/group_types_spec.rb b/spec/lib/gitlab/experimentation/group_types_spec.rb deleted file mode 100644 index 2b118d76fa4..00000000000 --- a/spec/lib/gitlab/experimentation/group_types_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Experimentation::GroupTypes do - it 'defines a GROUP_CONTROL constant' do - expect(described_class.const_defined?(:GROUP_CONTROL)).to be_truthy - end - - it 'defines a GROUP_EXPERIMENTAL constant' do - expect(described_class.const_defined?(:GROUP_EXPERIMENTAL)).to be_truthy - end -end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 7c84c737c00..17f802b9f66 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -239,7 +239,7 @@ RSpec.describe Gitlab::Git::Tree do let(:pagination_params) { { limit: 5, page_token: 'aabbccdd' } } it 'raises a command error' do - expect { entries }.to raise_error(Gitlab::Git::CommandError, 'could not find starting OID: aabbccdd') + expect { entries }.to raise_error(Gitlab::Git::CommandError, /could not find starting OID: aabbccdd/) end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 18d4b33f2b7..e9dde1c6180 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -552,6 +552,7 @@ project: - path_locks - approver_groups - repository_state +- wiki_repository - wiki_repository_state - source_pipelines - sourced_pipelines diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb index e54356d94c1..cbfab7e8884 100644 --- a/spec/lib/gitlab/json_spec.rb +++ b/spec/lib/gitlab/json_spec.rb @@ -468,6 +468,21 @@ RSpec.describe Gitlab::Json do expect(new_result).to eq(original_result) end + + it "behaves the same when processing invalid unicode data" do + invalid_obj = { test: "Gr\x80\x81e" } + default_encoder = ActiveSupport::JSON::Encoding::JSONGemEncoder + + original_result = ActiveSupport::JSON::Encoding.use_encoder(default_encoder) do + expect { ActiveSupport::JSON.encode(invalid_obj) }.to raise_error(JSON::GeneratorError) + end + + new_result = ActiveSupport::JSON::Encoding.use_encoder(described_class) do + expect { ActiveSupport::JSON.encode(invalid_obj) }.to raise_error(JSON::GeneratorError) + end + + expect(new_result).to eq(original_result) + end end end # rubocop: enable Gitlab/Json diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index 6aa89c7cb05..091f35bfbcc 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -24,47 +24,22 @@ RSpec.describe Gitlab::Metrics::MethodCall do allow(method_call).to receive(:above_threshold?).and_return(true) end - context 'prometheus instrumentation is enabled' do - before do - stub_feature_flags(prometheus_metrics_method_instrumentation: true) - end - - around do |example| - freeze_time do - example.run - end - end - - it 'metric is not a NullMetric' do - method_call.measure { 'foo' } - expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric) - end - - it 'observes the performance of the supplied block' do - expect(transaction) - .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo }) - - method_call.measure { 'foo' } + around do |example| + freeze_time do + example.run end end - context 'prometheus instrumentation is disabled' do - before do - stub_feature_flags(prometheus_metrics_method_instrumentation: false) - end - - it 'observes the performance of the supplied block' do - expect(transaction) - .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo }) - - method_call.measure { 'foo' } - end + it 'metric is not a NullMetric' do + method_call.measure { 'foo' } + expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric) + end - it 'observes using NullMetric' do - method_call.measure { 'foo' } + it 'observes the performance of the supplied block' do + expect(transaction) + .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo }) - expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).to be_instance_of(Gitlab::Metrics::NullMetric) - end + method_call.measure { 'foo' } end end diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb index 05cdc5bb79b..d42cef8bcba 100644 --- a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb +++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do it 'stores request id and enqueues stats job' do expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid) expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid) - expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + expect(client).to receive(:sadd?).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) expect(client).to receive(:expire).with(GitlabPerformanceBarStatsWorker::STATS_KEY, GitlabPerformanceBarStatsWorker::STATS_KEY_EXPIRE) peek_adapter.new(client).save('foo') @@ -56,7 +56,7 @@ RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do it 'stores request id but does not enqueue any job' do expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in) - expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + expect(client).to receive(:sadd?).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) peek_adapter.new(client).save('foo') end diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index c3b3d876779..6e3001b38b7 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -130,15 +130,13 @@ RSpec.describe Gitlab::Redis::MultiStore do primary_store.multi do |multi| multi.set(key1, value1) multi.set(key2, value2) - multi.sadd(skey, value1) - multi.sadd(skey, value2) + multi.sadd(skey, [value1, value2]) end secondary_store.multi do |multi| multi.set(key1, value1) multi.set(key2, value2) - multi.sadd(skey, value1) - multi.sadd(skey, value2) + multi.sadd(skey, [value1, value2]) end end @@ -332,8 +330,8 @@ RSpec.describe Gitlab::Redis::MultiStore do let_it_be(:skey) { "redis:set:key" } let_it_be(:svalues1) { [value2, value1] } let_it_be(:svalues2) { [value1] } - let_it_be(:skey_value1) { [skey, value1] } - let_it_be(:skey_value2) { [skey, value2] } + let_it_be(:skey_value1) { [skey, [value1]] } + let_it_be(:skey_value2) { [skey, [value2]] } let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) } where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do @@ -353,12 +351,12 @@ RSpec.describe Gitlab::Redis::MultiStore do primary_store.multi do |multi| multi.set(key2, value1) - multi.sadd(skey, value1) + multi.sadd?(skey, value1) end secondary_store.multi do |multi| multi.set(key2, value1) - multi.sadd(skey, value1) + multi.sadd?(skey, value1) end end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index c85a72be54c..3665f13015e 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -260,7 +260,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do redis.set("session:gitlab:#{rack_session.private_id}", '') redis.set(session_key, serialized_session) - redis.sadd(lookup_key, active_session_lookup_key) + redis.sadd?(lookup_key, active_session_lookup_key) end end @@ -338,7 +338,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do session_private_id = Rack::Session::SessionId.new(session_public_id).private_id active_session = ActiveSession.new(session_private_id: session_private_id) redis.set(key_name(user.id, session_private_id), dump_session(active_session)) - redis.sadd(lookup_key, session_private_id) + redis.sadd?(lookup_key, session_private_id) end # setup for unrelated user @@ -347,7 +347,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do active_session = ActiveSession.new(session_private_id: session_private_id) redis.set(key_name(unrelated_user_id, session_private_id), dump_session(active_session)) - redis.sadd(described_class.lookup_key_name(unrelated_user_id), session_private_id) + redis.sadd?(described_class.lookup_key_name(unrelated_user_id), session_private_id) end end @@ -372,7 +372,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do Gitlab::Redis::Sessions.with do |redis| redis.set(key_name(user.id, impersonated_session_id), dump_session(ActiveSession.new(session_id: Rack::Session::SessionId.new(impersonated_session_id), is_impersonated: true))) - redis.sadd(lookup_key, impersonated_session_id) + redis.sadd?(lookup_key, impersonated_session_id) end expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2) @@ -418,8 +418,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do it 'removes obsolete lookup entries' do Gitlab::Redis::Sessions.with do |redis| redis.set(session_key, '') - redis.sadd(lookup_key, current_session_id) - redis.sadd(lookup_key, '59822c7d9fcdfa03725eff41782ad97d') + redis.sadd(lookup_key, [current_session_id, '59822c7d9fcdfa03725eff41782ad97d']) end ActiveSession.cleanup(user) @@ -445,7 +444,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do key_name(user.id, number), dump_session(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago)) ) - redis.sadd(lookup_key, number.to_s) + redis.sadd?(lookup_key, number.to_s) end end end @@ -477,7 +476,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do it 'removes obsolete lookup entries even without active session' do Gitlab::Redis::Sessions.with do |redis| - redis.sadd(lookup_key, (max_number_of_sessions_plus_two + 1).to_s) + redis.sadd?(lookup_key, (max_number_of_sessions_plus_two + 1).to_s) end ActiveSession.cleanup(user) @@ -534,7 +533,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do key_name(user.id, number), dump_session(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago)) ) - redis.sadd(lookup_key, number.to_s) + redis.sadd?(lookup_key, number.to_s) end end end @@ -601,11 +600,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do dump_session(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago)) ) - redis.sadd(lookup_key, number.to_s) + redis.sadd?(lookup_key, number.to_s) end - redis.sadd(lookup_key, (active_count + 1).to_s) - redis.sadd(lookup_key, (active_count + 2).to_s) + redis.sadd?(lookup_key, [(active_count + 1).to_s, (active_count + 2).to_s]) end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e1bb1195813..e31298e489d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -25,6 +25,7 @@ RSpec.describe Ci::Build do it { is_expected.to have_many(:needs) } it { is_expected.to have_many(:sourced_pipelines) } + it { is_expected.to have_one(:sourced_pipeline) } it { is_expected.to have_many(:job_variables) } it { is_expected.to have_many(:report_results) } it { is_expected.to have_many(:pages_deployments) } @@ -2797,16 +2798,6 @@ RSpec.describe Ci::Build do expect(environment_based_variables_collection).to be_empty end - context 'when ci_job_jwt feature flag is disabled' do - before do - stub_feature_flags(ci_job_jwt: false) - end - - it 'CI_JOB_JWT is not included' do - expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') - end - end - context 'when CI_JOB_JWT generation fails' do [ OpenSSL::PKey::RSAError, diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 535999f211d..e62e5f84a6d 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -81,7 +81,7 @@ RSpec.describe Ci::Processable do commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason - sourced_pipelines artifacts_file_store artifacts_metadata_store + sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store metadata runner_session trace_chunks upstream_pipeline_id artifacts_file artifacts_metadata artifacts_size commands resource resource_group_id processed security_scans author diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb deleted file mode 100644 index dc740ce8b0f..00000000000 --- a/spec/models/experiment_spec.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Experiment do - subject { build(:experiment) } - - describe 'associations' do - it { is_expected.to have_many(:experiment_subjects) } - end - - describe 'validations' do - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } - it { is_expected.to validate_length_of(:name).is_at_most(255) } - end - - describe '#record_conversion_event_for_subject' do - let_it_be(:user) { create(:user) } - let_it_be(:experiment) { create(:experiment) } - let_it_be(:context) { { a: 42 } } - - subject(:record_conversion) { experiment.record_conversion_event_for_subject(user, context) } - - context 'when no existing experiment_subject record exists for the given user' do - it 'does not update or create an experiment_subject record' do - expect { record_conversion }.not_to change { ExperimentSubject.all.to_a } - end - end - - context 'when an existing experiment_subject exists for the given user' do - context 'but it has already been converted' do - let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago) } - - it 'does not update the converted_at value' do - expect { record_conversion }.not_to change { experiment_subject.converted_at } - end - end - - context 'and it has not yet been converted' do - let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) } - - it 'updates the converted_at value' do - expect { record_conversion }.to change { experiment_subject.reload.converted_at } - end - end - - context 'with no existing context' do - let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) } - - it 'updates the context' do - expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42) - end - end - - context 'with an existing context' do - let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago, context: { b: 1 }) } - - it 'merges the context' do - expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42, 'b' => 1) - end - end - end - end - - describe '#record_subject_and_variant!' do - let_it_be(:subject_to_record) { create(:group) } - let_it_be(:variant) { :control } - let_it_be(:experiment) { create(:experiment) } - - subject(:record_subject_and_variant!) { experiment.record_subject_and_variant!(subject_to_record, variant) } - - context 'when no existing experiment_subject record exists for the given subject' do - it 'creates an experiment_subject record' do - expect { record_subject_and_variant! }.to change(ExperimentSubject, :count).by(1) - expect(ExperimentSubject.last.variant).to eq(variant.to_s) - end - end - - context 'when an existing experiment_subject exists for the given subject' do - let_it_be(:experiment_subject) do - create(:experiment_subject, experiment: experiment, namespace: subject_to_record, user: nil, variant: :experimental) - end - - context 'when it belongs to the same variant' do - let(:variant) { :experimental } - - it 'does not initiate a transaction' do - expect(Experiment.connection).not_to receive(:transaction) - - subject - end - end - - context 'but it belonged to a different variant' do - it 'updates the variant value' do - expect { record_subject_and_variant! }.to change { experiment_subject.reload.variant }.to('control') - end - end - end - - describe 'providing a subject to record' do - context 'when given a group as subject' do - it 'saves the namespace as the experiment subject' do - expect(record_subject_and_variant!.namespace).to eq(subject_to_record) - end - end - - context 'when given a users namespace as subject' do - let_it_be(:subject_to_record) { build(:namespace) } - - it 'saves the namespace as the experiment_subject' do - expect(record_subject_and_variant!.namespace).to eq(subject_to_record) - end - end - - context 'when given a user as subject' do - let_it_be(:subject_to_record) { build(:user) } - - it 'saves the user as experiment_subject user' do - expect(record_subject_and_variant!.user).to eq(subject_to_record) - end - end - - context 'when given a project as subject' do - let_it_be(:subject_to_record) { build(:project) } - - it 'saves the project as experiment_subject user' do - expect(record_subject_and_variant!.project).to eq(subject_to_record) - end - end - - context 'when given no subject' do - let_it_be(:subject_to_record) { nil } - - it 'raises an error' do - expect { record_subject_and_variant! }.to raise_error('Incompatible subject provided!') - end - end - - context 'when given an incompatible subject' do - let_it_be(:subject_to_record) { build(:ci_build) } - - it 'raises an error' do - expect { record_subject_and_variant! }.to raise_error('Incompatible subject provided!') - end - end - end - end -end diff --git a/spec/models/experiment_subject_spec.rb b/spec/models/experiment_subject_spec.rb deleted file mode 100644 index d86dc3cbf65..00000000000 --- a/spec/models/experiment_subject_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ExperimentSubject, type: :model do - describe 'associations' do - it { is_expected.to belong_to(:experiment) } - it { is_expected.to belong_to(:user) } - it { is_expected.to belong_to(:namespace) } - it { is_expected.to belong_to(:project) } - end - - describe 'validations' do - it { is_expected.to validate_presence_of(:experiment) } - - describe 'must_have_one_subject_present' do - let(:experiment_subject) { build(:experiment_subject, user: nil, namespace: nil, project: nil) } - let(:error_message) { 'Must have exactly one of User, Namespace, or Project.' } - - it 'fails when no subject is present' do - expect(experiment_subject).not_to be_valid - expect(experiment_subject.errors[:base]).to include(error_message) - end - - it 'passes when user subject is present' do - experiment_subject.user = build(:user) - expect(experiment_subject).to be_valid - end - - it 'passes when namespace subject is present' do - experiment_subject.namespace = build(:group) - expect(experiment_subject).to be_valid - end - - it 'passes when project subject is present' do - experiment_subject.project = build(:project) - expect(experiment_subject).to be_valid - end - - it 'fails when more than one subject is present', :aggregate_failures do - # two subjects - experiment_subject.user = build(:user) - experiment_subject.namespace = build(:group) - expect(experiment_subject).not_to be_valid - expect(experiment_subject.errors[:base]).to include(error_message) - - # three subjects - experiment_subject.project = build(:project) - expect(experiment_subject).not_to be_valid - expect(experiment_subject.errors[:base]).to include(error_message) - end - end - end - - describe '.valid_subject?' do - subject(:valid_subject?) { described_class.valid_subject?(subject_class.new) } - - context 'when passing a Group, Namespace, User or Project' do - [Group, Namespace, User, Project].each do |subject_class| - let(:subject_class) { subject_class } - - it { is_expected.to be(true) } - end - end - - context 'when passing another object' do - let(:subject_class) { Issue } - - it { is_expected.to be(false) } - end - end -end diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb index ac8ea52dd3e..1d2c90dad51 100644 --- a/spec/models/integrations/bamboo_spec.rb +++ b/spec/models/integrations/bamboo_spec.rb @@ -23,6 +23,8 @@ RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do ) end + it_behaves_like Integrations::BaseCi + it_behaves_like Integrations::ResetSecretFields include_context Integrations::EnableSslVerification diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb index 3875f0edec5..b959ead2cae 100644 --- a/spec/models/integrations/base_chat_notification_spec.rb +++ b/spec/models/integrations/base_chat_notification_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' RSpec.describe Integrations::BaseChatNotification do + describe 'default values' do + it { expect(subject.category).to eq(:chat) } + end + describe 'validations' do before do allow(subject).to receive(:activated?).and_return(true) diff --git a/spec/models/integrations/base_issue_tracker_spec.rb b/spec/models/integrations/base_issue_tracker_spec.rb index 37f7d99717c..e1a764cd7cb 100644 --- a/spec/models/integrations/base_issue_tracker_spec.rb +++ b/spec/models/integrations/base_issue_tracker_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Integrations::BaseIssueTracker do let_it_be_with_refind(:project) { create :project } + describe 'default values' do + it { expect(subject.category).to eq(:issue_tracker) } + end + describe 'Validations' do describe 'only one issue tracker per project' do before do diff --git a/spec/models/integrations/base_third_party_wiki_spec.rb b/spec/models/integrations/base_third_party_wiki_spec.rb index 11e044c2a18..dbead636cb9 100644 --- a/spec/models/integrations/base_third_party_wiki_spec.rb +++ b/spec/models/integrations/base_third_party_wiki_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' RSpec.describe Integrations::BaseThirdPartyWiki do + describe 'default values' do + it { expect(subject.category).to eq(:third_party_wiki) } + end + describe 'Validations' do let_it_be_with_reload(:project) { create(:project) } diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb index 5535a13db7f..5f62c68bd2b 100644 --- a/spec/models/integrations/buildkite_spec.rb +++ b/spec/models/integrations/buildkite_spec.rb @@ -18,6 +18,8 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do ) end + it_behaves_like Integrations::BaseCi + it_behaves_like Integrations::ResetSecretFields it_behaves_like Integrations::HasWebHook do diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb index 59961b37c20..6ff6888e0d3 100644 --- a/spec/models/integrations/drone_ci_spec.rb +++ b/spec/models/integrations/drone_ci_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do let_it_be(:project) { create(:project, :repository, name: 'project') } + it_behaves_like Integrations::BaseCi + it_behaves_like Integrations::ResetSecretFields do let(:integration) { subject } end diff --git a/spec/models/integrations/jenkins_spec.rb b/spec/models/integrations/jenkins_spec.rb index 4e787f958af..0264982f0dc 100644 --- a/spec/models/integrations/jenkins_spec.rb +++ b/spec/models/integrations/jenkins_spec.rb @@ -23,6 +23,8 @@ RSpec.describe Integrations::Jenkins do } end + it_behaves_like Integrations::BaseCi + it_behaves_like Integrations::ResetSecretFields do let(:integration) { jenkins_integration } end diff --git a/spec/models/integrations/mock_ci_spec.rb b/spec/models/integrations/mock_ci_spec.rb index d29c63b3a97..83954812bfe 100644 --- a/spec/models/integrations/mock_ci_spec.rb +++ b/spec/models/integrations/mock_ci_spec.rb @@ -7,6 +7,8 @@ RSpec.describe Integrations::MockCi do subject(:integration) { described_class.new(project: project, mock_service_url: generate(:url)) } + it_behaves_like Integrations::BaseCi + include_context Integrations::EnableSslVerification describe '#commit_status' do diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb index f2b718a98d8..3c3850854b3 100644 --- a/spec/models/integrations/prometheus_spec.rb +++ b/spec/models/integrations/prometheus_spec.rb @@ -12,6 +12,8 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, let(:integration) { project.prometheus_integration } + it_behaves_like Integrations::BaseMonitoring + context 'redirects' do it 'does not follow redirects' do redirect_to = 'https://redirected.example.com' diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb index 5160b410514..e32088a2f79 100644 --- a/spec/models/integrations/teamcity_spec.rb +++ b/spec/models/integrations/teamcity_spec.rb @@ -22,6 +22,8 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do ) end + it_behaves_like Integrations::BaseCi + it_behaves_like Integrations::ResetSecretFields include_context Integrations::EnableSslVerification do diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb index f107a56c1b6..7e127caa649 100644 --- a/spec/models/merge_request_diff_file_spec.rb +++ b/spec/models/merge_request_diff_file_spec.rb @@ -203,16 +203,6 @@ RSpec.describe MergeRequestDiffFile do end end - context 'when externally_stored_diffs_caching_export feature flag is disabled' do - it 'calls #diff' do - stub_feature_flags(externally_stored_diffs_caching_export: false) - - expect(file).to receive(:diff) - - file.utf8_diff - end - end - context 'when diff is not stored externally' do it 'calls #diff' do expect(file).to receive(:diff) diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb index 5274526e81c..16894bf28f1 100644 --- a/spec/models/network/graph_spec.rb +++ b/spec/models/network/graph_spec.rb @@ -6,10 +6,24 @@ RSpec.describe Network::Graph do let(:project) { create(:project, :repository) } let!(:note_on_commit) { create(:note_on_commit, project: project) } - it '#initialize' do - graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil) + describe '#initialize' do + let(:graph) do + described_class.new(project, 'refs/heads/master', project.repository.commit, nil) + end + + it 'has initialized' do + expect(graph).to be_a(described_class) + end - expect(graph.notes).to eq({ note_on_commit.commit_id => 1 }) + context 'when disable_network_graph_note_counts is disabled' do + before do + stub_feature_flags(disable_network_graph_notes_count: false) + end + + it 'initializes the notes hash' do + expect(graph.notes).to eq({ note_on_commit.commit_id => 1 }) + end + end end describe '#commits' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 736e70d1efc..184500f3209 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -38,6 +38,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:hooks) } it { is_expected.to have_many(:protected_branches) } it { is_expected.to have_many(:exported_protected_branches) } + it { is_expected.to have_one(:wiki_repository).class_name('Projects::WikiRepository').inverse_of(:project) } it { is_expected.to have_one(:slack_integration) } it { is_expected.to have_one(:microsoft_teams_integration) } it { is_expected.to have_one(:mattermost_integration) } diff --git a/spec/models/projects/wiki_repository_spec.rb b/spec/models/projects/wiki_repository_spec.rb new file mode 100644 index 00000000000..6868e1f5fb9 --- /dev/null +++ b/spec/models/projects/wiki_repository_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::WikiRepository do + subject { described_class.new(project: build(:project)) } + + describe 'associations' do + it { is_expected.to belong_to(:project).inverse_of(:wiki_repository) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_uniqueness_of(:project) } + end +end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index d771c1e2dcc..ac8c4aacdf2 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -720,6 +720,16 @@ RSpec.describe API::MavenPackages do expect(response).to have_gitlab_http_status(:not_found) end + context 'with access to package registry for everyone' do + subject { download_file(file_name: package_file.file_name) } + + before do + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + end + + it_behaves_like 'successfully returning the file' + end + it_behaves_like 'downloads with a job token' it_behaves_like 'downloads with a deploy token' diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index bdcd6e7278d..373327787a2 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -5,16 +5,29 @@ require 'spec_helper' RSpec.describe API::NpmProjectPackages do include_context 'npm api setup' - describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do - it_behaves_like 'handling get metadata requests', scope: :project do - let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") } + shared_examples 'accept get request on private project with access to package registry for everyone' do + subject { get(url) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) end + + it_behaves_like 'returning response status', :ok + end + + describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do + let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") } + + it_behaves_like 'handling get metadata requests', scope: :project + it_behaves_like 'accept get request on private project with access to package registry for everyone' end describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do - it_behaves_like 'handling get dist tags requests', scope: :project do - let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") } - end + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") } + + it_behaves_like 'handling get dist tags requests', scope: :project + it_behaves_like 'accept get request on private project with access to package registry for everyone' end describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do @@ -108,6 +121,14 @@ RSpec.describe API::NpmProjectPackages do expect(response).to have_gitlab_http_status(:forbidden) end end + + context 'with access to package registry for everyone' do + before do + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + end + + it_behaves_like 'successfully downloads the file' + end end context 'internal project' do diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb index c6fb66506da..a08231fe939 100644 --- a/spec/requests/groups/observability_controller_spec.rb +++ b/spec/requests/groups/observability_controller_spec.rb @@ -9,13 +9,14 @@ RSpec.describe Groups::ObservabilityController do let_it_be(:user) { create(:user) } let(:observability_url) { Gitlab::Observability.observability_url } + let(:expected_observability_path) { "/" } - subject do - get group_observability_index_path(group) - response - end + shared_examples 'observability route request' do + subject do + get path + response + end - describe 'GET #index' do context 'when user is not authenticated' do it 'returns 404' do expect(subject).to have_gitlab_http_status(:not_found) @@ -57,98 +58,112 @@ RSpec.describe Groups::ObservabilityController do expect(subject).to render_template("layouts/fullscreen") expect(subject).not_to render_template('layouts/nav/breadcrumbs') expect(subject).to render_template("nav/sidebar/_group") + expect(subject).to render_template("groups/observability/observability") end - describe 'iframe' do - subject do - get group_observability_index_path(group) - Nokogiri::HTML.parse(response.body).at_css('iframe#observability-ui-iframe') - end + it 'renders the js-observability-app element correctly' do + element = Nokogiri::HTML.parse(subject.body).at_css('#js-observability-app') + expect(element.attributes['data-observability-iframe-src'].value).to eq(expected_observability_path) + end + end + end - it 'sets the iframe src to the proper URL' do - expected_url = "#{observability_url}/-/#{group.id}" + describe 'GET #dashboards' do + let(:path) { group_observability_dashboards_path(group) } + let(:expected_observability_path) { "#{observability_url}/#{group.id}/" } - expect(subject.attributes['src'].value).to eq(expected_url) - end + it_behaves_like 'observability route request' + end + + describe 'GET #manage' do + let(:path) { group_observability_manage_path(group) } + let(:expected_observability_path) { "#{observability_url}/#{group.id}/dashboards" } + + it_behaves_like 'observability route request' + end + + describe 'GET #explore' do + let(:path) { group_observability_explore_path(group) } + let(:expected_observability_path) { "#{observability_url}/#{group.id}/explore" } + + it_behaves_like 'observability route request' + end + + describe 'CSP' do + before do + setup_csp_for_controller(described_class, csp) + end + + subject do + get group_observability_dashboards_path(group) + response.headers['Content-Security-Policy'] + end + + context 'when there is no CSP config' do + let(:csp) { ActionDispatch::ContentSecurityPolicy.new } + + it 'does not add any csp header' do + expect(subject).to be_blank end + end - describe 'CSP' do - before do - setup_csp_for_controller(described_class, csp) + context 'when frame-src exists in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.frame_src 'https://something.test' end + end - subject do - get group_observability_index_path(group) - response.headers['Content-Security-Policy'] + it 'appends the proper url to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test #{observability_url} 'self'") + end + end + + context 'when self is already present in the policy' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.frame_src "'self'" end + end - context 'when there is no CSP config' do - let(:csp) { ActionDispatch::ContentSecurityPolicy.new } + it 'does not append self again' do + expect(subject).to include( + "frame-src 'self' #{observability_url};") + end + end - it 'does not add any csp header' do - expect(subject).to be_blank - end + context 'when default-src exists in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src 'https://something.test' end + end - context 'when frame-src exists in the CSP config' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.frame_src 'https://something.test' - end - end - - it 'appends the proper url to frame-src CSP directives' do - expect(subject).to include( - "frame-src https://something.test #{observability_url} 'self'") - end - end + it 'does not change default-src' do + expect(subject).to include( + "default-src https://something.test;") + end - context 'when self is already present in the policy' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.frame_src "'self'" - end - end - - it 'does not append self again' do - expect(subject).to include( - "frame-src 'self' #{observability_url};") - end - end + it 'appends the proper url to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test #{observability_url} 'self'") + end + end - context 'when default-src exists in the CSP config' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.default_src 'https://something.test' - end - end - - it 'does not change default-src' do - expect(subject).to include( - "default-src https://something.test;") - end - - it 'appends the proper url to frame-src CSP directives' do - expect(subject).to include( - "frame-src https://something.test #{observability_url} 'self'") - end + context 'when frame-src and default-src exist in the CSP config' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src 'https://something_default.test' + p.frame_src 'https://something.test' end + end - context 'when frame-src and default-src exist in the CSP config' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.default_src 'https://something_default.test' - p.frame_src 'https://something.test' - end - end - - it 'appends to frame-src CSP directives' do - expect(subject).to include( - "frame-src https://something.test #{observability_url} 'self'") - expect(subject).to include( - "default-src https://something_default.test") - end - end + it 'appends to frame-src CSP directives' do + expect(subject).to include( + "frame-src https://something.test #{observability_url} 'self'") + expect(subject).to include( + "default-src https://something_default.test") end end end diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb index ae69b222280..68e619e5246 100644 --- a/spec/routing/group_routing_spec.rb +++ b/spec/routing/group_routing_spec.rb @@ -72,8 +72,16 @@ RSpec.shared_examples 'groups routing' do expect(get("groups/#{group_path}/-/harbor/repositories/test/artifacts/test/tags")).to route_to('groups/harbor/tags#index', group_id: group_path, repository_id: 'test', artifact_id: 'test') end - it 'routes to the observability controller' do - expect(get("groups/#{group_path}/-/observability")).to route_to('groups/observability#index', group_id: group_path) + it 'routes to the observability controller dashboards method' do + expect(get("groups/#{group_path}/-/observability/dashboards")).to route_to('groups/observability#dashboards', group_id: group_path) + end + + it 'routes to the observability controller explore method' do + expect(get("groups/#{group_path}/-/observability/explore")).to route_to('groups/observability#explore', group_id: group_path) + end + + it 'routes to the observability controller manage method' do + expect(get("groups/#{group_path}/-/observability/manage")).to route_to('groups/observability#manage', group_id: group_path) end end diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb index e9c1fe23855..702c6d9fe98 100644 --- a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb +++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb @@ -184,22 +184,6 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do end end - describe 'auto merge' do - context 'when auto merge is enabled' do - let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) } - - it 'returns auto merge related information' do - expect(subject[:auto_merge_enabled]).to be_truthy - end - end - - context 'when auto merge is not enabled' do - it 'returns auto merge related information' do - expect(subject[:auto_merge_enabled]).to be_falsy - end - end - end - describe 'squash defaults for projects' do where(:squash_option, :value, :default, :readonly) do 'always' | true | true | true diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb index 59ffba0e7a9..418f629a301 100644 --- a/spec/serializers/merge_request_poll_widget_entity_spec.rb +++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb @@ -184,10 +184,4 @@ RSpec.describe MergeRequestPollWidgetEntity do end end end - - describe '#mergeable_discussions_state?' do - it 'returns mergeable discussions state' do - expect(subject[:mergeable_discussions_state]).to eq(true) - end - end end diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb new file mode 100644 index 00000000000..2ce47f42a72 --- /dev/null +++ b/spec/services/issuable/discussions_list_service_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Issuable::DiscussionsListService do + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, :repository, :private, group: group) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:label) { create(:label, project: project) } + + let(:finder_params_for_issuable) { {} } + + subject(:discussions_service) { described_class.new(current_user, issuable, finder_params_for_issuable) } + + describe 'fetching notes for issue' do + let_it_be(:issuable) { create(:issue, project: project) } + + it_behaves_like 'listing issuable discussions', :guest, 1, 7 + end + + describe 'fetching notes for merge requests' do + let_it_be(:issuable) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'listing issuable discussions', :reporter, 0, 6 + end +end diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb index 43c94ef07aa..27c0394ac8b 100644 --- a/spec/services/issues/relative_position_rebalancing_service_spec.rb +++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb @@ -93,8 +93,12 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s it 'resumes a started rebalance even if there are already too many rebalances running' do Gitlab::Redis::SharedState.with do |redis| - redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}") - redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "1/100") + redis.sadd("gitlab:issues-position-rebalances:running_rebalances", + [ + "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}", + "1/100" + ] + ) end caching = service.send(:caching) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d169c50013..8e73073e68b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -255,10 +255,6 @@ RSpec.configure do |config| # The survey popover can block the diffs causing specs to fail stub_feature_flags(mr_experience_survey: false) - # Merge request widget GraphQL requests are disabled in the tests - # for now whilst we migrate as much as we can over the GraphQL - # stub_feature_flags(merge_request_widget_graphql: false) - # Using FortiAuthenticator as OTP provider is disabled by default in # tests, until we introduce it in user settings stub_feature_flags(forti_authenticator: false) diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index b44552d6479..e1ed3ffacec 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -90,7 +90,11 @@ module NavbarStructureHelper _('Kubernetes'), new_nav_item: { nav_item: _('Observability'), - nav_sub_items: [] + nav_sub_items: [ + _('Dashboards'), + _('Explore'), + _('Manage Dashboards') + ] } ) end diff --git a/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb new file mode 100644 index 00000000000..08fab45e41b --- /dev/null +++ b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::BaseCi do + describe 'default values' do + it { expect(subject.category).to eq(:ci) } + end +end diff --git a/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb new file mode 100644 index 00000000000..5d7e7633a23 --- /dev/null +++ b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::BaseMonitoring do + describe 'default values' do + it { expect(subject.category).to eq(:monitoring) } + end +end diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index e35ac9c0d0d..7dfdd24177e 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -6,6 +6,10 @@ RSpec.shared_examples Integrations::BaseSlashCommands do it { is_expected.to have_many :chat_names } end + describe 'default values' do + it { expect(subject.category).to eq(:chat) } + end + describe '#valid_token?' do subject { described_class.new } diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index 32562aef8d2..f577e2ad323 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'with cross-reference system notes' do new_merge_request.project.add_developer(user) hidden_merge_request = create(:merge_request) - new_cross_reference = "test commit #{hidden_merge_request.project.commit}" + new_cross_reference = "test commit #{hidden_merge_request.project.commit.to_reference(project)}" new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference) create(:system_note_metadata, note: new_note, action: 'cross_reference') end diff --git a/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb new file mode 100644 index 00000000000..c38ca6a3bf0 --- /dev/null +++ b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'listing issuable discussions' do |user_role, internal_discussion_count, total_discussions_count| + before_all do + create_notes(issuable, "some user comment") + end + + context 'when user cannot read issue' do + it "returns no notes" do + expect(discussions_service.execute).to be_empty + end + end + + context 'when user can read issuable' do + before do + group.add_developer(current_user) + end + + context 'with paginated results' do + let(:finder_params_for_issuable) { { per_page: 2 } } + let(:next_page_cursor) { { cursor: discussions_service.paginator.cursor_for_next_page } } + + it "returns next page notes" do + next_page_discussions_service = described_class.new(current_user, issuable, + finder_params_for_issuable.merge(next_page_cursor)) + discussions = next_page_discussions_service.execute + + expect(discussions.count).to eq(2) + expect(discussions.first.notes.map(&:note)).to match_array(["added #{label.to_reference} label"]) + expect(discussions.second.notes.map(&:note)).to match_array(["removed #{label.to_reference} label"]) + end + end + + # confidential notes are currently available only on issues and epics + context 'and cannot read confidential notes' do + before do + group.add_member(current_user, user_role) + end + + it "returns non confidential notes" do + discussions = discussions_service.execute + + non_conf_discussion_count = total_discussions_count - internal_discussion_count + expect(discussions.count).to eq(non_conf_discussion_count) + expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(0) + expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count) + end + end + + # confidential notes are currently available only on issues and epics + context 'and can read confidential notes' do + it "returns all notes" do + discussions = discussions_service.execute + + expect(discussions.count).to eq(total_discussions_count) + expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(internal_discussion_count) + non_conf_discussion_count = total_discussions_count - internal_discussion_count + expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count) + end + end + + context 'and system notes only' do + let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_activity] } } + + it "returns system notes" do + discussions = discussions_service.execute + + expect(discussions.count { |disc| disc.notes.any?(&:system) }).to be > 0 + expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to eq(0) + end + end + + context 'and user comments only' do + let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_comments] } } + + it "returns user comments" do + discussions = discussions_service.execute + + expect(discussions.count { |disc| disc.notes.any?(&:system) }).to eq(0) + expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to be > 0 + end + end + end +end + +def create_notes(issuable, note_body) + assoc_name = issuable.to_ability_name + + create(:note, system: true, project: issuable.project, noteable: issuable) + + first_discussion = create(:discussion_note_on_issue, noteable: issuable, project: issuable.project, note: note_body) + create(:note, + discussion_id: first_discussion.discussion_id, noteable: issuable, + project: issuable.project, note: "reply on #{note_body}") + + create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'add') + create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'remove') + + unless issuable.is_a?(Epic) + create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'add') + create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'remove') + end + + # confidential notes are currently available only on issues and epics + return unless issuable.is_a?(Issue) || issuable.is_a?(Epic) + + first_internal_discussion = create(:discussion_note_on_issue, :confidential, + noteable: issuable, project: issuable.project, note: "confidential #{note_body}") + create(:note, :confidential, + discussion_id: first_internal_discussion.discussion_id, noteable: issuable, + project: issuable.project, note: "reply on confidential #{note_body}") +end diff --git a/spec/views/groups/observability.html.haml_spec.rb b/spec/views/groups/observability.html.haml_spec.rb deleted file mode 100644 index db280d5a2ba..00000000000 --- a/spec/views/groups/observability.html.haml_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'groups/observability/index' do - let_it_be(:iframe_url) { "foo.test" } - - before do - assign(:observability_iframe_src, iframe_url) - end - - it 'renders as expected' do - render - page = Capybara.string(rendered) - iframe = page.find('iframe#observability-ui-iframe') - expect(iframe['src']).to eq(iframe_url) - end -end diff --git a/spec/views/groups/observability/observability.html.haml_spec.rb b/spec/views/groups/observability/observability.html.haml_spec.rb new file mode 100644 index 00000000000..0561737cb39 --- /dev/null +++ b/spec/views/groups/observability/observability.html.haml_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'groups/observability/observability.html.haml' do + let(:iframe_url) { "foo.test" } + + before do + allow(view).to receive(:observability_iframe_src).and_return(iframe_url) + end + + it 'renders as expected' do + render + page = Capybara.string(rendered) + div = page.find('#js-observability-app') + expect(div['data-observability-iframe-src']).to eq(iframe_url) + end +end diff --git a/spec/workers/merge_requests/delete_branch_worker_spec.rb b/spec/workers/merge_requests/delete_branch_worker_spec.rb index f97e7d3b0bf..80ca8c061f5 100644 --- a/spec/workers/merge_requests/delete_branch_worker_spec.rb +++ b/spec/workers/merge_requests/delete_branch_worker_spec.rb @@ -55,36 +55,6 @@ RSpec.describe MergeRequests::DeleteBranchWorker do worker.perform(merge_request.id, user.id, branch, retarget_branch) end end - - context 'when delete service returns an error' do - let(:service_result) { ServiceResponse.error(message: 'placeholder') } - - it 'tracks the exception' do - expect_next_instance_of(::Branches::DeleteService) do |instance| - expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result) - end - - expect(service_result).to receive(:track_exception).and_call_original - - worker.perform(merge_request.id, user.id, branch, retarget_branch) - end - - context 'when track_delete_source_errors is disabled' do - before do - stub_feature_flags(track_delete_source_errors: false) - end - - it 'does not track the exception' do - expect_next_instance_of(::Branches::DeleteService) do |instance| - expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result) - end - - expect(service_result).not_to receive(:track_exception) - - worker.perform(merge_request.id, user.id, branch, retarget_branch) - end - end - end end it_behaves_like 'an idempotent worker' do diff --git a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb index 8dd302d81cf..2935d3ef5dc 100644 --- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb +++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb @@ -102,32 +102,6 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do context 'when delete service returns an error' do let(:service_result) { ServiceResponse.error(message: 'placeholder') } - it 'tracks the exception' do - expect_next_instance_of(::Branches::DeleteService) do |instance| - expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result) - end - - expect(service_result).to receive(:track_exception).and_call_original - - worker.perform(merge_request.id, sha, user.id) - end - - context 'when track_delete_source_errors is disabled' do - before do - stub_feature_flags(track_delete_source_errors: false) - end - - it 'does not track the exception' do - expect_next_instance_of(::Branches::DeleteService) do |instance| - expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result) - end - - expect(service_result).not_to receive(:track_exception) - - worker.perform(merge_request.id, sha, user.id) - end - end - it 'still retargets the merge request' do expect_next_instance_of(::Branches::DeleteService) do |instance| expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result) |