Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 21:07:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-09 21:07:44 +0300
commit453634293e24164ffaa5cd708e47a1cebc07bcd3 (patch)
treee49bb067fc508f57b03ac582872c4b1215eec326 /spec
parent608d6aaa3d80a33862ca2c29d96bfd687b1a011b (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb9
-rw-r--r--spec/features/broadcast_messages_spec.rb3
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb7
-rw-r--r--spec/features/profiles/keys_spec.rb3
-rw-r--r--spec/features/profiles/list_users_saved_replies_spec.rb21
-rw-r--r--spec/features/task_lists_spec.rb72
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_degradation.json5
-rw-r--r--spec/frontend/fixtures/saved_replies.rb46
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap21
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js22
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js68
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js7
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_comment_form_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js100
-rw-r--r--spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb4
-rw-r--r--spec/lib/gitlab/database/schema_validation/database_spec.rb45
-rw-r--r--spec/lib/gitlab/database/schema_validation/index_spec.rb22
-rw-r--r--spec/lib/gitlab/database/schema_validation/indexes_spec.rb9
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb45
-rw-r--r--spec/lib/gitlab/redis/repository_cache_spec.rb43
-rw-r--r--spec/lib/gitlab/regex_spec.rb69
-rw-r--r--spec/lib/gitlab/repository_cache/preloader_spec.rb89
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb31
-rw-r--r--spec/models/concerns/taskable_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb9
-rw-r--r--spec/requests/profiles/saved_replies_controller_spec.rb35
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb6
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb3
-rw-r--r--spec/services/ci/job_token_scope/remove_project_service_spec.rb2
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb21
-rw-r--r--spec/support/redis.rb6
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb34
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
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,