summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-22 13:39:34 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-22 13:39:34 +0000
commite23c409e66b47a970a3cb83ac15d2ff906e75ce0 (patch)
treeb1d580cd64c5d67a81a9445da42e82ceeefa96c5 /spec
parent2fa173410ad24b37aba6450ae4530ec231844d86 (diff)
downloadgitlab-ce-e23c409e66b47a970a3cb83ac15d2ff906e75ce0.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb2
-rw-r--r--spec/features/groups/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb1
-rw-r--r--spec/features/help_dropdown_spec.rb4
-rw-r--r--spec/features/projects/branches_spec.rb8
-rw-r--r--spec/features/projects/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb1
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb4
-rw-r--r--spec/features/projects/wikis_spec.rb4
-rw-r--r--spec/fixtures/packages/rubygems/package.gemspec2
-rw-r--r--spec/frontend/listbox/index_spec.js166
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js7
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js30
-rw-r--r--spec/frontend/work_items/mock_data.js16
-rw-r--r--spec/frontend/work_items/router_spec.js6
-rw-r--r--spec/graphql/mutations/work_items/update_widgets_spec.rb58
-rw-r--r--spec/helpers/groups_helper_spec.rb26
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/presenters/commit_presenter_spec.rb5
-rw-r--r--spec/presenters/projects/security/configuration_presenter_spec.rb31
-rw-r--r--spec/requests/api/graphql/milestone_spec.rb199
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb61
-rw-r--r--spec/requests/api/groups_spec.rb34
-rw-r--r--spec/support/matchers/event_store.rb48
-rw-r--r--spec/support_specs/matchers/event_store_spec.rb126
-rw-r--r--spec/views/layouts/header/_gitlab_version.html.haml_spec.rb6
27 files changed, 569 insertions, 294 deletions
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index 2cf56f93cf9..cb9188cf171 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Dashboard > User filters projects' do
let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/groups/labels/sort_labels_spec.rb b/spec/features/groups/labels/sort_labels_spec.rb
index fba166449f8..af4d39bc6fa 100644
--- a/spec/features/groups/labels/sort_labels_spec.rb
+++ b/spec/features/groups/labels/sort_labels_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Sort labels', :js do
let!(:label2) { create(:group_label, title: 'Bar', description: 'Fusce consequat', group: group) }
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
+
group.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index 22d7ff91d41..631aa940270 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Milestones sorting', :js do
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
end
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index e64c19d4708..a9c014a9408 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -59,6 +59,10 @@ RSpec.describe "Help Dropdown", :js do
expect(page).to have_text('Your GitLab Version')
expect(page).to have_text("#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}")
expect(page).to have_selector('.version-check-badge')
+ expect(page).to have_selector(
+ 'a[data-testid="gitlab-version-container"][href="/help/update/index"]'
+ )
+ expect(page).to have_selector('.version-check-badge[href="/help/update/index"]')
expect(page).to have_text(ui_text)
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 727f9aa486e..361a07ebd0b 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'Branches' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
let(:repository) { project.repository }
context 'logged in as developer' do
@@ -175,7 +175,7 @@ RSpec.describe 'Branches' do
search_for_branch('fix')
expect(page).not_to have_content('fix')
- expect(all('.all-branches').last).to have_selector('li', count: 0)
+ expect(all('.all-branches', wait: false).last).to have_selector('li', count: 0)
end
end
@@ -233,7 +233,7 @@ RSpec.describe 'Branches' do
end
context 'with one or more pipeline', :js do
- let(:project) { create(:project, :public, :empty_repo) }
+ let_it_be(:project) { create(:project, :public, :empty_repo) }
before do
sha = create_file(branch_name: "branch")
diff --git a/spec/features/projects/labels/sort_labels_spec.rb b/spec/features/projects/labels/sort_labels_spec.rb
index ecbc4b524dc..6a16f474056 100644
--- a/spec/features/projects/labels/sort_labels_spec.rb
+++ b/spec/features/projects/labels/sort_labels_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Sort labels', :js do
let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) }
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index c47350fb663..5c379ac1034 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe 'Milestones sorting', :js do
end
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
create(:milestone, start_date: 7.days.from_now, due_date: 10.days.from_now, title: "a", project: project)
create(:milestone, start_date: 6.days.from_now, due_date: 11.days.from_now, title: "c", project: project)
create(:milestone, start_date: 5.days.from_now, due_date: 12.days.from_now, title: "b", project: project)
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index b9b28398279..15f7dae502d 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe 'User sorts projects and order persists' do
context "from explore projects" do
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
visit(explore_projects_path)
find('#sort-projects-dropdown').click
@@ -55,6 +56,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from dashboard projects' do
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
visit(dashboard_projects_path)
find('#sort-projects-dropdown').click
@@ -66,6 +68,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group homepage', :js do
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(group_canonical_path(group))
@@ -80,6 +83,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group details', :js do
before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(details_group_path(group))
diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb
index 879ffd2932b..8ac17413df3 100644
--- a/spec/features/projects/wikis_spec.rb
+++ b/spec/features/projects/wikis_spec.rb
@@ -3,6 +3,10 @@
require "spec_helper"
RSpec.describe 'Project wikis', :js do
+ before do
+ stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
+ end
+
let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) }
diff --git a/spec/fixtures/packages/rubygems/package.gemspec b/spec/fixtures/packages/rubygems/package.gemspec
index ea03414cc6f..60acd078fad 100644
--- a/spec/fixtures/packages/rubygems/package.gemspec
+++ b/spec/fixtures/packages/rubygems/package.gemspec
@@ -30,7 +30,7 @@ Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.post_install_message = 'Installed, thank you!'
s.rdoc_options = ['--main', 'README.md']
- s.required_ruby_version = '>= 2.7.0'
+ s.required_ruby_version = '>= 2.7.0' # rubocop:disable Gemspec/RequiredRubyVersion
s.required_rubygems_version = '>= 1.8.11'
s.requirements = 'A high powered server or calculator'
s.rubygems_version = '1.8.09'
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index 07c6cca535a..c973960e683 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
-import { getAllByRole, getByRole } from '@testing-library/dom';
-import { GlDropdown } from '@gitlab/ui';
+import { getAllByRole, getByRole, getByTestId } from '@testing-library/dom';
+import { GlDropdown, GlListbox } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -28,20 +28,6 @@ describe('initListbox', () => {
instance = initListbox(...args);
};
- // TODO: Rewrite these finders to use better semantics once the
- // implementation is switched to GlListbox
- // https://gitlab.com/gitlab-org/gitlab/-/issues/348738
- const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
- const findItem = (text) => getByRole(document.body, 'menuitem', { name: text });
- const findItems = () => getAllByRole(document.body, 'menuitem');
- const findSelectedItems = () =>
- findItems().filter(
- (menuitem) =>
- !menuitem
- .querySelector('.gl-new-dropdown-item-check-icon')
- .classList.contains('gl-visibility-hidden'),
- );
-
it('returns null given no element', () => {
setup();
@@ -53,63 +39,141 @@ describe('initListbox', () => {
});
describe('given a valid element', () => {
- let onChangeSpy;
+ describe('when `glListboxForSortDropdowns` FF is enabled', () => {
+ let onChangeSpy;
- beforeEach(async () => {
- setHTMLFixture(fixture);
- onChangeSpy = jest.fn();
- setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
+ const listbox = () => createWrapper(instance).findComponent(GlListbox);
+ const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
+ const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
- await nextTick();
- });
+ beforeEach(async () => {
+ window.gon.features = { glListboxForSortDropdowns: true };
+ setHTMLFixture(fixture);
+ onChangeSpy = jest.fn();
+ setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
- afterEach(() => {
- resetHTMLFixture();
- });
+ await nextTick();
+ });
- it('returns an instance', () => {
- expect(instance).not.toBe(null);
- });
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- it('renders button with selected item text', () => {
- expect(findToggleButton().textContent.trim()).toBe('Bar');
- });
+ it('returns an instance', () => {
+ expect(instance).not.toBe(null);
+ });
- it('has the correct item selected', () => {
- const selectedItems = findSelectedItems();
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe('Bar');
- });
+ it('renders button with selected item text', () => {
+ expect(findToggleButton().textContent.trim()).toBe('Bar');
+ });
- it('applies additional classes from the original element', () => {
- expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
+ it('has the correct item selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe('Bar');
+ });
+
+ it('applies additional classes from the original element', () => {
+ expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
+ });
+
+ describe.each(parsedAttributes.items)('selecting an item', (item) => {
+ beforeEach(async () => {
+ listbox().vm.$emit('select', item.value);
+ await nextTick();
+ });
+
+ it('calls the onChange callback with the item', () => {
+ expect(onChangeSpy).toHaveBeenCalledWith(item);
+ });
+
+ it('updates the toggle button text', () => {
+ expect(findToggleButton().textContent.trim()).toBe(item.text);
+ });
+
+ it('marks the item as selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe(item.text);
+ });
+ });
+
+ it('passes the "right" prop through to the underlying component', () => {
+ expect(listbox().props('right')).toBe(parsedAttributes.right);
+ });
});
- describe.each(parsedAttributes.items)('clicking on an item', (item) => {
+ describe('when `glListboxForSortDropdowns` FF is disabled', () => {
+ let onChangeSpy;
+
+ const ITEM_ROLE = 'menuitem';
+ const dropdown = () => createWrapper(instance).findComponent(GlDropdown);
+
+ const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
+ const findItem = (text) => getByRole(document.body, ITEM_ROLE, { name: text });
+ const findItems = () => getAllByRole(document.body, ITEM_ROLE);
+ const findSelectedItems = () =>
+ findItems().filter(
+ (item) =>
+ !item
+ .querySelector('.gl-new-dropdown-item-check-icon')
+ .classList.contains('gl-visibility-hidden'),
+ );
beforeEach(async () => {
- findItem(item.text).click();
+ window.gon.features = { glListboxForSortDropdowns: false };
+ setHTMLFixture(fixture);
+ onChangeSpy = jest.fn();
+ setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
await nextTick();
});
- it('calls the onChange callback with the item', () => {
- expect(onChangeSpy).toHaveBeenCalledWith(item);
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('returns an instance', () => {
+ expect(instance).not.toBe(null);
});
- it('updates the toggle button text', () => {
- expect(findToggleButton().textContent.trim()).toBe(item.text);
+ it('renders button with selected item text', () => {
+ expect(findToggleButton().textContent.trim()).toBe('Bar');
});
- it('marks the item as selected', () => {
+ it('has the correct item selected', () => {
const selectedItems = findSelectedItems();
expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe(item.text);
+ expect(selectedItems[0].textContent.trim()).toBe('Bar');
+ });
+
+ it('applies additional classes from the original element', () => {
+ expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
});
- });
- it('passes the "right" prop through to the underlying component', () => {
- const wrapper = createWrapper(instance).findComponent(GlDropdown);
- expect(wrapper.props('right')).toBe(parsedAttributes.right);
+ describe.each(parsedAttributes.items)('selecting an item', (item) => {
+ beforeEach(async () => {
+ findItem(item.text).click();
+ await nextTick();
+ });
+
+ it('calls the onChange callback with the item', () => {
+ expect(onChangeSpy).toHaveBeenCalledWith(item);
+ });
+
+ it('updates the toggle button text', () => {
+ expect(findToggleButton().textContent.trim()).toBe(item.text);
+ });
+
+ it('marks the item as selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe(item.text);
+ });
+ });
+
+ it('passes the "right" prop through to the underlying component', () => {
+ expect(dropdown().props('right')).toBe(parsedAttributes.right);
+ });
});
});
});
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
index 6da1861ea7c..0d52542397f 100644
--- a/spec/frontend/repository/components/blob_controls_spec.js
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -8,9 +8,13 @@ import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql'
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createRouter from '~/repository/router';
import { updateElementsVisibility } from '~/repository/utils/dom';
+import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import { blobControlsDataMock, refMock } from '../mock_data';
jest.mock('~/repository/utils/dom');
+jest.mock('~/behaviors/shortcuts/shortcuts_blob');
+jest.mock('~/blob/blob_line_permalink_updater');
let router;
let wrapper;
@@ -82,4 +86,12 @@ describe('Blob controls component', () => {
expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true);
},
);
+
+ it('loads the ShortcutsBlob', () => {
+ expect(ShortcutsBlob).toHaveBeenCalled();
+ });
+
+ it('loads the BlobLinePermalinkUpdater', () => {
+ expect(BlobLinePermalinkUpdater).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
index 6699ae5fb69..f6bb8f5de6c 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
@@ -9,6 +10,8 @@ describe('GitlabVersionCheck', () => {
let wrapper;
let mock;
+ const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
const defaultResponse = {
code: 200,
res: { severity: 'success' },
@@ -102,6 +105,10 @@ describe('GitlabVersionCheck', () => {
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
+
+ it(`link is ${UPGRADE_DOCS_URL}`, () => {
+ expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index b047e0dc8d7..b5111fdcae4 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -19,6 +19,7 @@ import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { temporaryConfig } from '~/graphql_shared/issuable_client';
@@ -29,6 +30,7 @@ import {
workItemResponseFactory,
workItemTitleSubscriptionResponse,
workItemWeightSubscriptionResponse,
+ workItemAssigneesSubscriptionResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -46,6 +48,9 @@ describe('WorkItemDetail component', () => {
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const assigneesSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemAssigneesSubscriptionResponse);
const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -80,6 +85,7 @@ describe('WorkItemDetail component', () => {
[workItemQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
+ [workItemAssigneesSubscription, assigneesSubscriptionHandler],
confidentialityMock,
];
@@ -413,6 +419,30 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('assignees subscription', () => {
+ describe('when the assignees widget exists', () => {
+ it('calls the assignees subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the assignees widget does not exist', () => {
+ it('does not call the assignees subscription', async () => {
+ const response = workItemResponseFactory({ assigneesWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(assigneesSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('dates subscription', () => {
describe('when the due date widget exists', () => {
it('calls the dates subscription', async () => {
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index e1bc8d2f6b7..d1108a57e23 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -444,6 +444,22 @@ export const workItemWeightSubscriptionResponse = {
},
};
+export const workItemAssigneesSubscriptionResponse = {
+ data: {
+ issuableAssigneesUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemAssigneesWeight',
+ assignees: {
+ nodes: [mockAssignees[0]],
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index ab370e2ca8b..39e8eb837c0 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
+ workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
@@ -13,6 +14,7 @@ import App from '~/work_items/components/app.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
@@ -26,6 +28,9 @@ describe('Work items router', () => {
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
+ const assigneesSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemAssigneesSubscriptionResponse);
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -37,6 +42,7 @@ describe('Work items router', () => {
[workItemQuery, workItemQueryHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemTitleSubscription, titleSubscriptionHandler],
+ [workItemAssigneesSubscription, assigneesSubscriptionHandler],
];
if (IS_EE) {
diff --git a/spec/graphql/mutations/work_items/update_widgets_spec.rb b/spec/graphql/mutations/work_items/update_widgets_spec.rb
deleted file mode 100644
index 2e54b81b5c7..00000000000
--- a/spec/graphql/mutations/work_items/update_widgets_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Mutations::WorkItems::UpdateWidgets do
- include GraphqlHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
-
- let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
-
- describe '#resolve' do
- before do
- stub_spam_services
- end
-
- context 'when no work item matches the given id' do
- let(:current_user) { developer }
- let(:gid) { global_id_of(id: non_existing_record_id, model_name: WorkItem.name) }
-
- it 'raises an error' do
- expect { mutation.resolve(id: gid, resolve: true) }.to raise_error(
- Gitlab::Graphql::Errors::ResourceNotAvailable,
- Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
- )
- end
- end
-
- context 'when user can access the requested work item', :aggregate_failures do
- let(:current_user) { developer }
- let(:args) { {} }
-
- let_it_be(:work_item) { create(:work_item, project: project) }
-
- subject { mutation.resolve(id: work_item.to_global_id, **args) }
-
- context 'when `:work_items` is disabled for a project' do
- let_it_be(:project2) { create(:project) }
-
- it 'returns an error' do
- stub_feature_flags(work_items: project2) # only enable `work_item` for project2
-
- expect(subject[:errors]).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
-
- context 'when resolved with an input for description widget' do
- let(:args) { { description_widget: { description: "updated description" } } }
-
- it 'returns the updated work item' do
- expect(subject[:work_item].description).to eq("updated description")
- expect(subject[:errors]).to be_empty
- end
- end
- end
- end
-end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 00e620832b3..0b6e51c26f5 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -388,22 +388,30 @@ RSpec.describe GroupsHelper do
end
describe '#show_thanks_for_purchase_alert?' do
- subject { helper.show_thanks_for_purchase_alert? }
+ subject { helper.show_thanks_for_purchase_alert?(quantity) }
- it 'returns true with purchased_quantity present in params' do
- allow(controller).to receive(:params) { { purchased_quantity: '1' } }
+ context 'with quantity present' do
+ let(:quantity) { 1 }
- is_expected.to be_truthy
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
end
- it 'returns false with purchased_quantity not present in params' do
- is_expected.to be_falsey
+ context 'with quantity not present' do
+ let(:quantity) { nil }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
- it 'returns false with purchased_quantity is empty in params' do
- allow(controller).to receive(:params) { { purchased_quantity: '' } }
+ context 'with quantity empty' do
+ let(:quantity) { '' }
- is_expected.to be_falsey
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7ee381b29ea..190227080cd 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -5362,7 +5362,7 @@ RSpec.describe Ci::Build do
end
describe '#clone' do
- let_it_be(:user) { FactoryBot.build(:user) }
+ let_it_be(:user) { create(:user) }
context 'when given new job variables' do
context 'when the cloned build has an action' do
diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb
index df3ee69621b..eba393da2b7 100644
--- a/spec/presenters/commit_presenter_spec.rb
+++ b/spec/presenters/commit_presenter_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe CommitPresenter do
- let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
- let(:user) { create(:user) }
let(:presenter) { described_class.new(commit, current_user: user) }
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
describe '#web_path' do
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/commit/#{commit.sha}") }
end
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index 05e5a9d4f1d..ca7f96b567d 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -6,9 +6,8 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
include Gitlab::Routing.url_helpers
using RSpec::Parameterized::TableSyntax
- let(:project_with_repo) { create(:project, :repository) }
- let(:project_with_no_repo) { create(:project) }
- let(:current_user) { create(:user) }
+ let_it_be(:current_user) { build_stubbed(:user) }
+
let(:presenter) { described_class.new(project, current_user: current_user) }
before do
@@ -19,9 +18,9 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
subject(:html_data) { presenter.to_html_data_attribute }
context 'when latest default branch pipeline`s source is not auto devops' do
- let(:project) { project_with_repo }
+ let_it_be(:project) { create(:project, :repository) }
- let(:pipeline) do
+ let_it_be(:pipeline) do
create(
:ci_pipeline,
project: project,
@@ -119,6 +118,16 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
context 'when the job has more than one report' do
let(:features) { Gitlab::Json.parse(html_data[:features]) }
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha
+ )
+ end
let!(:artifacts) do
{ artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } }
@@ -161,6 +170,8 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
context "while retrieving information about gitlab ci file" do
+ let(:project) { create(:project, :repository) }
+
context 'when a .gitlab-ci.yml file exists' do
let!(:ci_config) do
project.repository.create_file(
@@ -189,7 +200,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
context 'when the project is empty' do
- let(:project) { project_with_no_repo }
+ let(:project) { create(:project) }
it 'includes a blank gitlab_ci history path' do
expect(html_data[:gitlab_ci_history_path]).to eq('')
@@ -197,7 +208,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
context 'when the project has no default branch set' do
- let(:project) { project_with_repo }
+ let(:project) { create(:project, :repository) }
it 'includes the path to gitlab_ci history' do
allow(project).to receive(:default_branch).and_return(nil)
@@ -207,9 +218,9 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
context "when the latest default branch pipeline's source is auto devops" do
- let(:project) { project_with_repo }
+ let_it_be(:project) { create(:project, :repository) }
- let(:pipeline) do
+ let_it_be(:pipeline) do
create(
:ci_pipeline,
:auto_devops_source,
@@ -256,7 +267,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
context 'when the project has no default branch pipeline' do
- let(:project) { project_with_repo }
+ let_it_be(:project) { create(:project, :repository) }
it 'reports that auto devops is disabled' do
expect(html_data[:auto_devops_enabled]).to be_falsy
diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb
index f6835936418..78e7ec39ee3 100644
--- a/spec/requests/api/graphql/milestone_spec.rb
+++ b/spec/requests/api/graphql/milestone_spec.rb
@@ -5,8 +5,12 @@ require 'spec_helper'
RSpec.describe 'Querying a Milestone' do
include GraphqlHelpers
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:guest) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:inherited_guest) { create(:user) }
+ let_it_be(:inherited_reporter) { create(:user) }
+ let_it_be(:inherited_developer) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:release_a) { create(:release, project: project) }
let_it_be(:release_b) { create(:release, project: project) }
@@ -14,116 +18,137 @@ RSpec.describe 'Querying a Milestone' do
before_all do
milestone.releases << [release_a, release_b]
project.add_guest(guest)
+ group.add_guest(inherited_guest)
+ group.add_reporter(inherited_reporter)
+ group.add_developer(inherited_developer)
end
let(:expected_release_nodes) do
contain_exactly(a_graphql_entity_for(release_a), a_graphql_entity_for(release_b))
end
- context 'when we post the query' do
- let(:current_user) { nil }
- let(:query) do
- graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, all_graphql_fields_for('Milestone'))
- end
+ shared_examples 'returns the milestone successfully' do
+ it_behaves_like 'a working graphql query'
- subject { graphql_data['milestone'] }
+ it { is_expected.to include('title' => milestone.name) }
- before do
- post_graphql(query, current_user: current_user)
+ it 'contains release information' do
+ is_expected.to include('releases' => include('nodes' => expected_release_nodes))
end
+ end
- context 'when the user has access to the milestone' do
- let(:current_user) { guest }
-
- it_behaves_like 'a working graphql query'
-
- it { is_expected.to include('title' => milestone.name) }
-
- it 'contains release information' do
- is_expected.to include('releases' => include('nodes' => expected_release_nodes))
+ context 'when we post the query' do
+ context 'and the project is private' do
+ let(:query) do
+ graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, all_graphql_fields_for('Milestone'))
end
- end
- context 'when the user does not have access to the milestone' do
- it_behaves_like 'a working graphql query'
-
- it { is_expected.to be_nil }
- end
+ subject { graphql_data['milestone'] }
- context 'when ID argument is missing' do
- let(:query) do
- graphql_query_for('milestone', {}, 'title')
+ before do
+ post_graphql(query, current_user: current_user)
end
- it 'raises an exception' do
- expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id"))
+ context 'when the user is a direct project member' do
+ context 'and the user is a guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'returns the milestone successfully'
+
+ context 'when there are two milestones' do
+ let_it_be(:milestone_b) { create(:milestone, project: project) }
+
+ let(:milestone_fields) do
+ <<~GQL
+ fragment milestoneFields on Milestone {
+ #{all_graphql_fields_for('Milestone', max_depth: 1)}
+ releases { nodes { #{all_graphql_fields_for('Release', max_depth: 1)} } }
+ }
+ GQL
+ end
+
+ let(:single_query) do
+ <<~GQL
+ query ($id_a: MilestoneID!) {
+ a: milestone(id: $id_a) { ...milestoneFields }
+ }
+
+ #{milestone_fields}
+ GQL
+ end
+
+ let(:multi_query) do
+ <<~GQL
+ query ($id_a: MilestoneID!, $id_b: MilestoneID!) {
+ a: milestone(id: $id_a) { ...milestoneFields }
+ b: milestone(id: $id_b) { ...milestoneFields }
+ }
+ #{milestone_fields}
+ GQL
+ end
+
+ it 'returns the correct releases associated with each milestone' do
+ r = run_with_clean_state(multi_query,
+ context: { current_user: current_user },
+ variables: {
+ id_a: global_id_of(milestone).to_s,
+ id_b: milestone_b.to_global_id.to_s
+ })
+
+ expect(r.to_h['errors']).to be_blank
+ expect(graphql_dig_at(r.to_h, :data, :a, :releases, :nodes)).to match expected_release_nodes
+ expect(graphql_dig_at(r.to_h, :data, :b, :releases, :nodes)).to be_empty
+ end
+
+ it 'does not suffer from N+1 performance issues' do
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(single_query,
+ context: { current_user: current_user },
+ variables: { id_a: milestone.to_global_id.to_s })
+ end
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(multi_query,
+ context: { current_user: current_user },
+ variables: {
+ id_a: milestone.to_global_id.to_s,
+ id_b: milestone_b.to_global_id.to_s
+ })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+ end
+ end
end
- end
- end
- context 'when there are two milestones' do
- let_it_be(:milestone_b) { create(:milestone, project: project) }
-
- let(:current_user) { guest }
- let(:milestone_fields) do
- <<~GQL
- fragment milestoneFields on Milestone {
- #{all_graphql_fields_for('Milestone', max_depth: 1)}
- releases { nodes { #{all_graphql_fields_for('Release', max_depth: 1)} } }
- }
- GQL
- end
+ context 'when the user is an inherited member from the group' do
+ where(:user) { [ref(:inherited_guest), ref(:inherited_reporter), ref(:inherited_developer)] }
- let(:single_query) do
- <<~GQL
- query ($id_a: MilestoneID!) {
- a: milestone(id: $id_a) { ...milestoneFields }
- }
+ with_them do
+ let(:current_user) { user }
- #{milestone_fields}
- GQL
- end
+ it_behaves_like 'returns the milestone successfully'
+ end
+ end
- let(:multi_query) do
- <<~GQL
- query ($id_a: MilestoneID!, $id_b: MilestoneID!) {
- a: milestone(id: $id_a) { ...milestoneFields }
- b: milestone(id: $id_b) { ...milestoneFields }
- }
- #{milestone_fields}
- GQL
- end
+ context 'when unauthenticated' do
+ let(:current_user) { nil }
- it 'produces correct results' do
- r = run_with_clean_state(multi_query,
- context: { current_user: current_user },
- variables: {
- id_a: global_id_of(milestone).to_s,
- id_b: milestone_b.to_global_id.to_s
- })
-
- expect(r.to_h['errors']).to be_blank
- expect(graphql_dig_at(r.to_h, :data, :a, :releases, :nodes)).to match expected_release_nodes
- expect(graphql_dig_at(r.to_h, :data, :b, :releases, :nodes)).to be_empty
- end
+ it_behaves_like 'a working graphql query'
- it 'does not suffer from N+1 performance issues' do
- baseline = ActiveRecord::QueryRecorder.new do
- run_with_clean_state(single_query,
- context: { current_user: current_user },
- variables: { id_a: milestone.to_global_id.to_s })
- end
+ it { is_expected.to be_nil }
- multi = ActiveRecord::QueryRecorder.new do
- run_with_clean_state(multi_query,
- context: { current_user: current_user },
- variables: {
- id_a: milestone.to_global_id.to_s,
- id_b: milestone_b.to_global_id.to_s
- })
- end
+ context 'when ID argument is missing' do
+ let(:query) do
+ graphql_query_for('milestone', {}, 'title')
+ end
- expect(multi).not_to exceed_query_limit(baseline)
+ it 'raises an exception' do
+ expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id"))
+ end
+ end
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
deleted file mode 100644
index 2a5cb937a2f..00000000000
--- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Update work item widgets' do
- include GraphqlHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
- let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
-
- let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } }
- let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) }
- let(:mutation) do
- graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS)
- errors
- workItem {
- description
- widgets {
- type
- ... on WorkItemWidgetDescription {
- description
- }
- }
- }
- FIELDS
- end
-
- context 'the user is not allowed to update a work item' do
- let(:current_user) { create(:user) }
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user has permissions to update a work item', :aggregate_failures do
- let(:current_user) { developer }
-
- it_behaves_like 'update work item description widget' do
- let(:new_description) { 'updated description' }
- end
-
- it_behaves_like 'has spam protection' do
- let(:mutation_class) { ::Mutations::WorkItems::UpdateWidgets }
- end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not update the work item and returns and error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- work_item.reload
- end.to not_change(work_item, :description)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
- end
-end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 6169bc9b2a2..02d29601ceb 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -505,13 +505,35 @@ RSpec.describe API::Groups do
group3.add_maintainer(user2)
end
- it 'returns an array of groups the user has at least master access' do
- get api('/groups', user2), params: { min_access_level: 40 }
+ context 'with min_access_level parameter' do
+ it 'returns an array of groups the user has at least master access' do
+ get api('/groups', user2), params: { min_access_level: 40 }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(response_groups).to contain_exactly(group2.id, group3.id)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(response_groups).to contain_exactly(group2.id, group3.id)
+ end
+
+ context 'distinct count with present_groups_select_all feature flag' do
+ subject { get api('/groups', user2), params: { min_access_level: 40 } }
+
+ it 'counts with *' do
+ count_sql = /#{Regexp.escape('SELECT count(*)')}/i
+ expect { subject }.to make_queries_matching count_sql
+ end
+
+ context 'when present_groups_select_all feature flag is disabled' do
+ before do
+ stub_feature_flags(present_groups_select_all: false)
+ end
+
+ it 'counts with count_column' do
+ count_sql = /#{Regexp.escape('SELECT count(count_column)')}/i
+ expect { subject }.to make_queries_matching count_sql
+ end
+ end
+ end
end
end
diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb
index 7cf25468eb2..582ea202187 100644
--- a/spec/support/matchers/event_store.rb
+++ b/spec/support/matchers/event_store.rb
@@ -6,7 +6,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class|
supports_block_expectations
match do |proc|
- raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call)
+ raise ArgumentError, 'publish_event matcher only supports block expectation' unless proc.respond_to?(:call)
@events ||= []
@@ -22,6 +22,8 @@ RSpec::Matchers.define :publish_event do |expected_event_class|
end
def match_data?(actual, expected)
+ return if actual.blank? || expected.blank?
+
values_match?(actual.keys, expected.keys) &&
actual.keys.all? do |key|
values_match?(expected[key], actual[key])
@@ -33,7 +35,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class|
end
failure_message do
- message = "expected #{expected_event_class} with #{@expected_data} to be published"
+ message = "expected #{expected_event_class} with #{@expected_data || 'no data'} to be published"
if @events.present?
<<~MESSAGE
@@ -46,7 +48,7 @@ RSpec::Matchers.define :publish_event do |expected_event_class|
end
match_when_negated do |proc|
- raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call)
+ raise ArgumentError, 'publish_event matcher only supports block expectation' unless proc.respond_to?(:call)
allow(Gitlab::EventStore).to receive(:publish)
@@ -57,7 +59,45 @@ RSpec::Matchers.define :publish_event do |expected_event_class|
def events_list
@events.map do |event|
- " - #{event.class.name} #{event.data}"
+ " - #{event.class.name} with #{event.data}"
end.join("\n")
end
end
+
+# not_publish_event enables multiple assertions on a single block, for example:
+# expect { Model.create(invalid: :attribute) }
+# .to not_change(Model, :count)
+# .and not_publish_event(ModelCreated)
+RSpec::Matchers.define :not_publish_event do |expected_event_class|
+ include RSpec::Matchers::Composable
+
+ supports_block_expectations
+
+ match do |proc|
+ raise ArgumentError, 'not_publish_event matcher only supports block expectation' unless proc.respond_to?(:call)
+
+ @events ||= []
+
+ allow(Gitlab::EventStore).to receive(:publish) do |published_event|
+ @events << published_event
+ end
+
+ proc.call
+
+ @events.none? do |event|
+ event.instance_of?(expected_event_class)
+ end
+ end
+
+ failure_message do
+ "expected #{expected_event_class} not to be published"
+ end
+
+ chain :with do |_| # rubocop: disable Lint/UnreachableLoop
+ raise ArgumentError, 'not_publish_event does not permit .with to avoid ambiguity'
+ end
+
+ match_when_negated do |proc|
+ raise ArgumentError, 'not_publish_event matcher does not support negation. Use `expect {}.to publish_event` instead'
+ end
+end
diff --git a/spec/support_specs/matchers/event_store_spec.rb b/spec/support_specs/matchers/event_store_spec.rb
new file mode 100644
index 00000000000..3614d05fde8
--- /dev/null
+++ b/spec/support_specs/matchers/event_store_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'json_schemer'
+
+load File.expand_path('../../../spec/support/matchers/event_store.rb', __dir__)
+
+RSpec.describe 'event store matchers', :aggregate_errors do
+ let(:event_type1) do
+ Class.new(Gitlab::EventStore::Event) do
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[id]
+ }
+ end
+ end
+ end
+
+ let(:event_type2) do
+ Class.new(Gitlab::EventStore::Event) do
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[id]
+ }
+ end
+ end
+ end
+
+ before do
+ stub_const('FakeEventType1', event_type1)
+ stub_const('FakeEventType2', event_type2)
+ end
+
+ def publishing_event(event_type, data = {})
+ ::Gitlab::EventStore.publish(event_type.new(data: data))
+ end
+
+ describe 'publish_event' do
+ it 'requires a block matcher' do
+ matcher = -> { expect(:anything).to publish_event(:anything) } # rubocop: disable RSpec/ExpectActual
+
+ expect(&matcher).to raise_error(
+ ArgumentError,
+ 'publish_event matcher only supports block expectation'
+ )
+ end
+
+ it 'validates the event type' do
+ valid_event_type = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to publish_event(FakeEventType1).with('id' => 1)
+ end
+
+ expect(&valid_event_type).not_to raise_error
+
+ invalid_event_type = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to publish_event(FakeEventType2).with('id' => 1)
+ end
+
+ expect(&invalid_event_type).to raise_error <<~MESSAGE
+ expected FakeEventType2 with {"id"=>1} to be published, but only the following events were published:
+ - FakeEventType1 with {"id"=>1}
+ MESSAGE
+ end
+
+ it 'validates the event data' do
+ missing_data = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to publish_event(FakeEventType1)
+ end
+
+ expect(&missing_data).to raise_error <<~MESSAGE
+ expected FakeEventType1 with no data to be published, but only the following events were published:
+ - FakeEventType1 with {"id"=>1}
+ MESSAGE
+
+ different_data = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to publish_event(FakeEventType1).with({ 'id' => 2 })
+ end
+
+ expect(&different_data).to raise_error <<~MESSAGE
+ expected FakeEventType1 with {"id"=>2} to be published, but only the following events were published:
+ - FakeEventType1 with {"id"=>1}
+ MESSAGE
+ end
+ end
+
+ describe 'not_publish_event' do
+ it 'requires a block matcher' do
+ matcher = -> { expect(:anything).to not_publish_event(:anything) } # rubocop: disable RSpec/ExpectActual
+
+ expect(&matcher)
+ .to raise_error(ArgumentError, 'not_publish_event matcher only supports block expectation')
+ end
+
+ it 'does not permit .with' do
+ matcher = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to not_publish_event(FakeEventType2).with({ 'id' => 1 })
+ end
+
+ expect(&matcher)
+ .to raise_error(ArgumentError, 'not_publish_event does not permit .with to avoid ambiguity')
+ end
+
+ it 'validates the event type' do
+ matcher = -> do
+ expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
+ .to not_publish_event(FakeEventType1)
+ end
+
+ expect(&matcher)
+ .to raise_error('expected FakeEventType1 not to be published')
+ end
+ end
+end
diff --git a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
index 0e24810f835..2f423c72ca6 100644
--- a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
+++ b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
@@ -12,5 +12,11 @@ RSpec.describe 'layouts/header/_gitlab_version' do
it 'renders the version check badge' do
expect(rendered).to have_selector('.js-gitlab-version-check')
end
+
+ it 'renders the container as a link' do
+ expect(rendered).to have_selector(
+ 'a[data-testid="gitlab-version-container"][href="/help/update/index"]'
+ )
+ end
end
end