diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-16 12:13:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-16 12:13:21 +0300 |
commit | 2c90b9b579fbfe3db191a032d2cb176761605a02 (patch) | |
tree | d9819280a1ec64ff82c31ce6081e00745a9648b4 /spec | |
parent | ccca6cec346d169fa2521c390760af9bd885ea77 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
25 files changed, 380 insertions, 424 deletions
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 48331d74dcc..47086ccdd2c 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -175,7 +175,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do it 'assigns error' do go - expect(assigns[:error]).to eq _('Invalid pin code') + expect(assigns[:error]).to eq({ message: 'Invalid pin code.' }) end it 'assigns qr_code' do diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb index fcb857df93f..a9256a73d7b 100644 --- a/spec/features/profiles/two_factor_auths_spec.rb +++ b/spec/features/profiles/two_factor_auths_spec.rb @@ -45,6 +45,19 @@ RSpec.describe 'Two factor auths' do expect(page).to have_content('Status: Enabled') end end + + context 'when invalid pin is provided' do + let_it_be(:user) { create(:omniauth_user) } + + it 'renders a error alert with a link to the troubleshooting section' do + visit profile_two_factor_auth_path + + fill_in 'pin_code', with: '123' + click_button 'Register with two-factor app' + + expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting')) + end + end end context 'when user has two-factor authentication enabled' do diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js index 200469b3f2a..45c5c87d800 100644 --- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js +++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js @@ -11,11 +11,11 @@ describe('IssueBoardFilter', () => { const findBoardsFilteredSearch = () => wrapper.findComponent(BoardFilteredSearch); - const createComponent = ({ epicFeatureAvailable = false } = {}) => { + const createComponent = ({ isSignedIn = false } = {}) => { wrapper = shallowMount(IssueBoardFilteredSpec, { propsData: { fullPath: 'gitlab-org', boardType: 'group' }, provide: { - epicFeatureAvailable, + isSignedIn, }, }); }; @@ -45,10 +45,24 @@ describe('IssueBoardFilter', () => { expect(findBoardsFilteredSearch().exists()).toBe(true); }); - it('passes the correct tokens to BoardFilteredSearch', () => { - const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones); + it.each` + isSignedIn + ${true} + ${false} + `( + 'passes the correct tokens to BoardFilteredSearch when user sign in is $isSignedIn', + ({ isSignedIn }) => { + createComponent({ isSignedIn }); - expect(findBoardsFilteredSearch().props('tokens')).toEqual(tokens); - }); + const tokens = mockTokens( + fetchLabelsSpy, + fetchAuthorsSpy, + wrapper.vm.fetchMilestones, + isSignedIn, + ); + + expect(findBoardsFilteredSearch().props('tokens')).toEqual(tokens); + }, + ); }); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 1898ee87d7e..bba8b9ecc15 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -4,6 +4,7 @@ import { ListType } from '~/boards/constants'; import { __ } from '~/locale'; import { DEFAULT_MILESTONES_GRAPHQL } 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'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; @@ -549,7 +550,16 @@ export const mockMoveData = { ...mockMoveIssueParams, }; -export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ +export const mockEmojiToken = { + type: 'my_reaction_emoji', + icon: 'thumb-up', + title: 'My-Reaction', + unique: true, + token: EmojiToken, + fetchEmojis: expect.any(Function), +}; + +export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji) => [ { icon: 'user', title: __('Assignee'), @@ -590,6 +600,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ symbol: '~', fetchLabels, }, + ...(hasEmoji ? [mockEmojiToken] : []), { icon: 'clock', title: __('Milestone'), diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index 9db0f823d84..c454d502beb 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -53,7 +53,7 @@ describe('DiscussionCounter component', () => { describe('has no resolvable discussions', () => { it('does not render', () => { - store.commit(types.SET_INITIAL_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]); + store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]); store.dispatch('updateResolvableDiscussionsCounts'); wrapper = shallowMount(DiscussionCounter, { store, localVue }); @@ -64,7 +64,7 @@ describe('DiscussionCounter component', () => { describe('has resolvable discussions', () => { const updateStore = (note = {}) => { discussionMock.notes[0] = { ...discussionMock.notes[0], ...note }; - store.commit(types.SET_INITIAL_DISCUSSIONS, [discussionMock]); + store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussionMock]); store.dispatch('updateResolvableDiscussionsCounts'); }; @@ -97,7 +97,7 @@ describe('DiscussionCounter component', () => { let toggleAllButton; const updateStoreWithExpanded = (expanded) => { const discussion = { ...discussionMock, expanded }; - store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]); + store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]); store.dispatch('updateResolvableDiscussionsCounts'); wrapper = shallowMount(DiscussionCounter, { store, localVue }); toggleAllButton = wrapper.find('.toggle-all-discussions-btn'); diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 2ff65d3f47e..bbe074f0105 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -119,7 +119,7 @@ describe('Actions Notes Store', () => { actions.setInitialNotes, [individualNote], { notes: [] }, - [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }], + [{ type: 'ADD_OR_UPDATE_DISCUSSIONS', payload: [individualNote] }], [], done, ); @@ -1395,4 +1395,93 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('fetchDiscussions', () => { + const discussion = { notes: [] }; + + afterEach(() => { + window.gon = {}; + }); + + it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', (done) => { + axiosMock.onAny().reply(200, { discussion }); + testAction( + actions.fetchDiscussions, + {}, + null, + [ + { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } }, + { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false }, + ], + [{ type: 'updateResolvableDiscussionsCounts' }], + done, + ); + }); + + it('dispatches `fetchDiscussionsBatch` action if `paginatedIssueDiscussions` feature flag is enabled', (done) => { + window.gon = { features: { paginatedIssueDiscussions: true } }; + + testAction( + actions.fetchDiscussions, + { path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' }, + null, + [], + [ + { + type: 'fetchDiscussionsBatch', + payload: { + config: { + params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' }, + }, + path: 'test-path', + perPage: 20, + }, + }, + ], + done, + ); + }); + }); + + describe('fetchDiscussionsBatch', () => { + const discussion = { notes: [] }; + + const config = { + params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' }, + }; + + const actionPayload = { config, path: 'test-path', perPage: 20 }; + + it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', (done) => { + axiosMock.onAny().reply(200, { discussion }, {}); + testAction( + actions.fetchDiscussionsBatch, + actionPayload, + null, + [ + { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } }, + { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false }, + ], + [{ type: 'updateResolvableDiscussionsCounts' }], + done, + ); + }); + + it('dispatches itself if there is `x-next-page-cursor` header', (done) => { + axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 }); + testAction( + actions.fetchDiscussionsBatch, + actionPayload, + null, + [{ type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } }], + [ + { + type: 'fetchDiscussionsBatch', + payload: { ...actionPayload, perPage: 30, cursor: 1 }, + }, + ], + done, + ); + }); + }); }); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index 99e24f724f4..c9e24039b64 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -159,7 +159,7 @@ describe('Notes Store mutations', () => { }); }); - describe('SET_INITIAL_DISCUSSIONS', () => { + describe('ADD_OR_UPDATE_DISCUSSIONS', () => { it('should set the initial notes received', () => { const state = { discussions: [], @@ -169,15 +169,17 @@ describe('Notes Store mutations', () => { individual_note: true, notes: [ { + id: 100, note: '1', }, { + id: 101, note: '2', }, ], }; - mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); + mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [note, legacyNote]); expect(state.discussions[0].id).toEqual(note.id); expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); @@ -190,7 +192,7 @@ describe('Notes Store mutations', () => { discussions: [], }; - mutations.SET_INITIAL_DISCUSSIONS(state, [ + mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [ { ...note, diff_file: { @@ -208,7 +210,7 @@ describe('Notes Store mutations', () => { discussions: [], }; - mutations.SET_INITIAL_DISCUSSIONS(state, [ + mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [ { ...note, diff_file: { diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 37e040a422b..2277bd78e86 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -351,12 +351,14 @@ RSpec.describe API::Helpers do let(:send_git_blob) do subject.send(:send_git_blob, repository, blob) + subject.header end before do allow(subject).to receive(:env).and_return({}) allow(subject).to receive(:content_type) allow(subject).to receive(:header).and_return({}) + allow(subject).to receive(:body).and_return('') allow(Gitlab::Workhorse).to receive(:send_git_blob) end diff --git a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb index 5e029f304c9..a0543ca9958 100644 --- a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb +++ b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb @@ -73,6 +73,25 @@ RSpec.describe Gitlab::BackgroundMigration::JobCoordinator do coordinator.steal('Foo') end + it 'sets up the shared connection while stealing jobs' do + connection = double('connection') + allow(coordinator).to receive(:connection).and_return(connection) + + expect(coordinator).to receive(:with_shared_connection).and_call_original + + expect(queue[0]).to receive(:delete).and_return(true) + + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) do + expect(Gitlab::Database::SharedModel.connection).to be(connection) + end + + coordinator.steal('Foo') do + expect(Gitlab::Database::SharedModel.connection).to be(connection) + + true # the job is only performed if the block returns true + end + end + it 'does not steal job that has already been taken' do expect(queue[0]).to receive(:delete).and_return(false) @@ -194,13 +213,20 @@ RSpec.describe Gitlab::BackgroundMigration::JobCoordinator do describe '#perform' do let(:migration) { spy(:migration) } + let(:connection) { double('connection') } before do stub_const('Gitlab::BackgroundMigration::Foo', migration) + + allow(coordinator).to receive(:connection).and_return(connection) end - it 'performs a background migration' do - expect(migration).to receive(:perform).with(10, 20).once + it 'performs a background migration with the configured shared connection' do + expect(coordinator).to receive(:with_shared_connection).and_call_original + + expect(migration).to receive(:perform).with(10, 20).once do + expect(Gitlab::Database::SharedModel.connection).to be(connection) + end coordinator.perform('Foo', [10, 20]) end diff --git a/spec/lib/gitlab/security/scan_configuration_spec.rb b/spec/lib/gitlab/security/scan_configuration_spec.rb deleted file mode 100644 index 9b62c13f3bb..00000000000 --- a/spec/lib/gitlab/security/scan_configuration_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Gitlab::Security::ScanConfiguration do - let_it_be(:project) { create(:project, :repository) } - - let(:scan) { described_class.new(project: project, type: type, configured: configured) } - - describe '#available?' do - subject { scan.available? } - - let(:configured) { true } - - context 'with a core scanner' do - let(:type) { :sast } - - it { is_expected.to be_truthy } - end - - context 'with custom scanner' do - let(:type) { :my_scanner } - - it { is_expected.to be_falsey } - end - end - - describe '#configured?' do - subject { scan.configured? } - - let(:type) { :sast } - let(:configured) { false } - - it { is_expected.to be_falsey } - end - - describe '#configuration_path' do - subject { scan.configuration_path } - - let(:configured) { true } - - context 'with a non configurable scaner' do - let(:type) { :secret_detection } - - it { is_expected.to be_nil } - end - - context 'with licensed scanner for FOSS environment' do - let(:type) { :dast } - - before do - stub_env('FOSS_ONLY', '1') - end - - it { is_expected.to be_nil } - end - - context 'with custom scanner' do - let(:type) { :my_scanner } - - it { is_expected.to be_nil } - end - end -end diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb index 38766d8decd..81ae30b7116 100644 --- a/spec/models/concerns/noteable_spec.rb +++ b/spec/models/concerns/noteable_spec.rb @@ -77,6 +77,70 @@ RSpec.describe Noteable do end end + describe '#discussion_root_note_ids' do + let!(:label_event) { create(:resource_label_event, merge_request: subject) } + let!(:system_note) { create(:system_note, project: project, noteable: subject) } + let!(:milestone_event) { create(:resource_milestone_event, merge_request: subject) } + let!(:state_event) { create(:resource_state_event, merge_request: subject) } + + it 'returns ordered discussion_ids and synthetic note ids' do + discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:all_notes]).map do |n| + { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id } + end + + expect(discussions).to match([ + a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id), + a_hash_including(table_name: 'resource_label_events', id: label_event.id), + a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id), + a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id), + a_hash_including(table_name: 'resource_state_events', id: state_event.id) + ]) + end + + it 'filters by comments only' do + discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_comments]).map do |n| + { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id } + end + + expect(discussions).to match([ + a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id), + a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id) + ]) + end + + it 'filters by system notes only' do + discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_activity]).map do |n| + { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id } + end + + expect(discussions).to match([ + a_hash_including(table_name: 'resource_label_events', id: label_event.id), + a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id), + a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id), + a_hash_including(table_name: 'resource_state_events', id: state_event.id) + ]) + end + end + describe '#grouped_diff_discussions' do let(:grouped_diff_discussions) { subject.grouped_diff_discussions } diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb deleted file mode 100644 index 836753d0483..00000000000 --- a/spec/presenters/projects/security/configuration_presenter_spec.rb +++ /dev/null @@ -1,301 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::Security::ConfigurationPresenter do - include Gitlab::Routing.url_helpers - using RSpec::Parameterized::TableSyntax - - let(:project_with_repo) { create(:project, :repository) } - let(:project_with_no_repo) { create(:project) } - let(:current_user) { create(:user) } - let(:presenter) { described_class.new(project, current_user: current_user) } - - before do - stub_licensed_features(licensed_scan_types.to_h { |type| [type, true] }) - - stub_feature_flags(corpus_management: false) - end - - describe '#to_html_data_attribute' do - subject(:html_data) { presenter.to_html_data_attribute } - - context 'when latest default branch pipeline`s source is not auto devops' do - let(:project) { project_with_repo } - - let(:pipeline) do - create( - :ci_pipeline, - project: project, - ref: project.default_branch, - sha: project.commit.sha - ) - end - - let!(:build_sast) { create(:ci_build, :sast, pipeline: pipeline) } - let!(:build_dast) { create(:ci_build, :dast, pipeline: pipeline) } - let!(:build_license_scanning) { create(:ci_build, :license_scanning, pipeline: pipeline) } - - it 'includes links to auto devops and secure product docs' do - expect(html_data[:auto_devops_help_page_path]).to eq(help_page_path('topics/autodevops/index')) - expect(html_data[:help_page_path]).to eq(help_page_path('user/application_security/index')) - end - - it 'returns info that Auto DevOps is not enabled' do - expect(html_data[:auto_devops_enabled]).to eq(false) - expect(html_data[:auto_devops_path]).to eq(project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) - end - - it 'includes a link to the latest pipeline' do - expect(html_data[:latest_pipeline_path]).to eq(project_pipeline_path(project, pipeline)) - end - - it 'has stubs for autofix' do - expect(html_data.keys).to include(:can_toggle_auto_fix_settings, :auto_fix_enabled, :auto_fix_user_path) - end - - context "while retrieving information about user's ability to enable auto_devops" do - where(:is_admin, :archived, :feature_available, :result) do - true | true | true | false - false | true | true | false - true | false | true | true - false | false | true | false - true | true | false | false - false | true | false | false - true | false | false | false - false | false | false | false - end - - with_them do - before do - allow_next_instance_of(described_class) do |presenter| - allow(presenter).to receive(:can?).and_return(is_admin) - allow(presenter).to receive(:archived?).and_return(archived) - allow(presenter).to receive(:feature_available?).and_return(feature_available) - end - end - - it 'includes can_enable_auto_devops' do - expect(html_data[:can_enable_auto_devops]).to eq(result) - end - end - end - - it 'includes feature information' do - feature = Gitlab::Json.parse(html_data[:features]).find { |scan| scan['type'] == 'sast' } - - expect(feature['type']).to eq('sast') - expect(feature['configured']).to eq(true) - expect(feature['configuration_path']).to eq(project_security_configuration_sast_path(project)) - expect(feature['available']).to eq(true) - end - - context 'when checking features configured status' do - let(:features) { Gitlab::Json.parse(html_data[:features]) } - - where(:type, :configured) do - :dast | true - :dast_profiles | true - :sast | true - :sast_iac | false - :container_scanning | false - :cluster_image_scanning | false - :dependency_scanning | false - :license_scanning | true - :secret_detection | false - :coverage_fuzzing | false - :api_fuzzing | false - :corpus_management | true - end - - with_them do - it 'returns proper configuration status' do - feature = features.find { |scan| scan['type'] == type.to_s } - - expect(feature['configured']).to eq(configured) - end - end - end - - context 'when the job has more than one report' do - let(:features) { Gitlab::Json.parse(html_data[:features]) } - - let!(:artifacts) do - { artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } } - end - - let!(:complicated_job) { build_stubbed(:ci_build, options: artifacts) } - - before do - allow_next_instance_of(::Security::SecurityJobsFinder) do |finder| - allow(finder).to receive(:execute).and_return([complicated_job]) - end - end - - where(:type, :configured) do - :dast | false - :dast_profiles | true - :sast | true - :sast_iac | false - :container_scanning | false - :cluster_image_scanning | false - :dependency_scanning | false - :license_scanning | true - :secret_detection | false - :coverage_fuzzing | false - :api_fuzzing | false - :corpus_management | true - end - - with_them do - it 'properly detects security jobs' do - feature = features.find { |scan| scan['type'] == type.to_s } - - expect(feature['configured']).to eq(configured) - end - end - end - - it 'includes a link to the latest pipeline' do - expect(subject[:latest_pipeline_path]).to eq(project_pipeline_path(project, pipeline)) - end - - context "while retrieving information about gitlab ci file" do - context 'when a .gitlab-ci.yml file exists' do - let!(:ci_config) do - project.repository.create_file( - project.creator, - Gitlab::FileDetector::PATTERNS[:gitlab_ci], - 'contents go here', - message: 'test', - branch_name: 'master') - end - - it 'expects gitlab_ci_present to be true' do - expect(html_data[:gitlab_ci_present]).to eq(true) - end - end - - context 'when a .gitlab-ci.yml file does not exist' do - it 'expects gitlab_ci_present to be false if the file is not present' do - expect(html_data[:gitlab_ci_present]).to eq(false) - end - end - end - - it 'includes the path to gitlab_ci history' do - expect(subject[:gitlab_ci_history_path]).to eq(project_blame_path(project, 'master/.gitlab-ci.yml')) - end - end - - context 'when the project is empty' do - let(:project) { project_with_no_repo } - - it 'includes a blank gitlab_ci history path' do - expect(html_data[:gitlab_ci_history_path]).to eq('') - end - end - - context 'when the project has no default branch set' do - let(:project) { project_with_repo } - - it 'includes the path to gitlab_ci history' do - allow(project).to receive(:default_branch).and_return(nil) - - expect(html_data[:gitlab_ci_history_path]).to eq(project_blame_path(project, 'master/.gitlab-ci.yml')) - end - end - - context "when the latest default branch pipeline's source is auto devops" do - let(:project) { project_with_repo } - - let(:pipeline) do - create( - :ci_pipeline, - :auto_devops_source, - project: project, - ref: project.default_branch, - sha: project.commit.sha - ) - end - - let!(:build_sast) { create(:ci_build, :sast, pipeline: pipeline, status: 'success') } - let!(:build_dast) { create(:ci_build, :dast, pipeline: pipeline, status: 'success') } - let!(:ci_build) { create(:ci_build, :secret_detection, pipeline: pipeline, status: 'pending') } - - it 'reports that auto devops is enabled' do - expect(html_data[:auto_devops_enabled]).to be_truthy - end - - context 'when gathering feature data' do - let(:features) { Gitlab::Json.parse(html_data[:features]) } - - where(:type, :configured) do - :dast | true - :dast_profiles | true - :sast | true - :sast_iac | false - :container_scanning | false - :cluster_image_scanning | false - :dependency_scanning | false - :license_scanning | false - :secret_detection | true - :coverage_fuzzing | false - :api_fuzzing | false - :corpus_management | true - end - - with_them do - it 'reports that all scanners are configured for which latest pipeline has builds' do - feature = features.find { |scan| scan['type'] == type.to_s } - - expect(feature['configured']).to eq(configured) - end - end - end - end - - context 'when the project has no default branch pipeline' do - let(:project) { project_with_repo } - - it 'reports that auto devops is disabled' do - expect(html_data[:auto_devops_enabled]).to be_falsy - end - - it 'includes a link to CI pipeline docs' do - expect(html_data[:latest_pipeline_path]).to eq(help_page_path('ci/pipelines')) - end - - context 'when gathering feature data' do - let(:features) { Gitlab::Json.parse(html_data[:features]) } - - where(:type, :configured) do - :dast | false - :dast_profiles | true - :sast | false - :sast_iac | false - :container_scanning | false - :cluster_image_scanning | false - :dependency_scanning | false - :license_scanning | false - :secret_detection | false - :coverage_fuzzing | false - :api_fuzzing | false - :corpus_management | true - end - - with_them do - it 'reports all security jobs as unconfigured with exception of "fake" jobs' do - feature = features.find { |scan| scan['type'] == type.to_s } - - expect(feature['configured']).to eq(configured) - end - end - end - end - - def licensed_scan_types - ::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseComplianceJobsFinder.allowed_job_types - [:cluster_image_scanning] - end - end -end diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 580f3d9e92d..410020b68cd 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -578,6 +578,7 @@ RSpec.describe API::Ci::Jobs do expect(response.headers.to_h) .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty end context 'when artifacts are locked' do @@ -948,6 +949,7 @@ RSpec.describe API::Ci::Jobs do expect(response.headers.to_h) .to include('Content-Type' => 'application/json', 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + expect(response.parsed_body).to be_empty end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 0b898496dd6..6aa12b6ff48 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -47,6 +47,15 @@ RSpec.describe API::Files do "/projects/#{project.id}/repository/files/#{file_path}" end + def expect_to_send_git_blob(url, params) + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get url, params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(response.parsed_body).to be_empty + end + context 'http headers' do it 'converts value into string' do helper.set_http_headers(test: 1) @@ -257,11 +266,7 @@ RSpec.describe API::Files do it 'returns raw file info' do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - - get api(url, api_user, **options), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, api_user, **options), params) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" end @@ -523,11 +528,8 @@ RSpec.describe API::Files do it 'returns raw file info' do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), params) end context 'when ref is not provided' do @@ -537,39 +539,29 @@ RSpec.describe API::Files do it 'returns response :ok', :aggregate_failures do url = route(file_path) + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: {} - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), {}) end end it 'returns raw file info for files with dots' do url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(url, current_user), params) end it 'returns file by commit sha' do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(route(file_path) + "/raw", current_user), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params) end it 'sets no-cache headers' do url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params + expect_to_send_git_blob(api(url, current_user), params) expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache") expect(response.headers["Pragma"]).to eq("no-cache") @@ -633,11 +625,9 @@ RSpec.describe API::Files do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - expect(Gitlab::Workhorse).to receive(:send_git_blob) + url = api(route(file_path) + "/raw", personal_access_token: token) - get api(route(file_path) + "/raw", personal_access_token: token), params: params - - expect(response).to have_gitlab_http_status(:ok) + expect_to_send_git_blob(url, params) end end end diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb index f23e374407b..33c86d56ed4 100644 --- a/spec/requests/api/project_snapshots_spec.rb +++ b/spec/requests/api/project_snapshots_spec.rb @@ -29,6 +29,7 @@ RSpec.describe API::ProjectSnapshots do repository: repository.gitaly_repository ).to_json ) + expect(response.parsed_body).to be_empty end it 'returns authentication error as project owner' do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 8cd1f15a88d..512cbf7c321 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -400,6 +400,7 @@ RSpec.describe API::ProjectSnippets do expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq 'text/plain' + expect(response.parsed_body).to be_empty end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 35e8ff2eeee..f3146480be2 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -197,6 +197,7 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:ok) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + expect(response.parsed_body).to be_empty end it 'sets inline content disposition by default' do @@ -274,6 +275,7 @@ RSpec.describe API::Repositories do expect(type).to eq('git-archive') expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/) + expect(response.parsed_body).to be_empty end it 'returns the repository archive archive.zip' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f4d15d0525e..dd5e6ac8a5e 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -113,6 +113,7 @@ RSpec.describe API::Snippets, factory_default: :keep do expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq 'text/plain' expect(headers['Content-Disposition']).to match(/^inline/) + expect(response.parsed_body).to be_empty end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb new file mode 100644 index 00000000000..f44b1f4d502 --- /dev/null +++ b/spec/requests/projects/issues_controller_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::IssuesController do + let_it_be(:issue) { create(:issue) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { issue.project } + let_it_be(:user) { issue.author } + + before do + login_as(user) + end + + describe 'GET #discussions' do + let_it_be(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + let_it_be(:discussion_reply) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, in_reply_to: discussion) } + let_it_be(:state_event) { create(:resource_state_event, issue: issue) } + let_it_be(:discussion_2) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + let_it_be(:discussion_3) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } + + context 'pagination' do + def get_discussions(**params) + get discussions_project_issue_path(project, issue, params: params.merge(format: :json)) + end + + it 'returns paginated notes and cursor based on per_page param' do + get_discussions(per_page: 2) + + discussions = Gitlab::Json.parse(response.body) + notes = discussions.flat_map { |d| d['notes'] } + + expect(discussions.count).to eq(2) + expect(notes).to match([ + a_hash_including('id' => discussion.id.to_s), + a_hash_including('id' => discussion_reply.id.to_s), + a_hash_including('type' => 'StateNote') + ]) + + cursor = response.header['X-Next-Page-Cursor'] + expect(cursor).to be_present + + get_discussions(per_page: 1, cursor: cursor) + + discussions = Gitlab::Json.parse(response.body) + notes = discussions.flat_map { |d| d['notes'] } + + expect(discussions.count).to eq(1) + expect(notes).to match([ + a_hash_including('id' => discussion_2.id.to_s) + ]) + end + + context 'when paginated_issue_discussions is disabled' do + before do + stub_feature_flags(paginated_issue_discussions: false) + end + + it 'returns all discussions and ignores per_page param' do + get_discussions(per_page: 2) + + discussions = Gitlab::Json.parse(response.body) + notes = discussions.flat_map { |d| d['notes'] } + + expect(discussions.count).to eq(4) + expect(notes.count).to eq(5) + end + end + end + end +end diff --git a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb index cb42ad5b617..71b1d0993ee 100644 --- a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb +++ b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb @@ -4,18 +4,20 @@ require 'spec_helper' RSpec.describe ResourceEvents::SyntheticLabelNotesBuilderService do describe '#execute' do - let!(:user) { create(:user) } + let_it_be(:user) { create(:user) } - let!(:issue) { create(:issue, author: user) } + let_it_be(:issue) { create(:issue, author: user) } - let!(:event1) { create(:resource_label_event, issue: issue) } - let!(:event2) { create(:resource_label_event, issue: issue) } - let!(:event3) { create(:resource_label_event, issue: issue) } + let_it_be(:event1) { create(:resource_label_event, issue: issue) } + let_it_be(:event2) { create(:resource_label_event, issue: issue) } + let_it_be(:event3) { create(:resource_label_event, issue: issue) } it 'returns the expected synthetic notes' do notes = ResourceEvents::SyntheticLabelNotesBuilderService.new(issue, user).execute expect(notes.size).to eq(3) end + + it_behaves_like 'filters by paginated notes', :resource_label_event end end diff --git a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb index 1b35e224e98..9c6b6a33b57 100644 --- a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb +++ b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb @@ -24,5 +24,7 @@ RSpec.describe ResourceEvents::SyntheticMilestoneNotesBuilderService do 'removed milestone' ]) end + + it_behaves_like 'filters by paginated notes', :resource_milestone_event end end diff --git a/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb new file mode 100644 index 00000000000..79500f3768b --- /dev/null +++ b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceEvents::SyntheticStateNotesBuilderService do + describe '#execute' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'filters by paginated notes', :resource_state_event + end +end diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml index e0166f2ae3f..e4dc958830f 100644 --- a/spec/support/database/cross-database-modification-allowlist.yml +++ b/spec/support/database/cross-database-modification-allowlist.yml @@ -8,14 +8,6 @@ - "./ee/spec/models/group_member_spec.rb" - "./ee/spec/replicators/geo/pipeline_artifact_replicator_spec.rb" - "./ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb" -- "./ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb" -- "./ee/spec/requests/api/graphql/mutations/dast/profiles/run_spec.rb" -- "./ee/spec/requests/api/graphql/mutations/dast/profiles/update_spec.rb" -- "./ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb" -- "./ee/spec/services/app_sec/dast/profiles/create_service_spec.rb" -- "./ee/spec/services/app_sec/dast/profiles/update_service_spec.rb" -- "./ee/spec/services/app_sec/dast/scans/create_service_spec.rb" -- "./ee/spec/services/app_sec/dast/scans/run_service_spec.rb" - "./ee/spec/services/ci/destroy_pipeline_service_spec.rb" - "./ee/spec/services/ci/retry_build_service_spec.rb" - "./ee/spec/services/ci/subscribe_bridge_service_spec.rb" @@ -25,7 +17,6 @@ - "./ee/spec/services/ee/users/destroy_service_spec.rb" - "./ee/spec/services/projects/transfer_service_spec.rb" - "./ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb" -- "./ee/spec/services/vulnerability_feedback/create_service_spec.rb" - "./spec/controllers/abuse_reports_controller_spec.rb" - "./spec/controllers/admin/spam_logs_controller_spec.rb" - "./spec/controllers/admin/users_controller_spec.rb" diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index dae3a3e74be..b13c4da0bed 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -86,6 +86,7 @@ RSpec.shared_examples 'snippet blob content' do expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + expect(response.parsed_body).to be_empty end context 'when snippet repository is empty' do diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb new file mode 100644 index 00000000000..716bee39fca --- /dev/null +++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'filters by paginated notes' do |event_type| + let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang + + before do + create(event_type, issue: event.issue) + end + + it 'only returns given notes' do + paginated_notes = { event_type.to_s.pluralize => [double(id: event.id)] } + notes = described_class.new(event.issue, user, paginated_notes: paginated_notes).execute + + expect(notes.size).to eq(1) + expect(notes.first.event).to eq(event) + end + + context 'when paginated notes is empty' do + it 'does not return any notes' do + notes = described_class.new(event.issue, user, paginated_notes: {}).execute + + expect(notes.size).to eq(0) + end + end +end |