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-12 00:09:14 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-12 00:09:14 +0300
commit0f079aa28d93f40ad7fda398fb2280c3e358098d (patch)
treebc67abda12fd05a9712cd0bd97f82e33eb6c927f /spec
parent603ee53dbdbd3adaced752e1a119eb40d64e9979 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/topics_controller_spec.rb40
-rw-r--r--spec/fixtures/api/schemas/external_validation.json6
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js22
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js6
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js6
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js14
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js6
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js8
-rw-r--r--spec/frontend/blob/pdf/pdf_viewer_spec.js6
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js16
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js123
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js97
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml18
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml25
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js38
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js10
-rw-r--r--spec/lib/gitlab/audit/target_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb19
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb32
-rw-r--r--spec/models/concerns/database_event_tracking_spec.rb4
-rw-r--r--spec/services/topics/merge_service_spec.rb60
28 files changed, 474 insertions, 122 deletions
diff --git a/spec/controllers/admin/topics_controller_spec.rb b/spec/controllers/admin/topics_controller_spec.rb
index ee36d5f1def..87093e0263b 100644
--- a/spec/controllers/admin/topics_controller_spec.rb
+++ b/spec/controllers/admin/topics_controller_spec.rb
@@ -173,4 +173,44 @@ RSpec.describe Admin::TopicsController do
end
end
end
+
+ describe 'POST #merge' do
+ let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
+ let_it_be(:project) { create(:project, topic_list: source_topic.name ) }
+
+ it 'merges source topic into target topic' do
+ post :merge, params: { source_topic_id: source_topic.id, target_topic_id: topic.id }
+
+ expect(response).to redirect_to(admin_topics_path)
+ expect(topic.projects).to contain_exactly(project)
+ expect { source_topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'renders a 404 error for non-existing id' do
+ post :merge, params: { source_topic_id: non_existing_record_id, target_topic_id: topic.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect { topic.reload }.not_to raise_error
+ end
+
+ it 'renders a 400 error for identical topic ids' do
+ post :merge, params: { source_topic_id: topic, target_topic_id: topic.id }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect { topic.reload }.not_to raise_error
+ end
+
+ context 'as a normal user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders a 404 error' do
+ post :merge, params: { source_topic_id: source_topic.id, target_topic_id: topic.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect { source_topic.reload }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json
index ddcabd4c61e..4a2538a020e 100644
--- a/spec/fixtures/api/schemas/external_validation.json
+++ b/spec/fixtures/api/schemas/external_validation.json
@@ -4,7 +4,8 @@
"project",
"user",
"pipeline",
- "builds"
+ "builds",
+ "total_builds_count"
],
"properties" : {
"project": {
@@ -80,6 +81,7 @@
}
}
}
- }
+ },
+ "total_builds_count": { "type": "integer" }
}
}
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 8450c6b9332..788ee0a86ab 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -36,20 +36,20 @@ describe('Blob Content component', () => {
describe('rendering', () => {
it('renders loader if `loading: true`', () => {
createComponent({ loading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(BlobContentError).exists()).toBe(false);
- expect(wrapper.find(RichViewer).exists()).toBe(false);
- expect(wrapper.find(SimpleViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobContentError).exists()).toBe(false);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false);
});
it('renders error if there is any in the viewer', () => {
const renderError = 'Oops';
const viewer = { ...SimpleViewerMock, renderError };
createComponent({}, viewer);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(BlobContentError).exists()).toBe(true);
- expect(wrapper.find(RichViewer).exists()).toBe(false);
- expect(wrapper.find(SimpleViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(BlobContentError).exists()).toBe(true);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false);
});
it.each`
@@ -60,7 +60,7 @@ describe('Blob Content component', () => {
'renders $type viewer when activeViewer is $type and no loading or error detected',
({ mock, viewer }) => {
createComponent({}, mock);
- expect(wrapper.find(viewer).exists()).toBe(true);
+ expect(wrapper.findComponent(viewer).exists()).toBe(true);
},
);
@@ -70,13 +70,13 @@ describe('Blob Content component', () => {
${RichBlobContentMock.richData} | ${RichViewerMock} | ${RichViewer}
`('renders correct content that is passed to the component', ({ content, mock, viewer }) => {
createComponent({ content }, mock);
- expect(wrapper.find(viewer).html()).toContain(content);
+ expect(wrapper.findComponent(viewer).html()).toContain(content);
});
});
describe('functionality', () => {
describe('render error', () => {
- const findErrorEl = () => wrapper.find(BlobContentError);
+ const findErrorEl = () => wrapper.findComponent(BlobContentError);
const renderError = BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id;
const viewer = { ...SimpleViewerMock, renderError };
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
index 9fc2356c018..5017b624292 100644
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ b/spec/frontend/blob/components/blob_edit_content_spec.js
@@ -69,7 +69,7 @@ describe('Blob Header Editing', () => {
});
it('initialises Source Editor', () => {
- const el = wrapper.find({ ref: 'editor' }).element;
+ const el = wrapper.findComponent({ ref: 'editor' }).element;
expect(utils.initSourceEditor).toHaveBeenCalledWith({
el,
blobPath: fileName,
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index b1ce0e9a4c5..c84b5896348 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -16,7 +16,7 @@ describe('Blob Header Editing', () => {
});
};
const findDeleteButton = () =>
- wrapper.findAll(GlButton).wrappers.find((x) => x.text() === 'Delete file');
+ wrapper.findAllComponents(GlButton).wrappers.find((x) => x.text() === 'Delete file');
beforeEach(() => {
createComponent();
@@ -32,7 +32,7 @@ describe('Blob Header Editing', () => {
});
it('contains a form input field', () => {
- expect(wrapper.find(GlFormInput).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormInput).exists()).toBe(true);
});
it('does not show delete button', () => {
@@ -42,7 +42,7 @@ describe('Blob Header Editing', () => {
describe('functionality', () => {
it('emits input event when the blob name is changed', async () => {
- const inputComponent = wrapper.find(GlFormInput);
+ const inputComponent = wrapper.findComponent(GlFormInput);
const newValue = 'bar.txt';
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index aa538facae2..2382ac15f40 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -30,8 +30,8 @@ describe('Blob Header Default Actions', () => {
beforeEach(() => {
createComponent();
- btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlButton);
+ btnGroup = wrapper.findComponent(GlButtonGroup);
+ buttons = wrapper.findAllComponents(GlButton);
});
afterEach(() => {
@@ -69,7 +69,7 @@ describe('Blob Header Default Actions', () => {
createComponent({
activeViewer: RICH_BLOB_VIEWER,
});
- buttons = wrapper.findAll(GlButton);
+ buttons = wrapper.findAllComponents(GlButton);
expect(buttons.at(0).attributes('disabled')).toBeTruthy();
});
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 8220b598ff6..8c32cba1ba4 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -25,7 +25,7 @@ describe('Blob Header Filepath', () => {
wrapper.destroy();
});
- const findBadge = () => wrapper.find(GlBadge);
+ const findBadge = () => wrapper.findComponent(GlBadge);
describe('rendering', () => {
it('matches the snapshot', () => {
@@ -46,7 +46,7 @@ describe('Blob Header Filepath', () => {
it('renders copy-to-clipboard icon that copies path of the Blob', () => {
createComponent();
- const btn = wrapper.find(ClipboardButton);
+ const btn = wrapper.findComponent(ClipboardButton);
expect(btn.exists()).toBe(true);
expect(btn.vm.text).toBe(MockBlob.path);
});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index ee42c2387ae..46740958090 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -31,7 +31,7 @@ describe('Blob Header Default Actions', () => {
});
describe('rendering', () => {
- const findDefaultActions = () => wrapper.find(DefaultActions);
+ const findDefaultActions = () => wrapper.findComponent(DefaultActions);
const slots = {
prepend: 'Foo Prepend',
@@ -45,17 +45,17 @@ describe('Blob Header Default Actions', () => {
it('renders all components', () => {
createComponent();
- expect(wrapper.find(TableContents).exists()).toBe(true);
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
+ expect(wrapper.findComponent(TableContents).exists()).toBe(true);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true);
expect(findDefaultActions().exists()).toBe(true);
- expect(wrapper.find(BlobFilepath).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true);
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
richViewer: null,
});
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
@@ -66,7 +66,7 @@ describe('Blob Header Default Actions', () => {
hideViewerSwitcher: true,
},
);
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
@@ -77,7 +77,7 @@ describe('Blob Header Default Actions', () => {
hideDefaultActions: true,
},
);
- expect(wrapper.find(DefaultActions).exists()).toBe(false);
+ expect(wrapper.findComponent(DefaultActions).exists()).toBe(false);
});
Object.keys(slots).forEach((slot) => {
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 91baaf3ea69..1eac0733646 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -35,8 +35,8 @@ describe('Blob Header Viewer Switcher', () => {
beforeEach(() => {
createComponent();
- btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlButton);
+ btnGroup = wrapper.findComponent(GlButtonGroup);
+ buttons = wrapper.findAllComponents(GlButton);
});
it('renders gl-button-group component', () => {
@@ -58,7 +58,7 @@ describe('Blob Header Viewer Switcher', () => {
function factory(propsData = {}) {
createComponent(propsData);
- buttons = wrapper.findAll(GlButton);
+ buttons = wrapper.findAllComponents(GlButton);
simpleBtn = buttons.at(0);
richBtn = buttons.at(1);
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 93406db2675..ea4badc03fb 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -31,10 +31,10 @@ describe('iPython notebook renderer', () => {
wrapper = shallowMount(component, { propsData: { endpoint, relativeRawPath } });
};
- const findLoading = () => wrapper.find(GlLoadingIcon);
- const findNotebookLab = () => wrapper.find(NotebookLab);
- const findLoadErrorMessage = () => wrapper.find({ ref: 'loadErrorMessage' });
- const findParseErrorMessage = () => wrapper.find({ ref: 'parsingErrorMessage' });
+ const findLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findNotebookLab = () => wrapper.findComponent(NotebookLab);
+ const findLoadErrorMessage = () => wrapper.findComponent({ ref: 'loadErrorMessage' });
+ const findParseErrorMessage = () => wrapper.findComponent({ ref: 'parsingErrorMessage' });
beforeEach(() => {
mock = new MockAdapter(axios);
diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js
index e332ea49fa6..23227df6357 100644
--- a/spec/frontend/blob/pdf/pdf_viewer_spec.js
+++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js
@@ -18,9 +18,9 @@ describe('PDF renderer', () => {
});
};
- const findLoading = () => wrapper.find(GlLoadingIcon);
- const findPdfLab = () => wrapper.find(PdfLab);
- const findLoadError = () => wrapper.find({ ref: 'loadError' });
+ const findLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findPdfLab = () => wrapper.findComponent(PdfLab);
+ const findLoadError = () => wrapper.findComponent({ ref: 'loadError' });
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 750dd8f0a72..81b38cfc278 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -52,7 +52,7 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the path from the commit cookie for back to the merge request button', () => {
- const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
});
@@ -67,16 +67,16 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the path from projectMergeRequestsPath for back to the merge request button', () => {
- const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
});
});
it('has expected structure', () => {
- const modal = wrapper.find(GlModal);
- const sprintf = modal.find(GlSprintf);
- const emoji = modal.find(GlEmoji);
+ const modal = wrapper.findComponent(GlModal);
+ const sprintf = modal.findComponent(GlSprintf);
+ const emoji = modal.findComponent(GlEmoji);
expect(wrapper.text()).toContain("That's it, well done!");
expect(sprintf.exists()).toBe(true);
@@ -84,7 +84,7 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the link for codeQualityLink', () => {
- expect(wrapper.find(GlLink).attributes('href')).toBe('/code-quality-link');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/code-quality-link');
});
it('calls to remove cookie', () => {
@@ -103,7 +103,7 @@ describe('PipelineTourSuccessModal', () => {
it('send an event when go to pipelines is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- const goToBtn = wrapper.find({ ref: 'goToPipelines' });
+ const goToBtn = wrapper.findComponent({ ref: 'goToPipelines' });
triggerEvent(goToBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
@@ -115,7 +115,7 @@ describe('PipelineTourSuccessModal', () => {
it('sends an event when back to the merge request is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- const goToBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
triggerEvent(goToBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 7e13994f2b7..6b329dc078a 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -98,7 +98,7 @@ describe('Suggest gitlab-ci.yml Popover', () => {
const expectedAction = 'click_button';
const expectedProperty = 'owner';
const expectedValue = '10';
- const dismissButton = wrapper.find(GlButton);
+ const dismissButton = wrapper.findComponent(GlButton);
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(dismissButton.element);
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
new file mode 100644
index 00000000000..fb79611229c
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -0,0 +1,123 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION } from '~/ci_variable_list/constants';
+import { createJoinedEnvironments, mapEnvironmentNames } from '~/ci_variable_list/utils';
+
+import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
+
+describe('Ci variable table', () => {
+ let wrapper;
+
+ const defaultProps = {
+ areScopedVariablesAvailable: true,
+ environments: mapEnvironmentNames(mockEnvs),
+ isLoading: false,
+ variables: mockVariablesWithScopes,
+ };
+
+ const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
+ const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
+
+ const createComponent = () => {
+ wrapper = shallowMount(CiVariableSettings, {
+ propsData: {
+ ...defaultProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('props passing', () => {
+ it('passes props down correctly to the ci table', () => {
+ expect(findCiVariableTable().props()).toEqual({
+ isLoading: defaultProps.isLoading,
+ variables: defaultProps.variables,
+ });
+ });
+
+ it('passes props down correctly to the ci modal', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().props()).toEqual({
+ areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
+ environments: createJoinedEnvironments(defaultProps.variables, defaultProps.environments),
+ mode: ADD_VARIABLE_ACTION,
+ selectedVariable: {},
+ });
+ });
+ });
+
+ describe('modal mode', () => {
+ it('passes down ADD mode when receiving an empty variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
+ });
+
+ it('passes down EDIT mode when receiving a variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
+ await nextTick();
+
+ expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
+ });
+ });
+
+ describe('variable modal', () => {
+ it('is hidden by default', () => {
+ expect(findCiVariableModal().exists()).toBe(false);
+ });
+
+ it('shows modal when adding a new variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(true);
+ });
+
+ it('shows modal when updating a variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(true);
+ });
+
+ it('hides modal when receiving the event from the modal', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ findCiVariableModal().vm.$emit('hideModal');
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(false);
+ });
+ });
+
+ describe('variable events', () => {
+ it.each`
+ eventName
+ ${'add-variable'}
+ ${'update-variable'}
+ ${'delete-variable'}
+ `('bubbles up the $eventName event', async ({ eventName }) => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ findCiVariableModal().vm.$emit(eventName, newVariable);
+ await nextTick();
+
+ expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
new file mode 100644
index 00000000000..b5b4881aa44
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -0,0 +1,97 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import { mockVariables } from '../mocks';
+
+describe('Ci variable table', () => {
+ let wrapper;
+
+ const defaultProps = {
+ isLoading: false,
+ variables: mockVariables,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = mountExtended(CiVariableTable, {
+ attachTo: document.body,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findRevealButton = () => wrapper.findByText('Reveal values');
+ const findAddButton = () => wrapper.findByLabelText('Add');
+ const findEditButton = () => wrapper.findByLabelText('Edit');
+ const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
+ const findHiddenValues = () => wrapper.findAll('[data-testid="hiddenValue"]');
+ const findRevealedValues = () => wrapper.findAll('[data-testid="revealedValue"]');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { variables: [] } });
+ });
+
+ it('displays empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
+ });
+
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
+ });
+
+ describe('When table has variables', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
+
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
+
+ it('displays the correct amount of variables', async () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
+ });
+ });
+
+ describe('Table click actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('reveals secret values when button is clicked', async () => {
+ expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
+ expect(findRevealedValues()).toHaveLength(0);
+
+ await findRevealButton().trigger('click');
+
+ expect(findHiddenValues()).toHaveLength(0);
+ expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
+ });
+
+ it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
+ await findEditButton().trigger('click');
+
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
+ });
+
+ it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
+ await findAddButton().trigger('click');
+
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ });
+ });
+});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index c59806a5d60..b9d2426c33d 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -24,12 +24,14 @@ import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_a
import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
// YAML POSITIVE TEST
+import ArtifactsYaml from './yaml_tests/positive_tests/artifacts.yml';
import CacheYaml from './yaml_tests/positive_tests/cache.yml';
import FilterYaml from './yaml_tests/positive_tests/filter.yml';
import IncludeYaml from './yaml_tests/positive_tests/include.yml';
import RulesYaml from './yaml_tests/positive_tests/rules.yml';
// YAML NEGATIVE TEST
+import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
@@ -63,6 +65,7 @@ describe('positive tests', () => {
FilterYaml,
IncludeYaml,
RulesYaml,
+ ArtifactsYaml,
}),
)('schema validates %s', (_, input) => {
expect(input).toValidateJsonSchema(schema);
@@ -84,6 +87,7 @@ describe('negative tests', () => {
// YAML
CacheNegativeYaml,
IncludeNegativeYaml,
+ ArtifactsNegativeYaml,
}),
)('schema validates %s', (_, input) => {
expect(input).not.toValidateJsonSchema(schema);
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
new file mode 100644
index 00000000000..f5670376efc
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
@@ -0,0 +1,18 @@
+# invalid artifact:reports:cyclonedx
+
+cyclonedx no paths:
+ artifacts:
+ reports:
+ cyclonedx:
+
+cyclonedx not a report:
+ artifacts:
+ cyclonedx: foo
+
+cyclonedx not an array or string:
+ artifacts:
+ reports:
+ cyclonedx:
+ paths:
+ - foo
+ - bar
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
new file mode 100644
index 00000000000..20c1fc2c50f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
@@ -0,0 +1,25 @@
+# valid artifact:reports:cyclonedx
+
+cyclonedx string path:
+ artifacts:
+ reports:
+ cyclonedx: foo
+
+cyclonedx glob path:
+ artifacts:
+ reports:
+ cyclonedx: "*.foo"
+
+cylonedx list of string paths:
+ artifacts:
+ reports:
+ cyclonedx:
+ - foo
+ - ./bar/baz
+
+cylonedx mixed list of string paths and globs:
+ artifacts:
+ reports:
+ cyclonedx:
+ - ./foo
+ - "bar/*.baz"
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 1e34087b290..38ebe36c2c5 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -14,21 +14,21 @@ describe('IDE commit module getters', () => {
describe('discardDraftButtonDisabled', () => {
it('returns true when commitMessage is empty', () => {
- expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(true);
});
it('returns false when commitMessage is not empty & loading is false', () => {
state.commitMessage = 'test';
state.submitCommitLoading = false;
- expect(getters.discardDraftButtonDisabled(state)).toBeFalsy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(false);
});
it('returns true when commitMessage is not empty & loading is true', () => {
state.commitMessage = 'test';
state.submitCommitLoading = true;
- expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(true);
});
});
@@ -152,13 +152,13 @@ describe('IDE commit module getters', () => {
it('returns false if NOT creating a new branch', () => {
state.commitAction = COMMIT_TO_CURRENT_BRANCH;
- expect(getters.isCreatingNewBranch(state)).toBeFalsy();
+ expect(getters.isCreatingNewBranch(state)).toBe(false);
});
it('returns true if creating a new branch', () => {
state.commitAction = COMMIT_TO_NEW_BRANCH;
- expect(getters.isCreatingNewBranch(state)).toBeTruthy();
+ expect(getters.isCreatingNewBranch(state)).toBe(true);
});
});
@@ -183,7 +183,7 @@ describe('IDE commit module getters', () => {
});
it('should never hide "New MR" option', () => {
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
});
@@ -195,13 +195,13 @@ describe('IDE commit module getters', () => {
it('should NOT hide "New MR" option if user can NOT push to the current branch', () => {
rootGetters.canPushToBranch = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
it('should hide "New MR" option if user can push to the current branch', () => {
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -211,7 +211,7 @@ describe('IDE commit module getters', () => {
});
it('should never hide "New MR" option', () => {
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
});
@@ -223,13 +223,13 @@ describe('IDE commit module getters', () => {
it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => {
rootGetters.hasMergeRequest = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
it('should hide "New MR" option if there is existing MR for the current branch', () => {
rootGetters.hasMergeRequest = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -247,17 +247,13 @@ describe('IDE commit module getters', () => {
it('should hide "New MR" when there is an existing MR', () => {
rootGetters.hasMergeRequest = true;
- expect(
- getters.shouldHideNewMrOption(state, localGetters, null, rootGetters),
- ).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
it('should hide "New MR" when there is no existing MR', () => {
rootGetters.hasMergeRequest = false;
- expect(
- getters.shouldHideNewMrOption(state, localGetters, null, rootGetters),
- ).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -270,17 +266,17 @@ describe('IDE commit module getters', () => {
rootGetters.hasMergeRequest = false;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
rootGetters.hasMergeRequest = true;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
rootGetters.hasMergeRequest = false;
rootGetters.canPushToBranch = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
});
});
@@ -292,7 +288,7 @@ describe('IDE commit module getters', () => {
rootGetters.hasMergeRequest = true;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index 3c3143b1865..9b9ee4172f9 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -94,8 +94,8 @@ describe('Test reports app', () => {
beforeEach(() => createComponent());
it('sets testReports and shows tests', () => {
- expect(wrapper.vm.testReports).toBeTruthy();
- expect(wrapper.vm.showTests).toBeTruthy();
+ expect(wrapper.vm.testReports).toEqual(expect.any(Object));
+ expect(wrapper.vm.showTests).toBe(true);
});
it('shows tests details', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 02148e044ec..6e89cd41559 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -369,7 +369,7 @@ describe('ReadyToMerge', () => {
const params = wrapper.vm.service.merge.mock.calls[0][0];
- expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.should_remove_source_branch).toBe(true);
expect(params.auto_merge_strategy).toBeUndefined();
});
@@ -393,7 +393,7 @@ describe('ReadyToMerge', () => {
const params = wrapper.vm.service.merge.mock.calls[0][0];
- expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.should_remove_source_branch).toBe(true);
expect(params.auto_merge_strategy).toBeUndefined();
});
@@ -469,8 +469,8 @@ describe('ReadyToMerge', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
- expect(cpc).toBeFalsy();
- expect(spc).toBeTruthy();
+ expect(cpc).toBe(false);
+ expect(spc).toBe(true);
});
it('should continue polling until MR is merged', async () => {
@@ -492,8 +492,8 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(cpc).toBeTruthy();
- expect(spc).toBeFalsy();
+ expect(cpc).toBe(true);
+ expect(spc).toBe(false);
});
});
});
@@ -527,13 +527,13 @@ describe('ReadyToMerge', () => {
mr: { commitsCount: 2, enableSquashBeforeMerge: true },
});
- expect(findCheckboxElement().exists()).toBeTruthy();
+ expect(findCheckboxElement().exists()).toBe(true);
});
it('should not be rendered when squash before merge is disabled', () => {
createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
- expect(findCheckboxElement().exists()).toBeFalsy();
+ expect(findCheckboxElement().exists()).toBe(false);
});
it('should be rendered when there is only 1 commit', () => {
@@ -710,7 +710,7 @@ describe('ReadyToMerge', () => {
it('should not be rendered if squash is disabled', () => {
createComponent();
- expect(findCommitDropdownElement().exists()).toBeFalsy();
+ expect(findCommitDropdownElement().exists()).toBe(false);
});
it('should be rendered if squash is enabled and there is more than 1 commit', async () => {
@@ -720,7 +720,7 @@ describe('ReadyToMerge', () => {
await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
- expect(findCommitDropdownElement().exists()).toBeTruthy();
+ expect(findCommitDropdownElement().exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index b8aeea38e77..f7030f38709 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -33,7 +33,7 @@ describe('DismissibleContainer', () => {
button.trigger('click');
- expect(wrapper.emitted().dismiss).toBeTruthy();
+ expect(wrapper.emitted().dismiss).toEqual(expect.any(Array));
});
});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 7bac9f8235b..b7ce3e47cef 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -351,7 +351,7 @@ describe('User Popover Component', () => {
await axios.waitForAll();
expect(wrapper.emitted().follow.length).toBe(1);
- expect(wrapper.emitted().unfollow).toBeFalsy();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
itTracksToggleFollowButtonClick('follow_from_user_popover');
@@ -376,8 +376,8 @@ describe('User Popover Component', () => {
it('emits no events', async () => {
await axios.waitForAll();
- expect(wrapper.emitted().follow).toBe(undefined);
- expect(wrapper.emitted().unfollow).toBe(undefined);
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
@@ -423,8 +423,8 @@ describe('User Popover Component', () => {
});
it('emits no events', () => {
- expect(wrapper.emitted().follow).toBe(undefined);
- expect(wrapper.emitted().unfollow).toBe(undefined);
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
diff --git a/spec/lib/gitlab/audit/target_spec.rb b/spec/lib/gitlab/audit/target_spec.rb
index 4748b4599fe..5c06cd117a9 100644
--- a/spec/lib/gitlab/audit/target_spec.rb
+++ b/spec/lib/gitlab/audit/target_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Audit::Target do
- let(:object) { double('object') } # rubocop:disable Rspec/VerifiedDoubles
+ let(:object) { double('object') } # rubocop:disable RSpec/VerifiedDoubles
subject { described_class.new(object) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index c19756c4783..fb1a360a4b7 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -148,6 +148,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
payload = Gitlab::Json.parse(params[:body])
+ expect(payload['total_builds_count']).to eq(0)
+
builds = payload['builds']
expect(builds.count).to eq(2)
expect(builds[0]['services']).to be_nil
@@ -160,6 +162,23 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
perform!
end
+
+ context "with existing jobs from other project's alive pipelines" do
+ before do
+ create(:ci_pipeline, :with_job, user: user)
+ create(:ci_pipeline, :with_job)
+ end
+
+ it 'returns the expected total_builds_count' do
+ expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
+ payload = Gitlab::Json.parse(params[:body])
+
+ expect(payload['total_builds_count']).to eq(1)
+ end
+
+ perform!
+ end
+ end
end
context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
index 72544536953..c3831b654cf 100644
--- a/spec/models/concerns/cross_database_modification_spec.rb
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -4,38 +4,6 @@ require 'spec_helper'
RSpec.describe CrossDatabaseModification do
describe '.transaction' do
- context 'feature flag disabled' do
- before do
- stub_feature_flags(track_gitlab_schema_in_current_transaction: false)
- end
-
- it 'does not add to gitlab_transactions_stack' do
- ApplicationRecord.transaction do
- expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
-
- Project.first
- end
-
- expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
- end
- end
-
- context 'feature flag is not yet setup' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
- end
-
- it 'does not add to gitlab_transactions_stack' do
- ApplicationRecord.transaction do
- expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
-
- Project.first
- end
-
- expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
- end
- end
-
it 'adds the current gitlab schema to gitlab_transactions_stack', :aggregate_failures do
ApplicationRecord.transaction do
expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
diff --git a/spec/models/concerns/database_event_tracking_spec.rb b/spec/models/concerns/database_event_tracking_spec.rb
index 79be8654498..976462b4174 100644
--- a/spec/models/concerns/database_event_tracking_spec.rb
+++ b/spec/models/concerns/database_event_tracking_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe DatabaseEventTracking, :snowplow do
self.table_name = 'application_setting_terms'
- self::SNOWPLOW_ATTRIBUTES = %w[id].freeze # rubocop:disable Rspec/LeakyConstantDeclaration
+ self::SNOWPLOW_ATTRIBUTES = %w[id].freeze # rubocop:disable RSpec/LeakyConstantDeclaration
end
end
@@ -17,7 +17,7 @@ RSpec.describe DatabaseEventTracking, :snowplow do
context 'if event emmiter failed' do
before do
- allow(Gitlab::Tracking).to receive(:event).and_raise(StandardError) # rubocop:disable Rspec/ExpectGitlabTracking
+ allow(Gitlab::Tracking).to receive(:event).and_raise(StandardError) # rubocop:disable RSpec/ExpectGitlabTracking
end
it 'tracks the exception' do
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
new file mode 100644
index 00000000000..971917eb8e9
--- /dev/null
+++ b/spec/services/topics/merge_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Topics::MergeService do
+ let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
+ let_it_be(:target_topic) { create(:topic, name: 'target_topic') }
+ let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name ) }
+ let_it_be(:project_2) { create(:project, :private, topic_list: source_topic.name ) }
+ let_it_be(:project_3) { create(:project, :public, topic_list: target_topic.name ) }
+ let_it_be(:project_4) { create(:project, :public, topic_list: [source_topic.name, target_topic.name] ) }
+
+ subject { described_class.new(source_topic, target_topic).execute }
+
+ describe '#execute' do
+ it 'merges source topic into target topic' do
+ subject
+
+ expect(target_topic.projects).to contain_exactly(project_1, project_2, project_3, project_4)
+ expect { source_topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'refreshes counters of target topic' do
+ expect { subject }
+ .to change { target_topic.reload.total_projects_count }.by(2)
+ .and change { target_topic.reload.non_private_projects_count }.by(1)
+ end
+
+ context 'when source topic fails to delete' do
+ it 'reverts previous changes' do
+ allow(source_topic.reload).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
+
+ expect(source_topic.projects).to contain_exactly(project_1, project_2, project_4)
+ expect(target_topic.projects).to contain_exactly(project_3, project_4)
+ end
+ end
+
+ context 'for parameter validation' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(source_topic_parameter, target_topic_parameter).execute }
+
+ where(:source_topic_parameter, :target_topic_parameter, :expected_message) do
+ nil | ref(:target_topic) | 'The source topic is not a topic.'
+ ref(:source_topic) | nil | 'The target topic is not a topic.'
+ ref(:target_topic) | ref(:target_topic) | 'The source topic and the target topic are identical.' # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ end
+
+ with_them do
+ it 'raises correct error' do
+ expect { subject }.to raise_error(ArgumentError) do |error|
+ expect(error.message).to eq(expected_message)
+ end
+ end
+ end
+ end
+ end
+end