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>2022-08-05 21:08:56 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-05 21:08:56 +0300
commit092e41f5660a356a6cebc26cd0274b531d8c70c6 (patch)
treee696f9ad230bc5d5a7222fb690e3699a1a8abe78 /spec
parent8ec882085e734458ffe0fff8e2e4b72bc3871419 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/fixtures/namespaces.rb46
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js18
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js164
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js81
-rw-r--r--spec/graphql/graphql_triggers_spec.rb14
-rw-r--r--spec/graphql/types/subscription_type_spec.rb1
-rw-r--r--spec/graphql/types/work_items/widget_interface_spec.rb1
-rw-r--r--spec/graphql/types/work_items/widgets/labels_type_spec.rb11
-rw-r--r--spec/helpers/namespaces_helper_spec.rb33
-rw-r--r--spec/lib/gitlab/database/lock_writes_manager_spec.rb123
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb63
-rw-r--r--spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb34
-rw-r--r--spec/models/work_item_spec.rb1
-rw-r--r--spec/models/work_items/type_spec.rb1
-rw-r--r--spec/models/work_items/widgets/labels_spec.rb31
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb38
-rw-r--r--spec/services/issues/update_service_spec.rb46
-rw-r--r--spec/services/work_items/update_service_spec.rb34
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb72
19 files changed, 691 insertions, 121 deletions
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
new file mode 100644
index 00000000000..b11f661fe09
--- /dev/null
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Jobs (JavaScript fixtures)' do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ describe GraphQL::Query, type: :request do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:groups) { create_list(:group, 4) }
+
+ before_all do
+ groups.each { |group| group.add_owner(user) }
+ end
+
+ query_name = 'search_namespaces_where_user_can_transfer_projects'
+ query_extension = '.query.graphql'
+
+ full_input_path = "projects/settings/graphql/queries/#{query_name}#{query_extension}"
+ base_output_path = "graphql/projects/settings/#{query_name}"
+
+ it "#{base_output_path}_page_1#{query_extension}.json" do
+ query = get_graphql_query_as_string(full_input_path)
+
+ post_graphql(query, current_user: user, variables: { first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{base_output_path}_page_2#{query_extension}.json" do
+ query = get_graphql_query_as_string(full_input_path)
+
+ post_graphql(query, current_user: user, variables: { first: 2 })
+
+ post_graphql(
+ query,
+ current_user: user,
+ variables: { first: 2, after: graphql_data_at('currentUser', 'groups', 'pageInfo', 'endCursor') }
+ )
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index f2901148e17..fb50d623543 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -50,6 +50,7 @@ describe('DependencyProxyApp', () => {
groupPath: 'gitlab-org',
groupId: dummyGrouptId,
noManifestsIllustration: 'noManifestsIllustration',
+ canClearCache: true,
};
function createComponent({ provide = provideDefaults } = {}) {
@@ -268,6 +269,23 @@ describe('DependencyProxyApp', () => {
'All items in the cache are scheduled for removal.',
);
});
+
+ describe('when user has no permission to clear cache', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ groupPath: 'gitlab-org',
+ groupId: dummyGrouptId,
+ noManifestsIllustration: 'noManifestsIllustration',
+ canClearCache: false,
+ },
+ });
+ });
+
+ it('does not show the clear cache dropdown list', () => {
+ expect(findClearCacheDropdownList().exists()).toBe(false);
+ });
+ });
});
});
});
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index 85b09ced024..bde7148078d 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,11 +1,19 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_1.query.graphql.json';
+import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_2.query.graphql.json';
import {
groupNamespaces,
userNamespaces,
} from 'jest/vue_shared/components/namespace_select/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+import searchNamespacesWhereUserCanTransferProjectsQuery from '~/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
describe('Transfer project form', () => {
let wrapper;
@@ -13,36 +21,50 @@ describe('Transfer project form', () => {
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- const createComponent = () =>
- shallowMountExtended(TransferProjectForm, {
+ const runDebounce = () => jest.runAllTimers();
+
+ Vue.use(VueApollo);
+
+ const defaultQueryHandler = jest
+ .fn()
+ .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1);
+
+ const createComponent = ({
+ requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]],
+ } = {}) => {
+ wrapper = shallowMountExtended(TransferProjectForm, {
propsData: {
userNamespaces,
groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
+ apolloProvider: createMockApollo(requestHandlers),
});
+ };
const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- beforeEach(() => {
- wrapper = createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
it('renders the namespace selector', () => {
+ createComponent();
+
expect(findNamespaceSelect().exists()).toBe(true);
});
it('renders the confirm button', () => {
+ createComponent();
+
expect(findConfirmDanger().exists()).toBe(true);
});
it('disables the confirm button by default', () => {
+ createComponent();
+
expect(findConfirmDanger().attributes('disabled')).toBe('true');
});
@@ -50,6 +72,8 @@ describe('Transfer project form', () => {
const [selectedItem] = groupNamespaces;
beforeEach(() => {
+ createComponent();
+
findNamespaceSelect().vm.$emit('select', selectedItem);
});
@@ -69,4 +93,132 @@ describe('Transfer project form', () => {
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
+
+ it('passes correct props to `NamespaceSelect` component', async () => {
+ createComponent();
+
+ runDebounce();
+ await waitForPromises();
+
+ const {
+ namespace,
+ groups,
+ } = searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser;
+
+ expect(findNamespaceSelect().props()).toMatchObject({
+ userNamespaces: [
+ {
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
+ },
+ ],
+ groupNamespaces: groups.nodes.map((node) => ({
+ id: getIdFromGraphQLId(node.id),
+ humanName: node.fullName,
+ })),
+ hasNextPageOfGroups: true,
+ isLoadingMoreGroups: false,
+ isSearchLoading: false,
+ shouldFilterNamespaces: false,
+ });
+ });
+
+ describe('when `search` event is fired', () => {
+ const arrange = async () => {
+ createComponent();
+
+ findNamespaceSelect().vm.$emit('search', 'foo');
+
+ await nextTick();
+ };
+
+ it('sets `isSearchLoading` prop to `true`', async () => {
+ await arrange();
+
+ expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
+ });
+
+ it('passes `search` variable to query', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' }));
+ });
+ });
+
+ describe('when `load-more-groups` event is fired', () => {
+ let queryHandler;
+
+ const arrange = async () => {
+ queryHandler = jest.fn();
+ queryHandler.mockResolvedValueOnce(
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1,
+ );
+ queryHandler.mockResolvedValueOnce(
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2,
+ );
+
+ createComponent({
+ requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]],
+ });
+
+ runDebounce();
+ await waitForPromises();
+
+ findNamespaceSelect().vm.$emit('load-more-groups');
+ await nextTick();
+ };
+
+ it('sets `isLoadingMoreGroups` prop to `true`', async () => {
+ await arrange();
+
+ expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true);
+ });
+
+ it('passes `after` and `first` variables to query', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(queryHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ first: 25,
+ after:
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
+ .pageInfo.endCursor,
+ }),
+ );
+ });
+
+ it('updates `groupNamespaces` prop with new groups', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(findNamespaceSelect().props('groupNamespaces')).toEqual(
+ [
+ ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
+ .nodes,
+ ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2.data.currentUser.groups
+ .nodes,
+ ].map((node) => ({
+ id: getIdFromGraphQLId(node.id),
+ humanName: node.fullName,
+ })),
+ );
+ });
+
+ it('updates `hasNextPageOfGroups` prop', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
index c11b20a692e..2c14d65186b 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
@@ -1,5 +1,12 @@
import { nextTick } from 'vue';
-import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, {
i18n,
@@ -7,7 +14,7 @@ import NamespaceSelect, {
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
import { userNamespaces, groupNamespaces } from './mock_data';
-const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces];
+const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
@@ -31,6 +38,8 @@ describe('Namespace Select', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownText = () => findDropdown().props('text');
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGroupDropdownItems = () =>
+ wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -59,7 +68,7 @@ describe('Namespace Select', () => {
it('splits group and user namespaces', () => {
const headers = findSectionHeaders();
- expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
+ expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
});
it('does not render wrapper as full width', () => {
@@ -89,18 +98,20 @@ describe('Namespace Select', () => {
describe('with search', () => {
it.each`
- term | includeEmptyNamespace | expectedItems
- ${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]}
- ${'sub'} | ${false} | ${[groupNamespaces[1]]}
- ${'User'} | ${false} | ${[...userNamespaces]}
- ${'User'} | ${true} | ${[...userNamespaces]}
- ${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
+ term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
+ ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
+ ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
+ ${'User'} | ${false} | ${true} | ${[...userNamespaces]}
+ ${'User'} | ${true} | ${true} | ${[...userNamespaces]}
+ ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
+ ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
`(
- 'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
- async ({ term, includeEmptyNamespace, expectedItems }) => {
+ 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
+ async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
wrapper = createComponent({
includeEmptyNamespace,
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
+ shouldFilterNamespaces,
});
search(term);
@@ -114,6 +125,18 @@ describe('Namespace Select', () => {
);
});
+ describe('when search is typed in', () => {
+ it('emits `search` event', async () => {
+ wrapper = createComponent();
+
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
+
+ await nextTick();
+
+ expect(wrapper.emitted('search')).toEqual([['foo']]);
+ });
+ });
+
describe('with a selected namespace', () => {
const selectedGroupIndex = 1;
const selectedItem = groupNamespaces[selectedGroupIndex];
@@ -121,7 +144,8 @@ describe('Namespace Select', () => {
beforeEach(() => {
wrapper = createComponent();
- findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
+ findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
});
it('sets the dropdown text', () => {
@@ -132,6 +156,10 @@ describe('Namespace Select', () => {
const args = [selectedItem];
expect(wrapper.emitted('select')).toEqual([args]);
});
+
+ it('clears search', () => {
+ expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
+ });
});
describe('with an empty namespace option', () => {
@@ -166,4 +194,33 @@ describe('Namespace Select', () => {
expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
});
});
+
+ describe('when `hasNextPageOfGroups` prop is `true`', () => {
+ it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
+ wrapper = createComponent({ hasNextPageOfGroups: true });
+
+ const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
+
+ intersectionObserver.vm.$emit('appear');
+
+ expect(intersectionObserver.exists()).toBe(true);
+ expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
+ });
+
+ describe('when `isLoadingMoreGroups` prop is `true`', () => {
+ it('renders a loading icon', () => {
+ wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when `isSearchLoading` prop is `true`', () => {
+ it('sets `isLoading` prop to `true`', () => {
+ wrapper = createComponent({ isSearchLoading: true });
+
+ expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
+ });
+ });
});
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index 84af33a5cb3..5e2ab74a0e5 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -47,4 +47,18 @@ RSpec.describe GraphqlTriggers do
GraphqlTriggers.issuable_labels_updated(issue)
end
end
+
+ describe '.issuable_dates_updated' do
+ it 'triggers the issuableDatesUpdated subscription' do
+ work_item = create(:work_item)
+
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'issuableDatesUpdated',
+ { issuable_id: work_item.to_gid },
+ work_item
+ ).and_call_original
+
+ GraphqlTriggers.issuable_dates_updated(work_item)
+ end
+ end
end
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index 1a2629ed422..9b043fa52cf 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
issue_crm_contacts_updated
issuable_title_updated
issuable_labels_updated
+ issuable_dates_updated
]
expect(described_class).to have_graphql_fields(*expected_fields).only
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
index caf986c961f..b9e8edacf15 100644
--- a/spec/graphql/types/work_items/widget_interface_spec.rb
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Types::WorkItems::WidgetInterface do
WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType
WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
+ WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
end
with_them do
diff --git a/spec/graphql/types/work_items/widgets/labels_type_spec.rb b/spec/graphql/types/work_items/widgets/labels_type_spec.rb
new file mode 100644
index 00000000000..028ebe979f3
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/labels_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::LabelsType do
+ it 'exposes the expected fields' do
+ expected_fields = %i[labels allowsScopedLabels type]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 39f0e1c15f5..f7500709d0e 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -45,39 +45,6 @@ RSpec.describe NamespacesHelper do
user_group.add_owner(user)
end
- describe '#namespaces_as_json' do
- let(:result) { helper.namespaces_as_json(user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'returns the user\'s groups' do
- json_data = Gitlab::Json.parse(result)
-
- expect(result).to include('group')
- expect(json_data['group']).to include(
- "id" => user_group.id,
- "name" => user_group.name,
- "display_path" => user_group.full_path,
- "human_name" => user_group.human_name
- )
- end
-
- it 'returns the user\'s namespace' do
- user_namespace = user.namespace
- json_data = Gitlab::Json.parse(result)
-
- expect(result).to include('user')
- expect(json_data['user']).to include(
- "id" => user_namespace.id,
- "name" => user_namespace.name,
- "display_path" => user_namespace.full_path,
- "human_name" => user_namespace.human_name
- )
- end
- end
-
describe '#namespaces_options' do
context 'when admin mode is enabled', :enable_admin_mode do
it 'returns groups without being a member for admin' do
diff --git a/spec/lib/gitlab/database/lock_writes_manager_spec.rb b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
new file mode 100644
index 00000000000..eb527d492cf
--- /dev/null
+++ b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LockWritesManager do
+ let(:connection) { ApplicationRecord.connection }
+ let(:test_table) { '_test_table' }
+ let(:logger) { instance_double(Logger) }
+
+ subject(:lock_writes_manager) do
+ described_class.new(
+ table_name: test_table,
+ connection: connection,
+ database_name: 'main',
+ logger: logger
+ )
+ end
+
+ before do
+ allow(logger).to receive(:info)
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0);
+
+ INSERT INTO #{test_table} (id, value)
+ VALUES (1, 1), (2, 2), (3, 3)
+ SQL
+ end
+
+ describe '#lock_writes' do
+ it 'prevents any writes on the table' do
+ subject.lock_writes
+
+ expect do
+ connection.execute("delete from #{test_table}")
+ end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/)
+ end
+
+ it 'prevents truncating the table' do
+ subject.lock_writes
+
+ expect do
+ connection.execute("truncate #{test_table}")
+ end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/)
+ end
+
+ it 'adds 3 triggers to the ci schema tables on the main database' do
+ expect do
+ subject.lock_writes
+ end.to change {
+ number_of_triggers_on(connection, test_table)
+ }.by(3) # Triggers to block INSERT / UPDATE / DELETE
+ # Triggers on TRUNCATE are not added to the information_schema.triggers
+ # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
+ end
+
+ it 'logs the write locking' do
+ expect(logger).to receive(:info).with("Database: 'main', Table: '_test_table': Lock Writes")
+
+ subject.lock_writes
+ end
+
+ it 'retries again if it receives a statement_timeout a few number of times' do
+ error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
+ call_count = 0
+ allow(connection).to receive(:execute) do |statement|
+ if statement.include?("CREATE TRIGGER")
+ call_count += 1
+ raise(ActiveRecord::QueryCanceled, error_message) if call_count.even?
+ end
+ end
+ subject.lock_writes
+ end
+
+ it 'raises the exception if it happened many times' do
+ error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
+ allow(connection).to receive(:execute) do |statement|
+ if statement.include?("CREATE TRIGGER")
+ raise(ActiveRecord::QueryCanceled, error_message)
+ end
+ end
+
+ expect do
+ subject.lock_writes
+ end.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ describe '#unlock_writes' do
+ before do
+ subject.lock_writes
+ end
+
+ it 'allows writing on the table again' do
+ subject.unlock_writes
+
+ expect do
+ connection.execute("delete from #{test_table}")
+ end.not_to raise_error
+ end
+
+ it 'removes the write protection triggers from the gitlab_main tables on the ci database' do
+ expect do
+ subject.unlock_writes
+ end.to change {
+ number_of_triggers_on(connection, test_table)
+ }.by(-3) # Triggers to block INSERT / UPDATE / DELETE
+ # Triggers on TRUNCATE are not added to the information_schema.triggers
+ # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
+ end
+
+ it 'logs the write unlocking' do
+ expect(logger).to receive(:info).with("Database: 'main', Table: '_test_table': Allow Writes")
+
+ subject.unlock_writes
+ end
+ end
+
+ def number_of_triggers_on(connection, table_name)
+ connection
+ .select_value("SELECT count(*) FROM information_schema.triggers WHERE event_object_table=$1", nil, [table_name])
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 8077515f478..dd5ad40d8ef 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1022,6 +1022,40 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(Project.sum(:star_count)).to eq(2 * Project.count)
end
end
+
+ context 'when the table is write-locked' do
+ let(:test_table) { '_test_table' }
+ let(:lock_writes_manager) do
+ Gitlab::Database::LockWritesManager.new(
+ table_name: test_table,
+ connection: model.connection,
+ database_name: 'main'
+ )
+ end
+
+ before do
+ model.connection.execute(<<~SQL)
+ CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0);
+
+ INSERT INTO #{test_table} (id, value)
+ VALUES (1, 1), (2, 2), (3, 3)
+ SQL
+
+ lock_writes_manager.lock_writes
+ end
+
+ it 'disables the write-lock trigger function' do
+ expect do
+ model.update_column_in_batches(test_table, :value, Arel.sql('1+1'), disable_lock_writes: true)
+ end.not_to raise_error
+ end
+
+ it 'raises an error if it does not disable the trigger function' do
+ expect do
+ model.update_column_in_batches(test_table, :value, Arel.sql('1+1'), disable_lock_writes: false)
+ end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{test_table}" is write protected/)
+ end
+ end
end
context 'when running inside the transaction' do
@@ -1122,6 +1156,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'copies the value to the new column using the type_cast_function', :aggregate_failures do
expect(model).to receive(:copy_indexes).with(:users, :id, :new)
expect(model).to receive(:add_not_null_constraint).with(:users, :new)
+ expect(model).to receive(:execute).with("SELECT set_config('lock_writes.users', 'false', true)")
expect(model).to receive(:execute).with("UPDATE \"users\" SET \"new\" = cast_to_jsonb_with_default(\"users\".\"id\") WHERE \"users\".\"id\" >= #{user.id}")
expect(copy_trigger).to receive(:create).with(:id, :new, trigger_name: nil)
@@ -1181,6 +1216,34 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ context 'when the table in the other database is write-locked' do
+ let(:test_table) { '_test_table' }
+ let(:lock_writes_manager) do
+ Gitlab::Database::LockWritesManager.new(
+ table_name: test_table,
+ connection: model.connection,
+ database_name: 'main'
+ )
+ end
+
+ before do
+ model.connection.execute(<<~SQL)
+ CREATE TABLE #{test_table} (id integer NOT NULL, value integer NOT NULL DEFAULT 0);
+
+ INSERT INTO #{test_table} (id, value)
+ VALUES (1, 1), (2, 2), (3, 3)
+ SQL
+
+ lock_writes_manager.lock_writes
+ end
+
+ it 'does not raise an error when renaming the column' do
+ expect do
+ model.rename_column_concurrently(test_table, :value, :new_value)
+ end.not_to raise_error
+ end
+ end
+
context 'when the column to be renamed does not exist' do
before do
allow(model).to receive(:columns).and_return([])
diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
index a7b195a16b4..ed06e409937 100644
--- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
+++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
@@ -9,6 +9,40 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
let_it_be(:form_builder) { described_class.new(:user, user, fake_action_view_base, {}) }
+ describe '#submit' do
+ context 'without pajamas_button enabled' do
+ subject(:submit_html) do
+ form_builder.submit('Save', class: 'gl-button btn-confirm custom-class', data: { test: true })
+ end
+
+ it 'renders a submit input' do
+ expected_html = <<~EOS
+ <input type="submit" name="commit" value="Save" class="gl-button btn-confirm custom-class" data-test="true" data-disable-with="Save" />
+ EOS
+
+ expect(html_strip_whitespace(submit_html)).to eq(html_strip_whitespace(expected_html))
+ end
+ end
+
+ context 'with pajamas_button enabled' do
+ subject(:submit_html) do
+ form_builder.submit('Save', pajamas_button: true, class: 'custom-class', data: { test: true })
+ end
+
+ it 'renders a submit button' do
+ expected_html = <<~EOS
+ <button class="gl-button btn btn-md btn-confirm custom-class" data-test="true" type="submit">
+ <span class="gl-button-text">
+ Save
+ </span>
+ </button>
+ EOS
+
+ expect(html_strip_whitespace(submit_html)).to eq(html_strip_whitespace(expected_html))
+ end
+ end
+ end
+
describe '#gitlab_ui_checkbox_component' do
context 'when not using slots' do
let(:optional_args) { {} }
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 98ba702da25..e2240c225a9 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe WorkItem do
is_expected.to include(
instance_of(WorkItems::Widgets::Description),
instance_of(WorkItems::Widgets::Hierarchy),
+ instance_of(WorkItems::Widgets::Labels),
instance_of(WorkItems::Widgets::Assignees),
instance_of(WorkItems::Widgets::StartAndDueDate)
)
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index ec0b5536546..e41df7f0f61 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -67,6 +67,7 @@ RSpec.describe WorkItems::Type do
is_expected.to include(
::WorkItems::Widgets::Description,
::WorkItems::Widgets::Hierarchy,
+ ::WorkItems::Widgets::Labels,
::WorkItems::Widgets::Assignees,
::WorkItems::Widgets::StartAndDueDate
)
diff --git a/spec/models/work_items/widgets/labels_spec.rb b/spec/models/work_items/widgets/labels_spec.rb
new file mode 100644
index 00000000000..15e8aaa1cf3
--- /dev/null
+++ b/spec/models/work_items/widgets/labels_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Labels do
+ let_it_be(:work_item) { create(:work_item, labels: [create(:label)]) }
+
+ describe '.type' do
+ subject { described_class.type }
+
+ it { is_expected.to eq(:labels) }
+ end
+
+ describe '#type' do
+ subject { described_class.new(work_item).type }
+
+ it { is_expected.to eq(:labels) }
+ end
+
+ describe '#labels' do
+ subject { described_class.new(work_item).labels }
+
+ it { is_expected.to eq(work_item.labels) }
+ end
+
+ describe '#allowScopedLabels' do
+ subject { described_class.new(work_item).allows_scoped_labels? }
+
+ it { is_expected.to eq(work_item.allows_scoped_labels?) }
+ end
+end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index b4f4cb68350..217d93535ea 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -215,6 +215,44 @@ RSpec.describe 'Query.work_item(id)' do
end
end
+ describe 'labels widget' do
+ let(:labels) { create_list(:label, 2, project: project) }
+ let(:work_item) { create(:work_item, project: project, labels: labels) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetLabels {
+ labels {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'LABELS',
+ 'labels' => {
+ 'nodes' => match_array(
+ labels.map { |a| { 'id' => a.to_gid.to_s, 'title' => a.title } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
+
describe 'start and due date widget' do
let(:work_item_fields) do
<<~GRAPHQL
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index e2e8828ae89..9ef969d802b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -988,6 +988,52 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
+ context 'updating dates' do
+ subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) }
+
+ let(:updated_date) { 1.week.from_now.to_date }
+
+ shared_examples 'issue update service that triggers date updates' do
+ it 'triggers graphql date updated subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(issue).and_call_original
+
+ result
+ end
+ end
+
+ shared_examples 'issue update service that does not trigger date updates' do
+ it 'does not trigger date updated subscriptions' do
+ expect(GraphqlTriggers).not_to receive(:issuable_dates_updated)
+
+ result
+ end
+ end
+
+ context 'when due_date is updated' do
+ let(:params) { { due_date: updated_date } }
+
+ it_behaves_like 'issue update service that triggers date updates'
+ end
+
+ context 'when start_date is updated' do
+ let(:params) { { start_date: updated_date } }
+
+ it_behaves_like 'issue update service that triggers date updates'
+ end
+
+ context 'when no date is updated' do
+ let(:params) { { title: 'should not trigger date updates' } }
+
+ it_behaves_like 'issue update service that does not trigger date updates'
+ end
+
+ context 'when update is not successful but date is provided' do
+ let(:params) { { title: '', due_date: updated_date } }
+
+ it_behaves_like 'issue update service that does not trigger date updates'
+ end
+ end
+
context 'updating asssignee_id' do
it 'does not update assignee when assignee_id is invalid' do
update_issue(assignee_ids: [-1])
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index dd5924e7434..7715fad9703 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -36,6 +36,14 @@ RSpec.describe WorkItems::UpdateService do
stub_spam_services
end
+ shared_examples 'update service that triggers graphql dates updated subscription' do
+ it 'triggers graphql subscription issueableDatesUpdated' do
+ expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
context 'when title is changed' do
let(:opts) { { title: 'changed' } }
@@ -187,6 +195,32 @@ RSpec.describe WorkItems::UpdateService do
end
end
+ context 'for start and due date widget' do
+ let(:updated_date) { 1.week.from_now.to_date }
+
+ context 'when due_date is updated' do
+ let(:widget_params) { { start_and_due_date_widget: { due_date: updated_date } } }
+
+ it_behaves_like 'update service that triggers graphql dates updated subscription'
+ end
+
+ context 'when start_date is updated' do
+ let(:widget_params) { { start_and_due_date_widget: { start_date: updated_date } } }
+
+ it_behaves_like 'update service that triggers graphql dates updated subscription'
+ end
+
+ context 'when no date param is updated' do
+ let(:opts) { { title: 'should not trigger' } }
+
+ it 'does not trigger date updated subscription' do
+ expect(GraphqlTriggers).not_to receive(:issuable_dates_updated)
+
+ update_work_item
+ end
+ end
+ end
+
context 'for the hierarchy widget' do
let(:opts) { { title: 'changed' } }
let_it_be(:child_work_item) { create(:work_item, :task, project: project) }
diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
index 3eb6d35f3f9..d03e15224cb 100644
--- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
+++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
@@ -49,26 +49,6 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
end
context 'when locking writes' do
- it 'adds 3 triggers to the ci schema tables on the main database' do
- expect do
- run_rake_task('gitlab:db:lock_writes')
- end.to change {
- number_of_triggers_on(main_connection, Ci::Build.table_name)
- }.by(3) # Triggers to block INSERT / UPDATE / DELETE
- # Triggers on TRUNCATE are not added to the information_schema.triggers
- # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
- end
-
- it 'adds 3 triggers to the main schema tables on the ci database' do
- expect do
- run_rake_task('gitlab:db:lock_writes')
- end.to change {
- number_of_triggers_on(ci_connection, Project.table_name)
- }.by(3) # Triggers to block INSERT / UPDATE / DELETE
- # Triggers on TRUNCATE are not added to the information_schema.triggers
- # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
- end
-
it 'still allows writes on the tables with the correct connections' do
Project.update_all(updated_at: Time.now)
Ci::Build.update_all(updated_at: Time.now)
@@ -107,31 +87,6 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
main_connection.execute("truncate ci_build_needs")
end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_build_needs" is write protected/)
end
-
- it 'retries again if it receives a statement_timeout a few number of times' do
- error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
- call_count = 0
- allow(main_connection).to receive(:execute) do |statement|
- if statement.include?("CREATE TRIGGER")
- call_count += 1
- raise(ActiveRecord::QueryCanceled, error_message) if call_count.even?
- end
- end
- run_rake_task('gitlab:db:lock_writes')
- end
-
- it 'raises the exception if it happened many times' do
- error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
- allow(main_connection).to receive(:execute) do |statement|
- if statement.include?("CREATE TRIGGER")
- raise(ActiveRecord::QueryCanceled, error_message)
- end
- end
-
- expect do
- run_rake_task('gitlab:db:lock_writes')
- end.to raise_error(ActiveRecord::QueryCanceled)
- end
end
context 'multiple shared databases' do
@@ -156,26 +111,8 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
run_rake_task('gitlab:db:lock_writes')
end
- it 'removes the write protection triggers from the gitlab_main tables on the ci database' do
- expect do
- run_rake_task('gitlab:db:unlock_writes')
- end.to change {
- number_of_triggers_on(ci_connection, Project.table_name)
- }.by(-3) # Triggers to block INSERT / UPDATE / DELETE
- # Triggers on TRUNCATE are not added to the information_schema.triggers
- # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
-
- expect do
- ci_connection.execute("delete from projects")
- end.not_to raise_error
- end
-
- it 'removes the write protection triggers from the gitlab_ci tables on the main database' do
- expect do
- run_rake_task('gitlab:db:unlock_writes')
- end.to change {
- number_of_triggers_on(main_connection, Ci::Build.table_name)
- }.by(-3)
+ it 'allows writes again on the gitlab_ci tables on the main database' do
+ run_rake_task('gitlab:db:unlock_writes')
expect do
main_connection.execute("delete from ci_builds")
@@ -187,9 +124,4 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
def number_of_triggers(connection)
connection.select_value("SELECT count(*) FROM information_schema.triggers")
end
-
- def number_of_triggers_on(connection, table_name)
- connection
- .select_value("SELECT count(*) FROM information_schema.triggers WHERE event_object_table=$1", nil, [table_name])
- end
end