diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-09 21:07:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-09 21:07:44 +0300 |
commit | 453634293e24164ffaa5cd708e47a1cebc07bcd3 (patch) | |
tree | e49bb067fc508f57b03ac582872c4b1215eec326 /spec | |
parent | 608d6aaa3d80a33862ca2c29d96bfd687b1a011b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
36 files changed, 694 insertions, 218 deletions
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 0afd2e10ea2..23b0b58158f 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::NotesController do +RSpec.describe Projects::NotesController, type: :controller, feature_category: :team_planning do include ProjectForksHelper let(:user) { create(:user) } diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 00866ca118f..4e0c098ad81 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -68,14 +68,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte check_pipeline_response(returned: 2, all: 6) end - context 'when performing gitaly calls', :request_store do - before do - # To prevent double writes / fallback read due to MultiStore which is failing the `Gitlab::GitalyClient - # .get_request_count` expectation. - stub_feature_flags(use_primary_store_as_default_for_repository_cache: false) - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) - end - + context 'when performing gitaly calls', :request_store, :use_null_store_as_repository_cache do it 'limits the Gitaly requests' do # Isolate from test preparation (Repository#exists? is also cached in RequestStore) RequestStore.end! diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb index 8300cfce539..b2b41d653b6 100644 --- a/spec/features/broadcast_messages_spec.rb +++ b/spec/features/broadcast_messages_spec.rb @@ -23,7 +23,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do end shared_examples 'a dismissable Broadcast Messages' do - it 'hides broadcast message after dismiss', :js do + it 'hides broadcast message after dismiss', :js, + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390900' do visit root_path find('.js-dismiss-current-broadcast-notification').click diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb index df039493cec..c5d0791dc57 100644 --- a/spec/features/issues/user_creates_issue_spec.rb +++ b/spec/features/issues/user_creates_issue_spec.rb @@ -157,15 +157,10 @@ RSpec.describe "User creates issue", feature_category: :team_planning do end end - context 'form filled by URL parameters' do + context 'form filled by URL parameters', :use_null_store_as_repository_cache do let(:project) { create(:project, :public, :repository) } before do - # With multistore feature flags enabled (using an actual Redis store instead of NullStore), - # it somehow writes an invalid content to Redis and the specs would fail. - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) - stub_feature_flags(use_primary_store_as_default_for_repository_cache: false) - project.repository.create_file( user, '.gitlab/issue_templates/bug.md', diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index 8d4666dcb50..c2f67f36850 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -122,7 +122,8 @@ RSpec.describe 'Profile > SSH Keys', feature_category: :user_profile do project.add_developer(user) end - it 'revoking the SSH key marks commits as unverified' do + it 'revoking the SSH key marks commits as unverified', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390905' do visit project_commit_path(project, commit) find('a.gpg-status-box', text: 'Verified').click diff --git a/spec/features/profiles/list_users_saved_replies_spec.rb b/spec/features/profiles/list_users_saved_replies_spec.rb new file mode 100644 index 00000000000..4f3678f8051 --- /dev/null +++ b/spec/features/profiles/list_users_saved_replies_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Profile > Notifications > List users saved replies', :js, + feature_category: :user_profile do + let_it_be(:user) { create(:user) } + let_it_be(:saved_reply) { create(:saved_reply, user: user) } + + before do + sign_in(user) + end + + it 'shows the user a list of their saved replies' do + visit profile_saved_replies_path + + expect(page).to have_content('My saved replies (1)') + expect(page).to have_content(saved_reply.name) + expect(page).to have_content(saved_reply.content) + end +end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index d35726fe125..95741d6cdf0 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -333,6 +333,41 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do expect(page).to have_selector('ul.task-list', count: 1) expect(page).to have_selector('li.task-list-item', count: 1) expect(page).to have_selector('ul input[checked]', count: 1) + expect(page).to have_content('1 of 1 checklist item completed') + end + end + + describe 'tasks in code blocks' do + let(:code_tasks_markdown) do + <<-EOT.strip_heredoc + ``` + - [ ] a + ``` + + - [ ] b + EOT + end + + let!(:issue) { create(:issue, description: code_tasks_markdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + wait_for_requests + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + + find('.task-list-item-checkbox').click + wait_for_requests + + visit_issue(project, issue) + wait_for_requests + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + expect(page).to have_content('1 of 1 checklist item completed') end end @@ -370,6 +405,43 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do end end + describe 'summary properly formatted' do + let(:summary_markdown) do + <<-EOT.strip_heredoc + <details open> + <summary>Valid detail/summary with tasklist</summary> + + - [ ] People Ops: do such and such + + </details> + + * [x] Task 1 + EOT + end + + let!(:issue) { create(:issue, description: summary_markdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + wait_for_requests + + expect(page).to have_selector('ul.task-list', count: 2) + expect(page).to have_selector('li.task-list-item', count: 2) + expect(page).to have_selector('ul input[checked]', count: 1) + + first('.task-list-item-checkbox').click + wait_for_requests + + visit_issue(project, issue) + wait_for_requests + + expect(page).to have_selector('ul.task-list', count: 2) + expect(page).to have_selector('li.task-list-item', count: 2) + expect(page).to have_selector('ul input[checked]', count: 2) + expect(page).to have_content('2 of 2 checklist items completed') + end + end + describe 'markdown starting with new line character' do let(:markdown_starting_with_new_line) do <<-EOT.strip_heredoc diff --git a/spec/fixtures/api/schemas/entities/codequality_degradation.json b/spec/fixtures/api/schemas/entities/codequality_degradation.json index 863b9f0c77e..ac772873daf 100644 --- a/spec/fixtures/api/schemas/entities/codequality_degradation.json +++ b/spec/fixtures/api/schemas/entities/codequality_degradation.json @@ -21,7 +21,10 @@ }, "web_url": { "type": "string" + }, + "engine_name": { + "type": "string" } }, "additionalProperties": false -}
\ No newline at end of file +} diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb new file mode 100644 index 00000000000..c80ba06bca1 --- /dev/null +++ b/spec/frontend/fixtures/saved_replies.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do + include JavaScriptFixturesHelpers + include ApiHelpers + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + before do + sign_in(current_user) + end + + context 'when user has no saved replies' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'saved_replies.query.graphql' + + it "#{base_output_path}saved_replies_empty.query.graphql.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + end + end + + context 'when user has saved replies' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'saved_replies.query.graphql' + + it "#{base_output_path}saved_replies.query.graphql.json" do + create(:saved_reply, user: current_user) + create(:saved_reply, user: current_user) + + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + end + end +end diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap new file mode 100644 index 00000000000..3abdfcdaf20 --- /dev/null +++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Saved replies list item component renders list item 1`] = ` +<li + class="gl-mb-5" +> + <div + class="gl-display-flex gl-align-items-center" + > + <strong> + test + </strong> + </div> + + <div + class="gl-mt-3 gl-font-monospace" + > + /assign_reviewer + </div> +</li> +`; diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js new file mode 100644 index 00000000000..cad1000473b --- /dev/null +++ b/spec/frontend/saved_replies/components/list_item_spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils'; +import ListItem from '~/saved_replies/components/list_item.vue'; + +let wrapper; + +function createComponent(propsData = {}) { + return shallowMount(ListItem, { + propsData, + }); +} + +describe('Saved replies list item component', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('renders list item', async () => { + wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } }); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js new file mode 100644 index 00000000000..66e9ddfe148 --- /dev/null +++ b/spec/frontend/saved_replies/components/list_spec.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json'; +import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import List from '~/saved_replies/components/list.vue'; +import ListItem from '~/saved_replies/components/list_item.vue'; +import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql'; + +let wrapper; + +function createMockApolloProvider(response) { + Vue.use(VueApollo); + + const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]]; + + return createMockApollo(requestHandlers); +} + +function createComponent(options = {}) { + const { mockApollo } = options; + + return mount(List, { + apolloProvider: mockApollo, + }); +} + +describe('Saved replies list component', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('does not render any list items when response is empty', async () => { + const mockApollo = createMockApolloProvider(noSavedRepliesResponse); + wrapper = createComponent({ mockApollo }); + + await waitForPromises(); + + expect(wrapper.findAllComponents(ListItem).length).toBe(0); + }); + + it('render saved replies count', async () => { + const mockApollo = createMockApolloProvider(savedRepliesResponse); + wrapper = createComponent({ mockApollo }); + + await waitForPromises(); + + expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)'); + }); + + it('renders list of saved replies', async () => { + const mockApollo = createMockApolloProvider(savedRepliesResponse); + const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes; + wrapper = createComponent({ mockApollo }); + + await waitForPromises(); + + expect(wrapper.findAllComponents(ListItem).length).toBe(2); + expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual( + expect.objectContaining(savedReplies[0]), + ); + expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual( + expect.objectContaining(savedReplies[1]), + ); + }); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js index fe9b984f028..1e2ec7e8dc2 100644 --- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js @@ -130,4 +130,11 @@ describe('Work Item Discussion', () => { expect(findToggleRepliesWidget().props('collapsed')).toBe(false); }); }); + + it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => { + createComponent(); + findThreadAtIndex(0).vm.$emit('deleteNote'); + + expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]); + }); }); diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index d42d82c4e90..8f7d27def15 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -1,4 +1,4 @@ -import { GlAvatarLink } from '@gitlab/ui'; +import { GlAvatarLink, GlDropdown } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import WorkItemNote from '~/work_items/components/notes/work_item_note.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -15,6 +15,8 @@ describe('Work Item Note', () => { const findNoteHeader = () => wrapper.findComponent(NoteHeader); const findNoteBody = () => wrapper.findComponent(NoteBody); const findNoteActions = () => wrapper.findComponent(NoteActions); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]'); const createComponent = ({ note = mockWorkItemCommentNote, isFirstNote = false } = {}) => { wrapper = shallowMount(WorkItemNote, { @@ -66,4 +68,34 @@ describe('Work Item Note', () => { expect(findNoteActions().props('showReply')).toBe(false); }); }); + + it('should display a dropdown if user has a permission to delete note', () => { + createComponent({ + note: { + ...mockWorkItemCommentNote, + userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true }, + }, + }); + + expect(findDropdown().exists()).toBe(true); + }); + + it('should not display a dropdown if user has no permission to delete note', () => { + createComponent(); + + expect(findDropdown().exists()).toBe(false); + }); + + it('should emit `deleteNote` event when delete note action is clicked', () => { + createComponent({ + note: { + ...mockWorkItemCommentNote, + userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true }, + }, + }); + + findDeleteNoteButton().vm.$emit('click'); + + expect(wrapper.emitted('deleteNote')).toEqual([[]]); + }); }); diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/work_item_comment_form_spec.js index e62eb32fad0..bef7efa2536 100644 --- a/spec/frontend/work_items/components/work_item_comment_form_spec.js +++ b/spec/frontend/work_items/components/work_item_comment_form_spec.js @@ -10,7 +10,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue'; import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue'; -import createNoteMutation from '~/work_items/graphql/create_work_item_note.mutation.graphql'; +import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index df9a141d330..e5b4bee68a8 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -1,16 +1,18 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import SystemNote from '~/work_items/components/notes/system_note.vue'; import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue'; import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql'; -import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql'; +import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql'; +import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; +import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants'; import { ASC, DESC } from '~/notes/constants'; import { @@ -47,6 +49,8 @@ describe('WorkItemNotes component', () => { Vue.use(VueApollo); + const showModal = jest.fn(); + const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote); const findAllListItems = () => wrapper.findAll('ul.timeline > *'); const findActivityLabel = () => wrapper.find('label'); @@ -56,6 +60,8 @@ describe('WorkItemNotes component', () => { const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index); const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion); const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); + const findDeleteNoteModal = () => wrapper.findComponent(GlModal); + const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse); const workItemNotesByIidQueryHandler = jest .fn() @@ -64,16 +70,22 @@ describe('WorkItemNotes component', () => { const workItemNotesWithCommentsQueryHandler = jest .fn() .mockResolvedValue(mockWorkItemNotesResponseWithComments); + const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({ + data: { destroyNote: { note: null, __typename: 'DestroyNote' } }, + }); + const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false, defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, + deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler, } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ [workItemNotesQuery, defaultWorkItemNotesQueryHandler], [workItemNotesByIidQuery, workItemNotesByIidQueryHandler], + [deleteWorkItemNoteMutation, deleteWINoteMutationHandler], ]), propsData: { workItemId, @@ -89,6 +101,9 @@ describe('WorkItemNotes component', () => { useIidInWorkItemsPath: fetchByIid, }, }, + stubs: { + GlModal: stubComponent(GlModal, { methods: { show: showModal } }), + }, }); }; @@ -240,4 +255,83 @@ describe('WorkItemNotes component', () => { ); }); }); + + it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + await waitForPromises(); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false }); + expect(showModal).toHaveBeenCalled(); + }); + + describe('when modal is open', () => { + beforeEach(() => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + return waitForPromises(); + }); + + it('sends the mutation with correct variables', () => { + const noteId = 'some-test-id'; + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId }); + findDeleteNoteModal().vm.$emit('primary'); + + expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({ + input: { + id: noteId, + }, + }); + }); + + it('successfully removes the note from the discussion', async () => { + expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { + id: mockDiscussions[0].notes.nodes[0].id, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1); + }); + + it('successfully removes the discussion from work item if discussion only had one note', async () => { + const secondDiscussion = findWorkItemCommentNoteAtIndex(1); + + expect(findAllWorkItemCommentNotes()).toHaveLength(2); + expect(secondDiscussion.props('discussion')).toHaveLength(1); + + secondDiscussion.vm.$emit('deleteNote', { + id: mockDiscussions[1].notes.nodes[0].id, + discussion: { id: mockDiscussions[1].id }, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + expect(findAllWorkItemCommentNotes()).toHaveLength(1); + }); + }); + + it('emits `error` event if delete note mutation is rejected', async () => { + createComponent({ + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + deleteWINoteMutationHandler: errorHandler, + }); + await waitForPromises(); + + findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { + id: mockDiscussions[0].notes.nodes[0].id, + }); + findDeleteNoteModal().vm.$emit('primary'); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([ + ['Something went wrong when deleting a comment. Please try again'], + ]); + }); }); diff --git a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb index 7fb45e93474..a5294e96d71 100644 --- a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb +++ b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb @@ -66,7 +66,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :c it 'executes project removal for the correct direction' do expect(::Ci::JobTokenScope::RemoveProjectService) .to receive(:new).with(project, current_user).and_return(service) - expect(service).to receive(:execute).with(target_project, direction: 'inbound') + expect(service).to receive(:execute).with(target_project, 'inbound') .and_return(instance_double('ServiceResponse', "success?": true)) subject @@ -78,7 +78,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :c it 'returns an error response' do expect(::Ci::JobTokenScope::RemoveProjectService).to receive(:new).with(project, current_user).and_return(service) - expect(service).to receive(:execute).with(target_project, direction: :outbound).and_return(ServiceResponse.error(message: 'The error message')) + expect(service).to receive(:execute).with(target_project, :outbound).and_return(ServiceResponse.error(message: 'The error message')) expect(subject.fetch(:ci_job_token_scope)).to be_nil expect(subject.fetch(:errors)).to include("The error message") diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb new file mode 100644 index 00000000000..c0026f91b46 --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/database_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do + let(:database_name) { 'main' } + let(:database_indexes) do + [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']] + end + + let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) } + let(:database_model) { Gitlab::Database.database_base_models[database_name] } + let(:connection) { database_model.connection } + + subject(:database) { described_class.new(connection) } + + before do + allow(connection).to receive(:exec_query).and_return(query_result) + end + + describe '#fetch_index_by_name' do + context 'when index does not exist' do + it 'returns nil' do + index = database.fetch_index_by_name('non_existing_index') + + expect(index).to be_nil + end + end + + it 'returns index by name' do + index = database.fetch_index_by_name('index') + + expect(index.name).to eq('index') + end + end + + describe '#indexes' do + it 'returns indexes' do + indexes = database.indexes + + expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index)) + expect(indexes.map(&:name)).to eq(['index']) + end + end +end diff --git a/spec/lib/gitlab/database/schema_validation/index_spec.rb b/spec/lib/gitlab/database/schema_validation/index_spec.rb new file mode 100644 index 00000000000..297211d79ed --- /dev/null +++ b/spec/lib/gitlab/database/schema_validation/index_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do + let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' } + + let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt } + + let(:index) { described_class.new(stmt) } + + describe '#name' do + it 'returns index name' do + expect(index.name).to eq('index_name') + end + end + + describe '#statement' do + it 'returns index statement' do + expect(index.statement).to eq(index_statement) + end + end +end diff --git a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb index 337597a49b0..4351031a4b4 100644 --- a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb +++ b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :d let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } let(:database_indexes) do [ - ['wrong_index', 'CREATE UNIQUE INDEX public.wrong_index ON table_name (column_name)'], - ['extra_index', 'CREATE INDEX public.extra_index ON table_name (column_name)'], + ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'], + ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'], ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))'] ] end @@ -20,7 +20,10 @@ RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :d let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) } - subject(:schema_validation) { described_class.new(structure_file_path, database_name) } + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) } + + subject(:schema_validation) { described_class.new(structure_file, database) } before do allow(connection).to receive(:exec_query).and_return(query_result) diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 81751511270..fa0b3d1c6dd 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -124,8 +124,8 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state method: 'GET', path: enabled_path, status: status_code, - request_urgency: :low, - target_duration_s: 5, + request_urgency: :medium, + target_duration_s: 0.5, metadata: a_hash_including( { 'meta.caller_id' => 'Projects::NotesController#index', diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index ce67d1d0297..8a88328e0c1 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -5,7 +5,7 @@ require 'rspec-parameterized' require 'support/helpers/rails_helpers' RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache, - feature_category: :scalability do + :use_null_store_as_repository_cache, feature_category: :scalability do using RSpec::Parameterized::TableSyntax describe '.add_instrumentation_data', :request_store do @@ -23,42 +23,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0) end - shared_examples 'make Gitaly calls' do - context 'when Gitaly calls are made' do - it 'adds Gitaly and Redis data' do - project = create(:project) - RequestStore.clear! - project.repository.exists? + context 'when Gitaly calls are made' do + it 'adds Gitaly and Redis data' do + project = create(:project) + RequestStore.clear! + project.repository.exists? - subject - - expect(payload[:gitaly_calls]).to eq(1) - expect(payload[:gitaly_duration_s]).to be >= 0 - # With MultiStore, the number of `redis_calls` depends on whether primary_store - # (Gitlab::Redis::Repositorycache) and secondary_store (Gitlab::Redis::Cache) are of the same instance. - # In GitLab.com CI, primary and secondary are the same instance, thus only 1 call being made. If primary - # and secondary are different instances, an additional fallback read to secondary_store will be made because - # the first `get` call is a cache miss. Then, the following expect will fail. - expect(payload[:redis_calls]).to eq(1) - expect(payload[:redis_duration_ms]).to be_nil - end - end - end - - context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is enabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true) - end - - it_behaves_like 'make Gitaly calls' - end + subject - context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is disabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) + expect(payload[:gitaly_calls]).to eq(1) + expect(payload[:gitaly_duration_s]).to be >= 0 + expect(payload[:redis_calls]).to eq(nil) + expect(payload[:redis_duration_ms]).to be_nil end - - it_behaves_like 'make Gitaly calls' end context 'when Redis calls are made' do diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb index 56f77782778..8cdc4580f9e 100644 --- a/spec/lib/gitlab/redis/repository_cache_spec.rb +++ b/spec/lib/gitlab/redis/repository_cache_spec.rb @@ -4,43 +4,6 @@ require 'spec_helper' RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do include_examples "redis_new_instance_shared_examples", 'repository_cache', Gitlab::Redis::Cache - include_examples "redis_shared_examples" - - describe '#pool' do - let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } - let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } - - subject { described_class.pool } - - before do - allow(described_class).to receive(:config_file_name).and_return(config_new_format_host) - - # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists - allow(Gitlab::Redis::Cache).to receive(:rails_root).and_return(mktmpdir) - allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(config_new_format_socket) - end - - around do |example| - clear_pool - example.run - ensure - clear_pool - end - - it 'instantiates an instance of MultiStore' do - subject.with do |redis_instance| - expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore) - - expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99") - expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0") - - expect(redis_instance.instance_name).to eq('RepositoryCache') - end - end - - it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_repository_cache, - :use_primary_store_as_default_for_repository_cache - end describe '#raw_config_hash' do it 'has a legacy default URL' do @@ -49,4 +12,10 @@ RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380') end end + + describe '.cache_store' do + it 'has a default ttl of 8 hours' do + expect(described_class.cache_store.options[:expires_in]).to eq(8.hours) + end + end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index ce5d0cfa632..406de3403f3 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1089,4 +1089,73 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do it { is_expected.not_to match('random string') } it { is_expected.not_to match('12321342545356434523412341245452345623453542345234523453245') } end + + describe 'code, html blocks, or html comment blocks regex' do + context 'code blocks' do + subject { described_class::MARKDOWN_CODE_BLOCK_REGEX } + + let(:expected) { %(```code\nsome code\n\n>>>\nthat includes a multiline-blockquote\n>>>\n```) } + let(:markdown) do + <<~MARKDOWN + Regular text + + ```code + some code + + >>> + that includes a multiline-blockquote + >>> + ``` + MARKDOWN + end + + it { is_expected.to match(%(```ruby\nsomething\n```)) } + it { is_expected.not_to match(%(must start in first column ```ruby\nsomething\n```)) } + it { is_expected.not_to match(%(```ruby must be multi-line ```)) } + it { expect(subject.match(markdown)[:code]).to eq expected } + end + + context 'HTML blocks' do + subject { described_class::MARKDOWN_HTML_BLOCK_REGEX } + + let(:expected) { %(<section>\n<p>paragraph</p>\n\n>>>\nthat includes a multiline-blockquote\n>>>\n</section>) } + let(:markdown) do + <<~MARKDOWN + Regular text + + <section> + <p>paragraph</p> + + >>> + that includes a multiline-blockquote + >>> + </section> + MARKDOWN + end + + it { is_expected.to match(%(<section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) } + it { is_expected.not_to match(%(<section>must be multi-line</section>)) } + it { expect(subject.match(markdown)[:html]).to eq expected } + end + + context 'HTML comment blocks' do + subject { described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX } + + let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\n-->) } + let(:markdown) do + <<~MARKDOWN + Regular text + + <!-- the start of an HTML comment + - [ ] list item commented out + --> + MARKDOWN + end + + it { is_expected.to match(%(<!--\ncomment\n-->)) } + it { is_expected.not_to match(%(must start in first column <!--\ncomment\n-->)) } + it { expect(subject.match(markdown)[:html_block_comment]).to eq expected } + end + end end diff --git a/spec/lib/gitlab/repository_cache/preloader_spec.rb b/spec/lib/gitlab/repository_cache/preloader_spec.rb index 21628481fed..e6fb0da6412 100644 --- a/spec/lib/gitlab/repository_cache/preloader_spec.rb +++ b/spec/lib/gitlab/repository_cache/preloader_spec.rb @@ -6,76 +6,51 @@ RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_redis_cachin feature_category: :source_code_management do let(:projects) { create_list(:project, 2, :repository) } let(:repositories) { projects.map(&:repository) } + let(:cache) { Gitlab::RepositoryCache.store } - before do - stub_feature_flags(use_primary_store_as_default_for_repository_cache: false) - end - - shared_examples 'preload' do - describe '#preload' do - context 'when the values are already cached' do - before do - # Warm the cache but use a different model so they are not memoized - repos = Project.id_in(projects).order(:id).map(&:repository) - - allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt') - allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md') - - repos.map(&:exists?) - repos.map(&:readme_path) - end - - it 'prevents individual cache reads for cached methods' do - expect(cache).to receive(:read_multi).once.and_call_original - - described_class.new(repositories).preload( - %i[exists? readme_path] - ) - - expect(cache).not_to receive(:read) - expect(cache).not_to receive(:write) + describe '#preload' do + context 'when the values are already cached' do + before do + # Warm the cache but use a different model so they are not memoized + repos = Project.id_in(projects).order(:id).map(&:repository) - expect(repositories[0].exists?).to eq(true) - expect(repositories[0].readme_path).to eq('README.txt') + allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt') + allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md') - expect(repositories[1].exists?).to eq(true) - expect(repositories[1].readme_path).to eq('README.md') - end + repos.map(&:exists?) + repos.map(&:readme_path) end - context 'when values are not cached' do - it 'reads and writes from cache individually' do - described_class.new(repositories).preload( - %i[exists? has_visible_content?] - ) + it 'prevents individual cache reads for cached methods' do + expect(cache).to receive(:read_multi).once.and_call_original - expect(cache).to receive(:read).exactly(4).times - expect(cache).to receive(:write).exactly(4).times + described_class.new(repositories).preload( + %i[exists? readme_path] + ) - repositories.each(&:exists?) - repositories.each(&:has_visible_content?) - end - end - end - end + expect(cache).not_to receive(:read) + expect(cache).not_to receive(:write) - context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is enabled' do - let(:cache) { Gitlab::RepositoryCache.store } + expect(repositories[0].exists?).to eq(true) + expect(repositories[0].readme_path).to eq('README.txt') - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true) + expect(repositories[1].exists?).to eq(true) + expect(repositories[1].readme_path).to eq('README.md') + end end - it_behaves_like 'preload' - end + context 'when values are not cached' do + it 'reads and writes from cache individually' do + described_class.new(repositories).preload( + %i[exists? has_visible_content?] + ) - context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is disabled' do - let(:cache) { Rails.cache } + expect(cache).to receive(:read).exactly(4).times + expect(cache).to receive(:write).exactly(4).times - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) + repositories.each(&:exists?) + repositories.each(&:has_visible_content?) + end end - - it_behaves_like 'preload' end end diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb index d41bf45f72e..6b52c315a70 100644 --- a/spec/lib/gitlab/repository_hash_cache_spec.rb +++ b/spec/lib/gitlab/repository_hash_cache_spec.rb @@ -69,35 +69,20 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do end end - shared_examples "key?" do - describe "#key?" do - subject { cache.key?(:example, "test") } + describe "#key?" do + subject { cache.key?(:example, "test") } - context "key exists" do - before do - cache.write(:example, test_hash) - end - - it { is_expected.to be(true) } + context "key exists" do + before do + cache.write(:example, test_hash) end - context "key doesn't exist" do - it { is_expected.to be(false) } - end + it { is_expected.to be(true) } end - end - - context "when both multistore FF is enabled" do - it_behaves_like "key?" - end - context "when both multistore FF is disabled" do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) - stub_feature_flags(use_primary_store_as_default_for_repository_cache: false) + context "key doesn't exist" do + it { is_expected.to be(false) } end - - it_behaves_like "key?" end describe "#read_members" do diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb index 140f6cda51c..0ad29454ff3 100644 --- a/spec/models/concerns/taskable_spec.rb +++ b/spec/models/concerns/taskable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Taskable do +RSpec.describe Taskable, feature_category: :team_planning do using RSpec::Parameterized::TableSyntax describe '.get_tasks' do @@ -13,8 +13,18 @@ RSpec.describe Taskable do - [x] Second item * [x] First item * [ ] Second item + + <!-- a comment + - [ ] Item in comment, ignore + rest of comment --> + + [ ] No-break space (U+00A0) + [ ] Figure space (U+2007) + + ``` + - [ ] Item in code, ignore + ``` + + [ ] Narrow no-break space (U+202F) + [ ] Thin space (U+2009) MARKDOWN diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb index 805f300072d..f1296c054f9 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb @@ -76,6 +76,15 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1) end + it 'responds successfully' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_errors).to be_nil + expect(graphql_data_at(:ciJobTokenScopeRemoveProject, :ciJobTokenScope, :projects, :nodes)) + .to contain_exactly({ 'path' => project.path }) + end + context 'when invalid target project is provided' do before do variables[:target_project_path] = 'unknown/project' diff --git a/spec/requests/profiles/saved_replies_controller_spec.rb b/spec/requests/profiles/saved_replies_controller_spec.rb new file mode 100644 index 00000000000..27a961a201f --- /dev/null +++ b/spec/requests/profiles/saved_replies_controller_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET #index' do + describe 'feature flag disabled' do + before do + stub_feature_flags(saved_replies: false) + + get '/-/profile/saved_replies' + end + + it { expect(response).to have_gitlab_http_status(:not_found) } + end + + describe 'feature flag enabled' do + before do + get '/-/profile/saved_replies' + end + + it { expect(response).to have_gitlab_http_status(:ok) } + + it 'sets hide search settings ivar' do + expect(assigns(:hide_search_settings)).to eq(true) + end + end + end +end diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb index 084cf2d4a5c..55540447da0 100644 --- a/spec/requests/projects/noteable_notes_spec.rb +++ b/spec/requests/projects/noteable_notes_spec.rb @@ -43,7 +43,7 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do expect(Gitlab::Metrics::RailsSlis.request_apdex).to( receive(:increment).with( labels: { - request_urgency: :low, + request_urgency: :medium, feature_category: "team_planning", endpoint_id: "Projects::NotesController#index" }, @@ -57,8 +57,8 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do 'process_action.action_controller', a_hash_including( { - request_urgency: :low, - target_duration_s: 5, + request_urgency: :medium, + target_duration_s: 0.5, metadata: a_hash_including({ 'meta.feature_category' => 'team_planning', 'meta.caller_id' => "Projects::NotesController#index" diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb index 0390e232fd5..32269e5475b 100644 --- a/spec/serializers/codequality_degradation_entity_spec.rb +++ b/spec/serializers/codequality_degradation_entity_spec.rb @@ -18,6 +18,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:file_path]).to eq("file_a.rb") expect(subject[:line]).to eq(10) expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10") + expect(subject[:engine_name]).to eq('structure') end end @@ -30,6 +31,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:file_path]).to eq("file_b.rb") expect(subject[:line]).to eq(10) expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10") + expect(subject[:engine_name]).to eq('rubocop') end end @@ -46,6 +48,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:file_path]).to eq("file_b.rb") expect(subject[:line]).to eq(10) expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10") + expect(subject[:engine_name]).to eq('rubocop') end end end diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb index e154d8c0422..5b39f8908f2 100644 --- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb +++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :conti end describe '#execute' do - subject(:result) { service.execute(target_project) } + subject(:result) { service.execute(target_project, :outbound) } it_behaves_like 'editable job token scope' do context 'when user has permissions on source and target project' do diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 2c1ebe27014..be059aec697 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -RSpec.describe Projects::ImportExport::ExportService do +RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do describe '#execute' do let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } - let(:project) { create(:project) } let(:shared) { project.import_export_shared } let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } @@ -220,5 +221,21 @@ RSpec.describe Projects::ImportExport::ExportService do expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message) end end + + it "avoids N+1 when exporting project members" do + group.add_owner(user) + group.add_maintainer(create(:user)) + project.add_maintainer(create(:user)) + + # warm up + service.execute + + control = ActiveRecord::QueryRecorder.new { service.execute } + + group.add_maintainer(create(:user)) + project.add_maintainer(create(:user)) + + expect { service.execute }.not_to exceed_query_limit(control) + end end end diff --git a/spec/support/redis.rb b/spec/support/redis.rb index 6d313c8aa16..d5ae0bf1582 100644 --- a/spec/support/redis.rb +++ b/spec/support/redis.rb @@ -25,4 +25,10 @@ RSpec.configure do |config| instance_class.with(&:flushdb) end end + + config.before(:each, :use_null_store_as_repository_cache) do |example| + null_store = ActiveSupport::Cache::NullStore.new + + allow(Gitlab::Redis::RepositoryCache).to receive(:cache_store).and_return(null_store) + end end diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb index 9b6ea3891d9..375d01bf2ba 100644 --- a/spec/tasks/cache/clear/redis_spec.rb +++ b/spec/tasks/cache/clear/redis_spec.rb @@ -3,7 +3,7 @@ require 'rake_helper' RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache, - :silence_stdout, feature_category: :redis do + :silence_stdout, :use_null_store_as_repository_cache, feature_category: :redis do before do Rake.application.rake_require 'tasks/cache' end @@ -20,37 +20,11 @@ RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :cl create(:ci_pipeline, project: project).project.pipeline_status end - context 'when use_primary_and_secondary_stores_for_repository_cache MultiStore FF is enabled' do - # Initially, project:{id}:pipeline_status is explicitly cached in Gitlab::Redis::Cache, whereas repository is - # cached in Rails.cache (which is a NullStore). - # With the MultiStore feature flag enabled, we use Gitlab::Redis::RepositoryCache instance as primary store and - # Gitlab::Redis::Cache as secondary store. - # This ends up storing 2 extra keys (exists? and root_ref) in both Gitlab::Redis::RepositoryCache and - # Gitlab::Redis::Cache instances when loading project.pipeline_status - let(:keys_size_changed) { -3 } - - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true) - allow(pipeline_status).to receive(:loaded).and_return(nil) - end - - it 'clears pipeline status cache' do - expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? } - end - - it_behaves_like 'clears the cache' + before do + allow(pipeline_status).to receive(:loaded).and_return(nil) end - context 'when use_primary_and_secondary_stores_for_repository_cache and - use_primary_store_as_default_for_repository_cache feature flags are disabled' do - before do - stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false) - stub_feature_flags(use_primary_store_as_default_for_repository_cache: false) - allow(pipeline_status).to receive(:loaded).and_return(nil) - end - - it_behaves_like 'clears the cache' - end + it_behaves_like 'clears the cache' end describe 'clearing set caches' do diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index c583631e4d5..c0d46a206ce 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -364,6 +364,7 @@ RSpec.describe 'Every Sidekiq worker' do 'Onboarding::PipelineCreatedWorker' => 3, 'Onboarding::ProgressWorker' => 3, 'Onboarding::UserAddedWorker' => 3, + 'Namespaces::FreeUserCap::OverLimitNotificationWorker' => false, 'Namespaces::RefreshRootStatisticsWorker' => 3, 'Namespaces::RootStatisticsWorker' => 3, 'Namespaces::ScheduleAggregationWorker' => 3, |