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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 15:08:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 15:08:37 +0300
commit1faea1c6a0464e44dca4477fb31846938c2ad871 (patch)
tree49e1efd28dc28a14e68b7c3510621499f1a5141c /spec
parentc2de38f36d2fb75a17ce161fa69f2b8a5e670f3e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb29
-rw-r--r--spec/factories/metrics/dashboard/annotations.rb6
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb29
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/frontend/__mocks__/jed/index.js17
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js17
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js14
-rw-r--r--spec/frontend/jobs/components/job/job_log_controllers_spec.js1
-rw-r--r--spec/frontend/observability/client_spec.js163
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js47
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js29
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js247
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js255
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js10
-rw-r--r--spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js62
-rw-r--r--spec/frontend/tracing/components/tracing_list_filtered_search_spec.js11
-rw-r--r--spec/frontend/tracing/components/tracing_list_spec.js12
-rw-r--r--spec/frontend/tracing/filters_spec.js (renamed from spec/frontend/tracing/utils_spec.js)46
-rw-r--r--spec/graphql/types/work_items/linked_item_type_spec.rb13
-rw-r--r--spec/graphql/types/work_items/widget_interface_spec.rb3
-rw-r--r--spec/graphql/types/work_items/widgets/linked_items_type_spec.rb12
-rw-r--r--spec/lib/api/entities/user_spec.rb16
-rw-r--r--spec/lib/api/ml/mlflow/api_helpers_spec.rb40
-rw-r--r--spec/migrations/20230807083334_add_linked_items_work_item_widget_spec.rb10
-rw-r--r--spec/models/ci/pipeline_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb1
-rw-r--r--spec/models/concerns/cross_database_ignored_tables_spec.rb222
-rw-r--r--spec/models/environment_spec.rb1
-rw-r--r--spec/models/metrics/dashboard/annotation_spec.rb39
-rw-r--r--spec/models/user_spec.rb41
-rw-r--r--spec/models/work_items/widget_definition_spec.rb3
-rw-r--r--spec/models/work_items/widgets/linked_items_spec.rb25
-rw-r--r--spec/policies/metrics/dashboard/annotation_policy_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb26
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb53
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb73
-rw-r--r--spec/requests/api/ml/mlflow/runs_spec.rb16
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb51
-rw-r--r--spec/services/metrics/dashboard/annotations/delete_service_spec.rb93
-rw-r--r--spec/services/users/update_service_spec.rb9
-rw-r--r--spec/support/database/auto_explain.rb15
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb2
-rw-r--r--spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb5
44 files changed, 1192 insertions, 649 deletions
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index f5c97f63293..b4ffe0bc844 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -110,31 +110,14 @@ RSpec.describe Profiles::PreferencesController do
end
end
- context 'on disable_follow_users feature flag' do
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(disable_follow_users: false)
- end
+ context 'on enabled_following setting' do
+ it 'does not update enabled_following preference of user' do
+ prefs = { enabled_following: false }
- it 'does not update enabled_following preference of user' do
- prefs = { enabled_following: false }
-
- go params: prefs
- user.reload
-
- expect(user.enabled_following).to eq(true)
- end
- end
-
- context 'with feature flag enabled' do
- it 'does not update enabled_following preference of user' do
- prefs = { enabled_following: false }
-
- go params: prefs
- user.reload
+ go params: prefs
+ user.reload
- expect(user.enabled_following).to eq(false)
- end
+ expect(user.enabled_following).to eq(false)
end
end
end
diff --git a/spec/factories/metrics/dashboard/annotations.rb b/spec/factories/metrics/dashboard/annotations.rb
index 2e5c373918e..50c9ed01fd8 100644
--- a/spec/factories/metrics/dashboard/annotations.rb
+++ b/spec/factories/metrics/dashboard/annotations.rb
@@ -5,11 +5,5 @@ FactoryBot.define do
description { "Dashbaord annoation description" }
dashboard_path { "custom_dashbaord.yml" }
starting_at { Time.current }
- environment
-
- trait :with_cluster do
- cluster
- environment { nil }
- end
end
end
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 5756218d20f..9883434eb68 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -97,8 +97,8 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js, feature_cat
describe 'build list build item' do
let(:build_item) do
- find('.mini-pipeline-graph-dropdown-item')
- first('.mini-pipeline-graph-dropdown-item')
+ find('.pipeline-job-item')
+ first('.pipeline-job-item')
end
it 'visits the build page when clicked' do
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index d2104799e79..5bb3d1af924 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :source_code_management do
let(:project) { create(:project, :public, :repository) }
- context 'when commit has pipelines' do
+ context 'when commit has pipelines and feature flag is enabled' do
let(:pipeline) do
create(
:ci_pipeline,
@@ -24,6 +24,33 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
wait_for_requests
end
+ it 'displays the graphql pipeline stage' do
+ expect(page).to have_selector('[data-testid="pipeline-stage"]')
+
+ build.drop
+ end
+ end
+
+ context 'when commit has pipelines and feature flag is disabled' do
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ status: :running,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha
+ )
+ end
+
+ let(:build) { create(:ci_build, pipeline: pipeline, status: :running) }
+
+ before do
+ stub_feature_flags(ci_graphql_pipeline_mini_graph: false)
+ build.run
+ visit project_commit_path(project, project.commit.id)
+ wait_for_requests
+ end
+
it 'display icon with status' do
expect(page).to have_selector('.ci-status-icon-running')
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f742ec3dfa6..26fcd8ca3ca 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -559,7 +559,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
find(dropdown_selector).click
within('.js-builds-dropdown-list') do
- build_element = page.find('.mini-pipeline-graph-dropdown-item')
+ build_element = page.find('.pipeline-job-item')
expect(build_element['title']).to eq('build - failed - (unknown failure)')
end
end
diff --git a/spec/frontend/__mocks__/jed/index.js b/spec/frontend/__mocks__/jed/index.js
new file mode 100644
index 00000000000..fa2be5aa6e2
--- /dev/null
+++ b/spec/frontend/__mocks__/jed/index.js
@@ -0,0 +1,17 @@
+/**
+ * ## Why are we mocking Jed?
+ *
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/390934#note_1494028934
+ *
+ * It's possible that some environments run a specific locale. If the unit
+ * tests run under this condition, hardcoded values will fail. To make
+ * tests more deterministic across environments, let's skip loading translations
+ * in FE unit tests.
+ */
+const Jed = jest.requireActual('jed');
+
+export default class MockJed extends Jed {
+ constructor() {
+ super({});
+ }
+}
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 858cf24061a..3bbe14adb88 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -38,8 +38,9 @@ describe('Pipeline Status', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
@@ -142,18 +143,18 @@ describe('Pipeline Status', () => {
});
it.each`
- state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
- ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
- ${false} | ${{}} | ${true} | ${false}
+ state | showLegacyPipelineMiniGraph | showPipelineMiniGraph
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${false}
`(
'renders the correct component when the feature flag is set to $state',
- async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
- createComponentWithApollo(provide);
+ async ({ state, showLegacyPipelineMiniGraph, showPipelineMiniGraph }) => {
+ createComponentWithApollo({ ciGraphqlPipelineMiniGraph: state });
await waitForPromises();
- expect(findPipelineEditorMiniGraph().exists()).toBe(showPipelineMiniGraph);
- expect(findPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ expect(findPipelineEditorMiniGraph().exists()).toBe(showLegacyPipelineMiniGraph);
+ expect(findPipelineMiniGraph().exists()).toBe(showPipelineMiniGraph);
},
);
});
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 79e1e087001..3b3e5098857 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -263,18 +263,18 @@ describe('Commit box pipeline mini graph', () => {
describe('feature flag behavior', () => {
it.each`
- state | provide | showPipelineMiniGraph | showGraphqlPipelineMiniGraph
- ${true} | ${{ ciGraphqlPipelineMiniGraph: true }} | ${false} | ${true}
- ${false} | ${{}} | ${true} | ${false}
+ state | showLegacyPipelineMiniGraph | showPipelineMiniGraph
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${false}
`(
'renders the correct component when the feature flag is set to $state',
- async ({ provide, showPipelineMiniGraph, showGraphqlPipelineMiniGraph }) => {
- createComponent(provide);
+ async ({ state, showLegacyPipelineMiniGraph, showPipelineMiniGraph }) => {
+ createComponent({ ciGraphqlPipelineMiniGraph: state });
await waitForPromises();
- expect(findLegacyPipelineMiniGraph().exists()).toBe(showPipelineMiniGraph);
- expect(findPipelineMiniGraph().exists()).toBe(showGraphqlPipelineMiniGraph);
+ expect(findLegacyPipelineMiniGraph().exists()).toBe(showLegacyPipelineMiniGraph);
+ expect(findPipelineMiniGraph().exists()).toBe(showPipelineMiniGraph);
},
);
diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
index 218096b9745..7b6d58f63d1 100644
--- a/spec/frontend/jobs/components/job/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
@@ -21,7 +21,6 @@ describe('Job log controllers', () => {
const defaultProps = {
rawPath: '/raw',
- erasePath: '/erase',
size: 511952,
isScrollTopDisabled: false,
isScrollBottomDisabled: false,
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 239d7adf986..10fdc8c33c4 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -9,6 +9,7 @@ describe('buildClient', () => {
let axiosMock;
const tracingUrl = 'https://example.com/tracing';
+ const EXPECTED_ERROR_MESSAGE = 'traces are missing/invalid in the response';
beforeEach(() => {
axiosMock = new MockAdapter(axios);
@@ -24,11 +25,51 @@ describe('buildClient', () => {
axiosMock.restore();
});
+ describe('fetchTrace', () => {
+ it('fetches the trace from the tracing URL', async () => {
+ const mockTraces = [
+ { trace_id: 'trace-1', spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
+ ];
+
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: mockTraces,
+ });
+
+ const result = await client.fetchTrace('trace-1');
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
+ withCredentials: true,
+ params: { trace_id: 'trace-1' },
+ });
+ expect(result).toEqual({
+ ...mockTraces[0],
+ duration: 1,
+ });
+ });
+
+ it('rejects if trace id is missing', () => {
+ return expect(client.fetchTrace()).rejects.toThrow('traceId is required.');
+ });
+
+ it('rejects if traces are empty', () => {
+ axiosMock.onGet(tracingUrl).reply(200, { traces: [] });
+
+ return expect(client.fetchTrace('trace-1')).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ });
+
+ it('rejects if traces are invalid', () => {
+ axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
+
+ return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ });
+ });
+
describe('fetchTraces', () => {
- it('should fetch traces from the tracing URL', async () => {
+ it('fetches traces from the tracing URL', async () => {
const mockTraces = [
- { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
- { id: 2, spans: [{ duration_nano: 2000 }] },
+ { trace_id: 'trace-1', spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
+ { trace_id: 'trace-2', spans: [{ duration_nano: 2000 }] },
];
axiosMock.onGet(tracingUrl).reply(200, {
@@ -40,27 +81,127 @@ describe('buildClient', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
withCredentials: true,
+ params: new URLSearchParams(),
});
expect(result).toEqual([
- { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }], duration: 3 },
- { id: 2, spans: [{ duration_nano: 2000 }], duration: 2 },
+ {
+ ...mockTraces[0],
+ duration: 1,
+ },
+ {
+ ...mockTraces[1],
+ duration: 2,
+ },
]);
});
it('rejects if traces are missing', () => {
axiosMock.onGet(tracingUrl).reply(200, {});
- return expect(client.fetchTraces()).rejects.toThrow(
- 'traces are missing/invalid in the response',
- );
+ return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
});
it('rejects if traces are invalid', () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
- return expect(client.fetchTraces()).rejects.toThrow(
- 'traces are missing/invalid in the response',
- );
+ return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ });
+
+ describe('query filter', () => {
+ beforeEach(() => {
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: [],
+ });
+ });
+
+ const getQueryParam = () => decodeURIComponent(axios.get.mock.calls[0][1].params.toString());
+
+ it('does not set any query param without filters', async () => {
+ await client.fetchTraces();
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('converts filter to proper query params', async () => {
+ await client.fetchTraces({
+ durationMs: [
+ { operator: '>', value: '100' },
+ { operator: '<', value: '1000' },
+ ],
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '!=', value: 'not-op' },
+ ],
+ serviceName: [
+ { operator: '=', value: 'service' },
+ { operator: '!=', value: 'not-service' },
+ ],
+ period: [{ operator: '=', value: '5m' }],
+ traceId: [
+ { operator: '=', value: 'trace-id' },
+ { operator: '!=', value: 'not-trace-id' },
+ ],
+ });
+ expect(getQueryParam()).toBe(
+ 'gt[duration_nano]=100000&lt[duration_nano]=1000000' +
+ '&operation=op&not[operation]=not-op' +
+ '&service_name=service&not[service_name]=not-service' +
+ '&period=5m' +
+ '&trace_id=trace-id&not[trace_id]=not-trace-id',
+ );
+ });
+
+ it('handles repeated params', async () => {
+ await client.fetchTraces({
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '=', value: 'op2' },
+ ],
+ });
+ expect(getQueryParam()).toBe('operation=op&operation=op2');
+ });
+
+ it('ignores unsupported filters', async () => {
+ await client.fetchTraces({
+ unsupportedFilter: [{ operator: '=', value: 'foo' }],
+ });
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('ignores empty filters', async () => {
+ await client.fetchTraces({
+ durationMs: null,
+ traceId: undefined,
+ });
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('ignores unsupported operators', async () => {
+ await client.fetchTraces({
+ durationMs: [
+ { operator: '*', value: 'foo' },
+ { operator: '=', value: 'foo' },
+ { operator: '!=', value: 'foo' },
+ ],
+ operation: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ serviceName: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ period: [{ operator: '!=', value: 'foo' }],
+ traceId: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ });
+
+ expect(getQueryParam()).toBe('');
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index 98cd87cb07b..500fb0d7598 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -38,9 +38,9 @@ describe('Details Header', () => {
const findTitle = () => wrapper.findByTestId('title');
const findTagsCount = () => wrapper.findByTestId('tags-count');
const findCleanup = () => wrapper.findByTestId('cleanup');
- const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findDeleteButton = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
- const findMenu = () => wrapper.findComponent(GlDropdown);
+ const findMenu = () => wrapper.findComponent(GlDisclosureDropdown);
const findSize = () => wrapper.findByTestId('image-size');
const mountComponent = ({
@@ -58,10 +58,6 @@ describe('Details Header', () => {
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
- stubs: {
- GlDropdown,
- GlDropdownItem,
- },
});
};
@@ -126,35 +122,36 @@ describe('Details Header', () => {
},
);
- describe('delete button', () => {
- it('exists', () => {
- mountComponent();
+ it('has the correct props', () => {
+ mountComponent();
- expect(findDeleteButton().exists()).toBe(true);
+ expect(findMenu().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ placement: 'right',
+ textSrOnly: true,
+ noCaret: true,
+ toggleText: 'More actions',
});
+ });
- it('has the correct text', () => {
+ describe('delete item', () => {
+ beforeEach(() => {
mountComponent();
-
- expect(findDeleteButton().text()).toBe('Delete image repository');
});
- it('has the correct props', () => {
- mountComponent();
+ it('exists', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
- expect(findDeleteButton().attributes()).toMatchObject(
- expect.objectContaining({
- variant: 'danger',
- }),
- );
+ it('has the correct text', () => {
+ expect(findDeleteButton().text()).toBe('Delete image repository');
});
it('emits the correct event', () => {
- mountComponent();
-
- findDeleteButton().vm.$emit('click');
+ findDeleteButton().vm.$emit('action');
- expect(wrapper.emitted('delete')).toEqual([[]]);
+ expect(wrapper.emitted('delete')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js
new file mode 100644
index 00000000000..b89f27e5c05
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import JobItem from '~/pipelines/components/pipeline_mini_graph/job_item.vue';
+
+describe('JobItem', () => {
+ let wrapper;
+
+ const defaultProps = {
+ job: { id: '3' },
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(JobItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the received HTML', () => {
+ expect(wrapper.html()).toContain(defaultProps.job.id);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js
new file mode 100644
index 00000000000..3697eaeea1a
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -0,0 +1,247 @@
+import { GlDropdown } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue';
+import eventHub from '~/pipelines/event_hub';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stageReply } from '../../mock_data';
+
+const dropdownPath = 'path.json';
+
+describe('Pipelines stage component', () => {
+ let wrapper;
+ let mock;
+ let glTooltipDirectiveMock;
+
+ const createComponent = (props = {}) => {
+ glTooltipDirectiveMock = jest.fn();
+ wrapper = mount(LegacyPipelineStage, {
+ attachTo: document.body,
+ directives: {
+ GlTooltip: glTooltipDirectiveMock,
+ },
+ propsData: {
+ stage: {
+ status: {
+ group: 'success',
+ icon: 'status_success',
+ title: 'success',
+ },
+ dropdown_path: dropdownPath,
+ },
+ updateDropdown: false,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(eventHub, '$emit');
+ });
+
+ afterEach(() => {
+ eventHub.$emit.mockRestore();
+ mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
+ });
+
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
+ const findDropdownMenu = () =>
+ wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
+ const findDropdownMenuTitle = () =>
+ wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]');
+ const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
+ const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]');
+
+ const openStageDropdown = async () => {
+ await findDropdownToggle().trigger('click');
+ await waitForPromises();
+ await nextTick();
+ };
+
+ describe('loading state', () => {
+ beforeEach(async () => {
+ createComponent({ updateDropdown: true });
+
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+
+ await openStageDropdown();
+ });
+
+ it('displays loading state while jobs are being fetched', async () => {
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(findLoadingState().exists()).toBe(true);
+ expect(findLoadingState().text()).toBe(LegacyPipelineStage.i18n.loadingText);
+ });
+
+ it('does not display loading state after jobs have been fetched', async () => {
+ await waitForPromises();
+
+ expect(findLoadingState().exists()).toBe(false);
+ });
+ });
+
+ describe('default appearance', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets up the tooltip to not have a show delay animation', () => {
+ expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true);
+ });
+
+ it('renders a dropdown with the status icon', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdownToggle().exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
+ });
+
+ it('renders a borderless ci-icon', () => {
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().props('isBorderless')).toBe(true);
+ expect(findCiIcon().classes('borderless')).toBe(true);
+ });
+
+ it('renders a ci-icon with a custom border class', () => {
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().classes('gl-border')).toBe(true);
+ });
+ });
+
+ describe('when user opens dropdown and stage request is successful', () => {
+ beforeEach(async () => {
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ createComponent();
+
+ await openStageDropdown();
+ await jest.runAllTimers();
+ await axios.waitForAll();
+ });
+
+ it('renders the received data and emits the correct events', () => {
+ expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
+ expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ expect(wrapper.emitted('miniGraphStageClick')).toEqual([[]]);
+ });
+
+ it('refreshes when updateDropdown is set to true', async () => {
+ expect(mock.history.get).toHaveLength(1);
+
+ wrapper.setProps({ updateDropdown: true });
+ await axios.waitForAll();
+
+ expect(mock.history.get).toHaveLength(2);
+ });
+ });
+
+ describe('when user opens dropdown and stage request fails', () => {
+ it('should close the dropdown', async () => {
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+
+ await openStageDropdown();
+ await axios.waitForAll();
+ await waitForPromises();
+
+ expect(findDropdown().classes('show')).toBe(false);
+ });
+ });
+
+ describe('update endpoint correctly', () => {
+ beforeEach(async () => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(HTTP_STATUS_OK, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ await axios.waitForAll();
+ });
+
+ it('should update the stage to request the new endpoint provided', async () => {
+ await openStageDropdown();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(findDropdownMenu().text()).toContain('this is the updated content');
+ });
+ });
+
+ describe('job update in dropdown', () => {
+ beforeEach(async () => {
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(HTTP_STATUS_OK);
+
+ createComponent();
+ await waitForPromises();
+ await nextTick();
+ });
+
+ const clickCiAction = async () => {
+ await openStageDropdown();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ await findCiActionBtn().trigger('click');
+ };
+
+ it('keeps dropdown open when job item action is clicked', async () => {
+ await clickCiAction();
+ await waitForPromises();
+
+ expect(findDropdown().classes('show')).toBe(true);
+ });
+ });
+
+ describe('With merge trains enabled', () => {
+ it('shows a warning on the dropdown', async () => {
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ createComponent({
+ isMergeTrain: true,
+ });
+
+ await openStageDropdown();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ const warning = findMergeTrainWarning();
+
+ expect(warning.text()).toBe('Merge train pipeline jobs can not be retried');
+ });
+ });
+
+ describe('With merge trains disabled', () => {
+ beforeEach(async () => {
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ createComponent();
+
+ await openStageDropdown();
+ await axios.waitForAll();
+ });
+
+ it('does not show a warning on the dropdown', () => {
+ const warning = findMergeTrainWarning();
+
+ expect(warning.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index 21d92fec9bf..1989aad12b0 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -1,247 +1,46 @@
-import { GlDropdown } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
-import eventHub from '~/pipelines/event_hub';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { stageReply } from '../../mock_data';
-
-const dropdownPath = 'path.json';
-
-describe('Pipelines stage component', () => {
- let wrapper;
- let mock;
- let glTooltipDirectiveMock;
-
- const createComponent = (props = {}) => {
- glTooltipDirectiveMock = jest.fn();
- wrapper = mount(PipelineStage, {
- attachTo: document.body,
- directives: {
- GlTooltip: glTooltipDirectiveMock,
- },
- propsData: {
- stage: {
- status: {
- group: 'success',
- icon: 'status_success',
- title: 'success',
- },
- dropdown_path: dropdownPath,
- },
- updateDropdown: false,
- ...props,
- },
- });
- };
+import createMockApollo from 'helpers/mock_apollo_helper';
- beforeEach(() => {
- mock = new MockAdapter(axios);
- jest.spyOn(eventHub, '$emit');
- });
+import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql';
+import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
- afterEach(() => {
- eventHub.$emit.mockRestore();
- mock.restore();
- // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
- wrapper.destroy();
- });
+Vue.use(VueApollo);
- const findCiActionBtn = () => wrapper.find('.js-ci-action');
- const findCiIcon = () => wrapper.findComponent(CiIcon);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
- const findDropdownMenu = () =>
- wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
- const findDropdownMenuTitle = () =>
- wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]');
- const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
- const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]');
+describe('PipelineStage', () => {
+ let wrapper;
+ let pipelineStageResponse;
- const openStageDropdown = async () => {
- await findDropdownToggle().trigger('click');
- await waitForPromises();
- await nextTick();
+ const defaultProps = {
+ pipelineEtag: '/etag',
+ stageId: '1',
};
- describe('loading state', () => {
- beforeEach(async () => {
- createComponent({ updateDropdown: true });
-
- mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ const createComponent = ({ pipelineStageHandler = pipelineStageResponse } = {}) => {
+ const handlers = [[getPipelineStageQuery, pipelineStageHandler]];
+ const mockApollo = createMockApollo(handlers);
- await openStageDropdown();
- });
-
- it('displays loading state while jobs are being fetched', async () => {
- jest.runOnlyPendingTimers();
- await nextTick();
-
- expect(findLoadingState().exists()).toBe(true);
- expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText);
+ wrapper = shallowMountExtended(PipelineStage, {
+ propsData: {
+ ...defaultProps,
+ },
+ apolloProvider: mockApollo,
});
- it('does not display loading state after jobs have been fetched', async () => {
- await waitForPromises();
+ return waitForPromises();
+ };
- expect(findLoadingState().exists()).toBe(false);
- });
- });
+ const findPipelineStage = () => wrapper.findComponent(PipelineStage);
- describe('default appearance', () => {
+ describe('when mounted', () => {
beforeEach(() => {
createComponent();
});
- it('sets up the tooltip to not have a show delay animation', () => {
- expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true);
- });
-
- it('renders a dropdown with the status icon', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findDropdownToggle().exists()).toBe(true);
- expect(findCiIcon().exists()).toBe(true);
- });
-
- it('renders a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('renders a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
- });
-
- describe('when user opens dropdown and stage request is successful', () => {
- beforeEach(async () => {
- mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
- createComponent();
-
- await openStageDropdown();
- await jest.runAllTimers();
- await axios.waitForAll();
- });
-
- it('renders the received data and emits the correct events', () => {
- expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
- expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
- expect(wrapper.emitted('miniGraphStageClick')).toEqual([[]]);
- });
-
- it('refreshes when updateDropdown is set to true', async () => {
- expect(mock.history.get).toHaveLength(1);
-
- wrapper.setProps({ updateDropdown: true });
- await axios.waitForAll();
-
- expect(mock.history.get).toHaveLength(2);
- });
- });
-
- describe('when user opens dropdown and stage request fails', () => {
- it('should close the dropdown', async () => {
- mock.onGet(dropdownPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- createComponent();
-
- await openStageDropdown();
- await axios.waitForAll();
- await waitForPromises();
-
- expect(findDropdown().classes('show')).toBe(false);
- });
- });
-
- describe('update endpoint correctly', () => {
- beforeEach(async () => {
- const copyStage = { ...stageReply };
- copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(HTTP_STATUS_OK, copyStage);
- createComponent({
- stage: {
- status: {
- group: 'running',
- icon: 'status_running',
- title: 'running',
- },
- dropdown_path: 'bar.json',
- },
- });
- await axios.waitForAll();
- });
-
- it('should update the stage to request the new endpoint provided', async () => {
- await openStageDropdown();
- jest.runOnlyPendingTimers();
- await waitForPromises();
-
- expect(findDropdownMenu().text()).toContain('this is the updated content');
- });
- });
-
- describe('job update in dropdown', () => {
- beforeEach(async () => {
- mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(HTTP_STATUS_OK);
-
- createComponent();
- await waitForPromises();
- await nextTick();
- });
-
- const clickCiAction = async () => {
- await openStageDropdown();
- jest.runOnlyPendingTimers();
- await waitForPromises();
-
- await findCiActionBtn().trigger('click');
- };
-
- it('keeps dropdown open when job item action is clicked', async () => {
- await clickCiAction();
- await waitForPromises();
-
- expect(findDropdown().classes('show')).toBe(true);
- });
- });
-
- describe('With merge trains enabled', () => {
- it('shows a warning on the dropdown', async () => {
- mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
- createComponent({
- isMergeTrain: true,
- });
-
- await openStageDropdown();
- jest.runOnlyPendingTimers();
- await waitForPromises();
-
- const warning = findMergeTrainWarning();
-
- expect(warning.text()).toBe('Merge train pipeline jobs can not be retried');
- });
- });
-
- describe('With merge trains disabled', () => {
- beforeEach(async () => {
- mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
- createComponent();
-
- await openStageDropdown();
- await axios.waitForAll();
- });
-
- it('does not show a warning on the dropdown', () => {
- const warning = findMergeTrainWarning();
-
- expect(warning.exists()).toBe(false);
+ it('renders job item', () => {
+ expect(findPipelineStage().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index 73e810bde99..c212087b7e3 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
-import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
+import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue';
import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
const mockStages = pipelines[0].details.stages;
@@ -8,8 +8,8 @@ const mockStages = pipelines[0].details.stages;
describe('Pipeline Stages', () => {
let wrapper;
- const findPipelineStages = () => wrapper.findAllComponents(PipelineStage);
- const findPipelineStagesAt = (i) => findPipelineStages().at(i);
+ const findLegacyPipelineStages = () => wrapper.findAllComponents(LegacyPipelineStage);
+ const findPipelineStagesAt = (i) => findLegacyPipelineStages().at(i);
const createComponent = (props = {}) => {
wrapper = shallowMount(PipelineStages, {
@@ -23,14 +23,14 @@ describe('Pipeline Stages', () => {
it('renders stages', () => {
createComponent();
- expect(findPipelineStages()).toHaveLength(mockStages.length);
+ expect(findLegacyPipelineStages()).toHaveLength(mockStages.length);
});
it('does not fail when stages are empty', () => {
createComponent({ stages: [] });
expect(wrapper.exists()).toBe(true);
- expect(findPipelineStages()).toHaveLength(0);
+ expect(findLegacyPipelineStages()).toHaveLength(0);
});
it('update dropdown is false by default', () => {
diff --git a/spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js b/spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js
new file mode 100644
index 00000000000..571c2add626
--- /dev/null
+++ b/spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js
@@ -0,0 +1,62 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ConfidentialityDropdown from '~/sidebar/components/confidential/confidentiality_dropdown.vue';
+
+describe('ConfidentialityDropdown component', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findHiddenInput = () => wrapper.find('input');
+
+ function createComponent() {
+ wrapper = shallowMount(ConfidentialityDropdown, {
+ stubs: {
+ GlCollapsibleListbox,
+ },
+ });
+ }
+
+ describe('with no value selected', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('hidden input value is undefined', () => {
+ expect(findHiddenInput().attributes('value')).toBeUndefined();
+ });
+
+ it('renders default text', () => {
+ expect(findDropdown().props('toggleText')).toBe('Select confidentiality');
+ });
+ });
+
+ describe('when selecting a value', () => {
+ const optionToSelect = { text: 'Not confidential', value: 'false' };
+
+ beforeEach(() => {
+ createComponent();
+ findDropdown().vm.$emit('select', optionToSelect.value);
+ });
+
+ it('updates value of the hidden input', () => {
+ expect(findHiddenInput().attributes('value')).toBe(optionToSelect.value);
+ });
+ });
+
+ describe('when reset is triggered', () => {
+ beforeEach(() => {
+ createComponent();
+ findDropdown().vm.$emit('select', 'true');
+ });
+
+ it('clears dropdown selection', async () => {
+ expect(findDropdown().props('toggleText')).not.toBe('Select confidentiality');
+
+ findDropdown().vm.$emit('reset');
+ await nextTick();
+
+ expect(findDropdown().props('toggleText')).toBe('Select confidentiality');
+ });
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js b/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js
index df5703888e2..ad15dd4a371 100644
--- a/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js
+++ b/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js
@@ -6,7 +6,7 @@ import TracingListFilteredSearch from '~/tracing/components/tracing_list_filtere
describe('TracingListFilteredSearch', () => {
let wrapper;
const initialFilters = [
- { type: 'time_range', value: 'last1h' },
+ { type: 'period', value: '1h' },
{ type: 'service_name', value: 'example-service' },
];
beforeEach(() => {
@@ -26,8 +26,13 @@ describe('TracingListFilteredSearch', () => {
});
it('emits submit event on filtered search submit', () => {
- wrapper.findComponent(GlFilteredSearch).vm.$emit('submit', { filters: [] });
+ wrapper
+ .findComponent(GlFilteredSearch)
+ .vm.$emit('submit', { filters: [{ type: 'period', value: '1h' }] });
+
expect(wrapper.emitted('submit')).toHaveLength(1);
- expect(wrapper.emitted('submit')[0][0]).toEqual({ filters: [] });
+ expect(wrapper.emitted('submit')[0][0]).toEqual({
+ filters: [{ type: 'period', value: '1h' }],
+ });
});
});
diff --git a/spec/frontend/tracing/components/tracing_list_spec.js b/spec/frontend/tracing/components/tracing_list_spec.js
index e28311abb22..9aa37ac9c9c 100644
--- a/spec/frontend/tracing/components/tracing_list_spec.js
+++ b/spec/frontend/tracing/components/tracing_list_spec.js
@@ -11,13 +11,13 @@ import {
filterObjToQuery,
filterObjToFilterToken,
filterTokensToFilterObj,
-} from '~/tracing/utils';
+} from '~/tracing/filters';
import FilteredSearch from '~/tracing/components/tracing_list_filtered_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
jest.mock('~/alert');
-jest.mock('~/tracing/utils');
+jest.mock('~/tracing/filters');
describe('TracingList', () => {
let wrapper;
@@ -146,6 +146,14 @@ describe('TracingList', () => {
expect(filterObjToQuery).toHaveBeenCalledWith(mockUpdatedFilterObj);
expect(findUrlSync().props('query')).toBe(mockUpdatedQuery);
});
+
+ it('fetches traces with filters', () => {
+ expect(observabilityClientMock.fetchTraces).toHaveBeenCalledWith(mockFilterObj);
+
+ findFilteredSearch().vm.$emit('submit', {});
+
+ expect(observabilityClientMock.fetchTraces).toHaveBeenLastCalledWith(mockUpdatedFilterObj);
+ });
});
describe('when tracing is not enabled', () => {
diff --git a/spec/frontend/tracing/utils_spec.js b/spec/frontend/tracing/filters_spec.js
index e726ed63e18..ee396326f45 100644
--- a/spec/frontend/tracing/utils_spec.js
+++ b/spec/frontend/tracing/filters_spec.js
@@ -5,20 +5,18 @@ import {
processFilters,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
import {
- TIME_RANGE_FILTER_TOKEN_TYPE,
+ PERIOD_FILTER_TOKEN_TYPE,
SERVICE_NAME_FILTER_TOKEN_TYPE,
OPERATION_FILTER_TOKEN_TYPE,
TRACE_ID_FILTER_TOKEN_TYPE,
- DURATION_FILTER_TOKEN_TYPE,
-} from '~/tracing/constants';
-
-import {
+ DURATION_MS_FILTER_TOKEN_TYPE,
queryToFilterObj,
filterObjToQuery,
filterObjToFilterToken,
filterTokensToFilterObj,
-} from '~/tracing/utils';
+} from '~/tracing/filters';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils');
@@ -27,11 +25,11 @@ describe('utils', () => {
it('should build a filter obj', () => {
const url = 'http://example.com/';
urlQueryToFilter.mockReturnValue({
- time_range: 'last_7_days',
+ period: '7d',
service: 'my_service',
operation: 'my_operation',
trace_id: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
[FILTERED_SEARCH_TERM]: 'test',
});
@@ -45,11 +43,11 @@ describe('utils', () => {
filteredSearchTermKey: 'search',
});
expect(filterObj).toEqual({
- timeRange: 'last_7_days',
+ period: '7d',
service: 'my_service',
operation: 'my_operation',
traceId: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
search: 'test',
});
});
@@ -60,27 +58,27 @@ describe('utils', () => {
filterToQueryObject.mockReturnValue('mockquery');
const query = filterObjToQuery({
- timeRange: 'last_7_days',
+ period: '7d',
serviceName: 'my_service',
operation: 'my_operation',
traceId: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
search: 'test',
});
expect(filterToQueryObject).toHaveBeenCalledWith(
{
- time_range: 'last_7_days',
+ period: '7d',
service: 'my_service',
operation: 'my_operation',
trace_id: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
'filtered-search-term': 'test',
},
{
customOperators: [
- { applyOnlyToKey: 'duration', operator: '>', prefix: 'gt' },
- { applyOnlyToKey: 'duration', operator: '<', prefix: 'lt' },
+ { applyOnlyToKey: 'durationMs', operator: '>', prefix: 'gt' },
+ { applyOnlyToKey: 'durationMs', operator: '<', prefix: 'lt' },
],
filteredSearchTermKey: 'search',
},
@@ -95,20 +93,20 @@ describe('utils', () => {
prepareTokens.mockReturnValue(mockTokens);
const tokens = filterObjToFilterToken({
- timeRange: 'last_7_days',
+ period: '7d',
serviceName: 'my_service',
operation: 'my_operation',
traceId: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
search: 'test',
});
expect(prepareTokens).toHaveBeenCalledWith({
- [TIME_RANGE_FILTER_TOKEN_TYPE]: 'last_7_days',
+ [PERIOD_FILTER_TOKEN_TYPE]: '7d',
[SERVICE_NAME_FILTER_TOKEN_TYPE]: 'my_service',
[OPERATION_FILTER_TOKEN_TYPE]: 'my_operation',
[TRACE_ID_FILTER_TOKEN_TYPE]: 'my_trace_id',
- [DURATION_FILTER_TOKEN_TYPE]: '500',
+ [DURATION_MS_FILTER_TOKEN_TYPE]: '500',
[FILTERED_SEARCH_TERM]: 'test',
});
expect(tokens).toBe(mockTokens);
@@ -120,10 +118,10 @@ describe('utils', () => {
const mockTokens = [];
processFilters.mockReturnValue({
[SERVICE_NAME_FILTER_TOKEN_TYPE]: 'my_service',
- [TIME_RANGE_FILTER_TOKEN_TYPE]: 'last_7_days',
+ [PERIOD_FILTER_TOKEN_TYPE]: '7d',
[OPERATION_FILTER_TOKEN_TYPE]: 'my_operation',
[TRACE_ID_FILTER_TOKEN_TYPE]: 'my_trace_id',
- [DURATION_FILTER_TOKEN_TYPE]: '500',
+ [DURATION_MS_FILTER_TOKEN_TYPE]: '500',
[FILTERED_SEARCH_TERM]: 'test',
});
@@ -132,10 +130,10 @@ describe('utils', () => {
expect(processFilters).toHaveBeenCalledWith(mockTokens);
expect(filterObj).toEqual({
serviceName: 'my_service',
- timeRange: 'last_7_days',
+ period: '7d',
operation: 'my_operation',
traceId: 'my_trace_id',
- duration: '500',
+ durationMs: '500',
search: 'test',
});
});
diff --git a/spec/graphql/types/work_items/linked_item_type_spec.rb b/spec/graphql/types/work_items/linked_item_type_spec.rb
new file mode 100644
index 00000000000..7d7fda45ce4
--- /dev/null
+++ b/spec/graphql/types/work_items/linked_item_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::LinkedItemType, feature_category: :portfolio_management do
+ specify { expect(described_class.graphql_name).to eq('LinkedWorkItemType') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[linkCreatedAt linkId linkType linkUpdatedAt workItem]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
index d955ec5023e..645e63033c5 100644
--- a/spec/graphql/types/work_items/widget_interface_spec.rb
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::WorkItems::WidgetInterface do
+RSpec.describe Types::WorkItems::WidgetInterface, feature_category: :team_planning do
include GraphqlHelpers
it 'exposes the expected fields' do
@@ -23,6 +23,7 @@ RSpec.describe Types::WorkItems::WidgetInterface do
WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType
WorkItems::Widgets::CurrentUserTodos | Types::WorkItems::Widgets::CurrentUserTodosType
WorkItems::Widgets::AwardEmoji | Types::WorkItems::Widgets::AwardEmojiType
+ WorkItems::Widgets::LinkedItems | Types::WorkItems::Widgets::LinkedItemsType
end
with_them do
diff --git a/spec/graphql/types/work_items/widgets/linked_items_type_spec.rb b/spec/graphql/types/work_items/widgets/linked_items_type_spec.rb
new file mode 100644
index 00000000000..28916070ed8
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/linked_items_type_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::LinkedItemsType, feature_category: :portfolio_management do
+ it 'exposes the expected fields' do
+ expected_fields = %i[type linkedItems]
+
+ expect(described_class.graphql_name).to eq('WorkItemWidgetLinkedItems')
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index 6475dcd7618..1d80aad2127 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -54,19 +54,7 @@ RSpec.describe API::Entities::User do
it_behaves_like 'exposes relationship'
end
- context 'when current user can read user profile and disable_follow_users is switched off' do
- let(:can_read_user_profile) { true }
-
- before do
- stub_feature_flags(disable_follow_users: false)
- user.enabled_following = false
- user.save!
- end
-
- it_behaves_like 'exposes relationship'
- end
-
- context 'when current user can read user profile, disable_follow_users is switched on and user disabled it for themself' do
+ context 'when current user can read user profile and user disabled it for themself' do
let(:can_read_user_profile) { true }
before do
@@ -77,7 +65,7 @@ RSpec.describe API::Entities::User do
it_behaves_like 'does not expose relationship'
end
- context 'when current user can read user profile, disable_follow_users is switched on and current user disabled it for themself' do
+ context 'when current user can read user profile and current user disabled it for themself' do
let(:can_read_user_profile) { true }
before do
diff --git a/spec/lib/api/ml/mlflow/api_helpers_spec.rb b/spec/lib/api/ml/mlflow/api_helpers_spec.rb
new file mode 100644
index 00000000000..4f6a37c66c4
--- /dev/null
+++ b/spec/lib/api/ml/mlflow/api_helpers_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::ApiHelpers, feature_category: :mlops do
+ include described_class
+
+ describe '#packages_url' do
+ subject { packages_url }
+
+ let_it_be(:user_project) { build_stubbed(:project) }
+
+ context 'with an empty relative URL root' do
+ before do
+ allow(Gitlab::Application.routes).to receive(:default_url_options)
+ .and_return(protocol: 'http', host: 'localhost', script_name: '')
+ end
+
+ it { is_expected.to eql("http://localhost/api/v4/projects/#{user_project.id}/packages/generic") }
+ end
+
+ context 'with a forward slash relative URL root' do
+ before do
+ allow(Gitlab::Application.routes).to receive(:default_url_options)
+ .and_return(protocol: 'http', host: 'localhost', script_name: '/')
+ end
+
+ it { is_expected.to eql("http://localhost/api/v4/projects/#{user_project.id}/packages/generic") }
+ end
+
+ context 'with a relative URL root' do
+ before do
+ allow(Gitlab::Application.routes).to receive(:default_url_options)
+ .and_return(protocol: 'http', host: 'localhost', script_name: '/gitlab/root')
+ end
+
+ it { is_expected.to eql("http://localhost/gitlab/root/api/v4/projects/#{user_project.id}/packages/generic") }
+ end
+ end
+end
diff --git a/spec/migrations/20230807083334_add_linked_items_work_item_widget_spec.rb b/spec/migrations/20230807083334_add_linked_items_work_item_widget_spec.rb
new file mode 100644
index 00000000000..cd6da15403f
--- /dev/null
+++ b/spec/migrations/20230807083334_add_linked_items_work_item_widget_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddLinkedItemsWorkItemWidget, :migration, feature_category: :portfolio_management do
+ it_behaves_like 'migration that adds widget to work items definitions', widget_name: 'Linked items' do
+ let(:work_item_type_count) { 8 }
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 4011040a534..7e572e2fdc6 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let_it_be(:user) { create(:user, :public_email) }
let_it_be(:namespace) { create_default(:namespace).freeze }
- let_it_be(:project) { create_default(:project, :repository).freeze }
+ let_it_be_with_refind(:project) { create_default(:project, :repository).freeze }
it 'paginates 15 pipelines per page' do
expect(described_class.default_per_page).to eq(15)
@@ -2280,7 +2280,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
describe '#modified_paths' do
- let(:pipeline) { create(:ci_empty_pipeline, :created) }
+ let(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }
context 'when old and new revisions are set' do
before do
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 73df283d996..7dafec2536f 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -26,7 +26,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:cluster_project) }
it { is_expected.to have_many(:deployment_clusters) }
- it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:successful_deployments) }
it { is_expected.to have_many(:environments).through(:deployments) }
diff --git a/spec/models/concerns/cross_database_ignored_tables_spec.rb b/spec/models/concerns/cross_database_ignored_tables_spec.rb
new file mode 100644
index 00000000000..901a6f39eaf
--- /dev/null
+++ b/spec/models/concerns/cross_database_ignored_tables_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CrossDatabaseIgnoredTables, feature_category: :cell, query_analyzers: false do
+ # We enable only the PreventCrossDatabaseModification query analyzer in these tests
+ before do
+ stub_const("CiModel", ci_model)
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return(
+ [Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification]
+ )
+ end
+
+ around do |example|
+ Gitlab::Database::QueryAnalyzer.instance.within { example.run }
+ end
+
+ let(:cross_database_exception) do
+ Gitlab::Database::QueryAnalyzers::
+ PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError
+ end
+
+ let(:ci_model) do
+ Class.new(Ci::ApplicationRecord) do
+ self.table_name = '_test_gitlab_ci_items'
+
+ belongs_to :main_model_object, class_name: 'MainModel',
+ inverse_of: 'ci_model_object', foreign_key: 'main_model_id'
+ end
+ end
+
+ before_all do
+ Ci::ApplicationRecord.connection.execute(
+ 'CREATE TABLE _test_gitlab_ci_items(
+ id BIGSERIAL PRIMARY KEY, main_model_id INTEGER, updated_at timestamp without time zone
+ )'
+ )
+ ApplicationRecord.connection.execute(
+ 'CREATE TABLE _test_gitlab_main_items(
+ id BIGSERIAL PRIMARY KEY, updated_at timestamp without time zone
+ )'
+ )
+ end
+
+ after(:all) do
+ ApplicationRecord.connection.execute('DROP TABLE _test_gitlab_main_items')
+ Ci::ApplicationRecord.connection.execute('DROP TABLE _test_gitlab_ci_items')
+ end
+
+ describe '.cross_database_ignore_tables' do
+ context 'when the tables are not ignored' do
+ before do
+ stub_const("MainModel", create_main_model([], []))
+ end
+
+ it 'raises an error when we doing cross-database modification using create' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+
+ it 'raises an error when we doing cross-database modification using update' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'raises an error when we doing cross-database modification using destroy' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on save' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[save]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'does not raise an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.not_to raise_error
+ end
+
+ it 'still raises an error when deleting an object' do # save doesn't include destroy
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on save with if statement' do
+ before do
+ stub_const(
+ "MainModel",
+ create_main_model(
+ %w[_test_gitlab_ci_items],
+ %I[save],
+ & proc { condition }
+ )
+ )
+
+ expect_next_instance_of(MainModel) do |instance|
+ allow(instance).to receive(:condition).and_return(condition_value)
+ end
+ end
+
+ context 'when condition returns true' do
+ let(:condition_value) { true }
+
+ it 'does not raise an error on creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+ end
+
+ context 'when condition returns false' do
+ let(:condition_value) { false }
+
+ it 'raises an error on creating a new object' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+ end
+ end
+
+ context 'when the tables are ignored on create' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[create]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'raises an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'still raises an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on update' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[update]))
+ end
+
+ it 'raises an error when creating a new object' do
+ expect { MainModel.create! }.to raise_error(cross_database_exception)
+ end
+
+ it 'does not raise an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.not_to raise_error
+ end
+
+ it 'still raises an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.to raise_error(cross_database_exception)
+ end
+ end
+
+ context 'when the tables are ignored on create and destroy' do
+ before do
+ stub_const("MainModel", create_main_model(%w[_test_gitlab_ci_items], %I[create destroy]))
+ end
+
+ it 'does not raise an error when creating a new object' do
+ expect { MainModel.create! }.not_to raise_error
+ end
+
+ it 'raises an error when updating an existing object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.update!(updated_at: Time.zone.now) }.to raise_error(cross_database_exception)
+ end
+
+ it 'does not raise an error when deleting an object' do
+ main_model_object = create_main_model_object
+ expect { main_model_object.destroy! }.not_to raise_error
+ end
+ end
+ end
+
+ def create_main_model(ignored_tables, events, &condition_block)
+ klass = Class.new(ApplicationRecord) do
+ include CrossDatabaseIgnoredTables
+
+ self.table_name = '_test_gitlab_main_items'
+
+ has_one :ci_model_object, autosave: true, class_name: 'CiModel',
+ inverse_of: 'main_model_object', foreign_key: 'main_model_id',
+ dependent: :nullify, touch: true
+ before_create :prepare_ci_model_object
+
+ def condition
+ true
+ end
+
+ def prepare_ci_model_object
+ build_ci_model_object
+ end
+ end
+
+ if ignored_tables.any? && events.any?
+ klass.class_eval do
+ cross_database_ignore_tables ignored_tables, on: events, url: "TODO", if: condition_block
+ end
+ end
+
+ klass
+ end
+
+ # This helper allows creating a test model object without raising a cross database exception
+ def create_main_model_object
+ Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
+ [CiModel.table_name], url: "TODO"
+ ) do
+ MainModel.create!
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 2ad92907d77..d20069d15e0 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
it { is_expected.to belong_to(:cluster_agent).optional }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_one(:upcoming_deployment) }
it { is_expected.to have_one(:latest_opened_most_severe_alert) }
diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb
index 9b8601e4052..7c4f392fcdc 100644
--- a/spec/models/metrics/dashboard/annotation_spec.rb
+++ b/spec/models/metrics/dashboard/annotation_spec.rb
@@ -5,11 +5,6 @@ require 'spec_helper'
RSpec.describe Metrics::Dashboard::Annotation do
using RSpec::Parameterized::TableSyntax
- describe 'associations' do
- it { is_expected.to belong_to(:environment).inverse_of(:metrics_dashboard_annotations) }
- it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster').inverse_of(:metrics_dashboard_annotations) }
- end
-
describe 'validation' do
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:dashboard_path) }
@@ -18,18 +13,6 @@ RSpec.describe Metrics::Dashboard::Annotation do
it { is_expected.to validate_length_of(:panel_xid).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(255) }
- context 'orphaned annotation' do
- subject { build(:metrics_dashboard_annotation, environment: nil) }
-
- it { is_expected.not_to be_valid }
-
- it 'reports error about both missing relations' do
- subject.valid?
-
- expect(subject.errors.full_messages).to include(/Annotation must belong to a cluster or an environment/)
- end
- end
-
context 'ending_at_after_starting_at' do
where(:starting_at, :ending_at, :valid?, :message) do
2.days.ago.beginning_of_day | 1.day.ago.beginning_of_day | true | nil
@@ -49,28 +32,6 @@ RSpec.describe Metrics::Dashboard::Annotation do
end
end
end
-
- context 'environments annotation' do
- subject { build(:metrics_dashboard_annotation) }
-
- it { is_expected.to be_valid }
- end
-
- context 'clusters annotation' do
- subject { build(:metrics_dashboard_annotation, :with_cluster) }
-
- it { is_expected.to be_valid }
- end
-
- context 'annotation with shared ownership' do
- subject { build(:metrics_dashboard_annotation, :with_cluster, environment: build(:environment)) }
-
- it 'reports error about both shared ownership' do
- subject.valid?
-
- expect(subject.errors.full_messages).to include(/Annotation can't belong to both a cluster and an environment at the same time/)
- end
- end
end
describe 'scopes' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5d964044041..788600194a5 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4079,32 +4079,6 @@ RSpec.describe User, feature_category: :user_profile do
expect(user.following?(followee)).to be_falsey
end
-
- context 'when disable_follow_users feature flag is off' do
- before do
- stub_feature_flags(disable_follow_users: false)
- end
-
- it 'follows user even if user disabled following' do
- user = create(:user)
- user.enabled_following = false
-
- followee = create(:user)
-
- expect(user.follow(followee)).to be_truthy
- expect(user.following?(followee)).to be_truthy
- end
-
- it 'follows user even if followee user disabled following' do
- user = create(:user)
-
- followee = create(:user)
- followee.enabled_following = false
-
- expect(user.follow(followee)).to be_truthy
- expect(user.following?(followee)).to be_truthy
- end
- end
end
describe '#unfollow' do
@@ -4151,15 +4125,11 @@ RSpec.describe User, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
let_it_be(:followee) { create(:user) }
- where(:user_enabled_following, :followee_enabled_following, :feature_flag_status, :result) do
- true | true | false | true
- true | false | false | true
- true | true | true | true
- true | false | true | false
- false | true | false | true
- false | true | true | false
- false | false | false | true
- false | false | true | false
+ where(:user_enabled_following, :followee_enabled_following, :result) do
+ true | true | true
+ true | false | false
+ false | true | false
+ false | false | false
end
with_them do
@@ -4167,7 +4137,6 @@ RSpec.describe User, feature_category: :user_profile do
user.enabled_following = user_enabled_following
followee.enabled_following = followee_enabled_following
followee.save!
- stub_feature_flags(disable_follow_users: feature_flag_status)
end
it { expect(user.following_users_allowed?(followee)).to eq result }
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
index a33e08a1bf2..da772eec39c 100644
--- a/spec/models/work_items/widget_definition_spec.rb
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
::WorkItems::Widgets::Notes,
::WorkItems::Widgets::Notifications,
::WorkItems::Widgets::CurrentUserTodos,
- ::WorkItems::Widgets::AwardEmoji
+ ::WorkItems::Widgets::AwardEmoji,
+ ::WorkItems::Widgets::LinkedItems
]
if Gitlab.ee?
diff --git a/spec/models/work_items/widgets/linked_items_spec.rb b/spec/models/work_items/widgets/linked_items_spec.rb
new file mode 100644
index 00000000000..b4a53b75561
--- /dev/null
+++ b/spec/models/work_items/widgets/linked_items_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::LinkedItems, feature_category: :portfolio_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:work_item) { create(:work_item) }
+ let_it_be(:work_item_link) { create(:work_item_link, source: work_item) }
+
+ describe '.type' do
+ subject { described_class.type }
+
+ it { is_expected.to eq(:linked_items) }
+ end
+
+ describe '#type' do
+ subject { described_class.new(work_item).type }
+
+ it { is_expected.to eq(:linked_items) }
+ end
+
+ describe '#related_issues' do
+ it { expect(described_class.new(work_item).related_issues(user)).to eq(work_item.related_issues(user)) }
+ end
+end
diff --git a/spec/policies/metrics/dashboard/annotation_policy_spec.rb b/spec/policies/metrics/dashboard/annotation_policy_spec.rb
deleted file mode 100644
index 2d1ef0ee0cb..00000000000
--- a/spec/policies/metrics/dashboard/annotation_policy_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Metrics::Dashboard::AnnotationPolicy, :models do
- let(:policy) { described_class.new(user, annotation) }
-
- let_it_be(:user) { create(:user) }
-
- shared_examples 'metrics dashboard annotation policy' do
- context 'when guest' do
- before do
- project.add_guest(user)
- end
-
- it { expect(policy).to be_disallowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :admin_metrics_dashboard_annotation }
- end
-
- context 'when reporter' do
- before do
- project.add_reporter(user)
- end
-
- it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :admin_metrics_dashboard_annotation }
- end
-
- context 'when developer' do
- before do
- project.add_developer(user)
- end
-
- it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :admin_metrics_dashboard_annotation }
- end
-
- context 'when maintainer' do
- before do
- project.add_maintainer(user)
- end
-
- it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :admin_metrics_dashboard_annotation }
- end
- end
-
- describe 'rules' do
- context 'environments annotation' do
- let_it_be(:environment) { create(:environment) }
- let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
-
- it_behaves_like 'metrics dashboard annotation policy' do
- let(:project) { environment.project }
- end
- end
-
- context 'cluster annotation' do
- let_it_be(:cluster) { create(:cluster, :project) }
- let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: nil, cluster: cluster) }
-
- it_behaves_like 'metrics dashboard annotation policy' do
- let(:project) { cluster.project }
- end
- end
- end
-end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index 09977cd19d7..c81f6381398 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository) }
- let_it_be(:environment) { create(:environment, project: project) }
- let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
+ let_it_be(:annotation) { create(:metrics_dashboard_annotation) }
let(:variables) { { id: GitlabSchema.id_from_object(annotation).to_s } }
let(:mutation) { graphql_mutation(:delete_annotation, variables) }
@@ -28,14 +27,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
project.add_developer(current_user)
end
- context 'with valid params' do
- it 'deletes the annotation' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to change { Metrics::Dashboard::Annotation.count }.by(-1)
- end
- end
-
context 'with invalid params' do
let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
@@ -44,21 +35,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
end
end
- context 'when the delete fails' do
- let(:service_response) { { message: 'Annotation has not been deleted', status: :error, last_step: :delete } }
-
- before do
- allow_next_instance_of(Metrics::Dashboard::Annotations::DeleteService) do |delete_service|
- allow(delete_service).to receive(:execute).and_return(service_response)
- end
- end
- it 'returns the error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['errors']).to eq([service_response[:message]])
- end
- end
-
context 'when metrics dashboard feature is unavailable' do
before do
stub_feature_flags(remove_monitor_metrics: true)
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 478112b687a..4aba83dae92 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -361,6 +361,59 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when fetching work item linked items widget' do
+ let_it_be(:related_items) { create_list(:work_item, 3, project: project, milestone: milestone1) }
+
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetLinkedItems {
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ linkCreatedAt
+ linkUpdatedAt
+ workItem {
+ id
+ widgets {
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ before do
+ create(:work_item_link, source: item1, target: related_items[0], link_type: 'relates_to')
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create(:work_item_link, source: item1, target: related_items[1], link_type: 'relates_to')
+ create(:work_item_link, source: item1, target: related_items[2], link_type: 'relates_to')
+
+ expect_graphql_errors_to_be_empty
+ # TODO: Fix N+1 queries executed for the linked work item widgets
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/420605
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control).with_threshold(11)
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 6702224f303..fa354bc1f66 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -539,6 +539,79 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ describe 'linked items widget' do
+ let_it_be(:related_item1) { create(:work_item, project: project) }
+ let_it_be(:related_item2) { create(:work_item, project: project) }
+ let_it_be(:related_item3) { create(:work_item) }
+ let_it_be(:link1) { create(:work_item_link, source: work_item, target: related_item1, link_type: 'relates_to') }
+ let_it_be(:link2) { create(:work_item_link, source: work_item, target: related_item2, link_type: 'relates_to') }
+ let_it_be(:link3) { create(:work_item_link, source: work_item, target: related_item3, link_type: 'relates_to') }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetLinkedItems {
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ linkCreatedAt
+ linkUpdatedAt
+ workItem {
+ id
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'LINKED_ITEMS',
+ 'linkedItems' => { 'nodes' => match_array(
+ [
+ hash_including(
+ 'linkId' => link1.to_gid.to_s, 'linkType' => 'relates_to',
+ 'linkCreatedAt' => link1.created_at.iso8601, 'linkUpdatedAt' => link1.updated_at.iso8601,
+ 'workItem' => { 'id' => related_item1.to_gid.to_s }
+ ),
+ hash_including(
+ 'linkId' => link2.to_gid.to_s, 'linkType' => 'relates_to',
+ 'linkCreatedAt' => link2.created_at.iso8601, 'linkUpdatedAt' => link2.updated_at.iso8601,
+ 'workItem' => { 'id' => related_item2.to_gid.to_s }
+ )
+ ]
+ ) }
+ )
+ )
+ )
+ end
+
+ context 'when `linked_work_items` feature flag is disabled' do
+ before do
+ stub_feature_flags(linked_work_items: false)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns empty result' do
+ expect(work_item_data).to include(
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'LINKED_ITEMS',
+ 'linkedItems' => { "nodes" => [] }
+ )
+ )
+ )
+ end
+ end
+ end
end
describe 'notes widget' do
diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb
index a85fe4d867a..45479666e9a 100644
--- a/spec/requests/api/ml/mlflow/runs_spec.rb
+++ b/spec/requests/api/ml/mlflow/runs_spec.rb
@@ -39,6 +39,11 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do
response
end
+ before do
+ allow(Gitlab::Application.routes).to receive(:default_url_options)
+ .and_return(protocol: 'http', host: 'www.example.com', script_name: '')
+ end
+
RSpec.shared_examples 'MLflow|run_id param error cases' do
context 'when run id is not passed' do
let(:params) { {} }
@@ -162,6 +167,17 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do
})
end
+ context 'with a relative root URL' do
+ before do
+ allow(Gitlab::Application.routes).to receive(:default_url_options)
+ .and_return(protocol: 'http', host: 'www.example.com', script_name: '/gitlab/root')
+ end
+
+ it 'gets a run including a valid artifact_uri' do
+ expect(json_response['run']['info']['artifact_uri']).to eql("http://www.example.com/gitlab/root/api/v4/projects/#{project_id}/packages/generic/ml_experiment_#{experiment.iid}/#{candidate.iid}/")
+ end
+ end
+
describe 'Error States' do
it_behaves_like 'MLflow|run_id param error cases'
it_behaves_like 'MLflow|shared error cases'
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index a76d575a1e0..769e8adc750 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -31,6 +31,23 @@ RSpec.describe Issuable::BulkUpdateService, feature_category: :team_planning do
end
end
+ shared_examples 'updates confidentiality' do
+ it 'succeeds' do
+ result = bulk_update(issuables, confidential: true)
+
+ expect(result.success?).to be_truthy
+ expect(result.payload[:count]).to eq(issuables.count)
+ end
+
+ it 'updates the issuables confidentiality' do
+ bulk_update(issuables, confidential: true)
+
+ issuables.each do |issuable|
+ expect(issuable.reload.confidential).to be(true)
+ end
+ end
+ end
+
shared_examples 'updating labels' do
def create_issue_with_labels(labels)
create(:labeled_issue, project: project, labels: labels)
@@ -303,6 +320,16 @@ RSpec.describe Issuable::BulkUpdateService, feature_category: :team_planning do
end
end
+ describe 'updating confidentiality' do
+ let(:issuables) { create_list(:issue, 2, project: project) }
+
+ it_behaves_like 'updates confidentiality'
+
+ it_behaves_like 'not scheduling cached group count clear' do
+ let(:params) { { confidential: true } }
+ end
+ end
+
describe 'updating labels' do
let(:bug) { create(:label, project: project) }
let(:regression) { create(:label, project: project) }
@@ -390,6 +417,30 @@ RSpec.describe Issuable::BulkUpdateService, feature_category: :team_planning do
end
end
+ describe 'updating confidentiality' do
+ let_it_be(:project) { create(:project, :repository, group: group) }
+
+ before do
+ group.add_maintainer(user)
+ end
+
+ context 'with issues' do
+ let(:issuables) { create_list(:issue, 2, project: project) }
+
+ it_behaves_like 'updates confidentiality'
+ end
+
+ context 'with merge requests' do
+ let(:issuables) { [create(:merge_request, source_project: project, target_project: project)] }
+
+ it 'does not throw an error' do
+ result = bulk_update(issuables, confidential: true)
+
+ expect(result.success?).to be_truthy
+ end
+ end
+ end
+
describe 'updating labels' do
let(:project) { create(:project, :repository, group: group) }
let(:bug) { create(:group_label, group: group) }
diff --git a/spec/services/metrics/dashboard/annotations/delete_service_spec.rb b/spec/services/metrics/dashboard/annotations/delete_service_spec.rb
deleted file mode 100644
index 557d6d95767..00000000000
--- a/spec/services/metrics/dashboard/annotations/delete_service_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Metrics::Dashboard::Annotations::DeleteService, feature_category: :metrics do
- let(:user) { create(:user) }
- let(:service_instance) { described_class.new(user, annotation) }
-
- shared_examples 'executed annotation deletion' do
- it 'returns success response', :aggregate_failures do
- expect(annotation).to receive(:destroy).and_return(true)
-
- response = service_instance.execute
-
- expect(response[:status]).to be :success
- end
- end
-
- shared_examples 'prevented annotation deletion' do |message|
- it 'returns error response', :aggregate_failures do
- response = service_instance.execute
-
- expect(response[:status]).to be :error
- expect(response[:message]).to eql message
- end
-
- it 'does not change db state' do
- expect(annotation).not_to receive(:destroy)
-
- service_instance.execute
- end
- end
-
- describe '.execute' do
- context 'with specific environment' do
- let(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
- let(:environment) { create(:environment) }
-
- context 'with anonymous user' do
- it_behaves_like 'prevented annotation deletion', 'You are not authorized to delete this annotation'
- end
-
- context 'with maintainer user' do
- before do
- environment.project.add_maintainer(user)
- end
-
- it_behaves_like 'executed annotation deletion'
-
- context 'annotation failed to delete' do
- it 'returns error response', :aggregate_failures do
- allow(annotation).to receive(:destroy).and_return(false)
-
- response = service_instance.execute
-
- expect(response[:status]).to be :error
- expect(response[:message]).to eql 'Annotation has not been deleted'
- end
- end
- end
- end
-
- context 'with specific cluster' do
- let(:annotation) { create(:metrics_dashboard_annotation, cluster: cluster, environment: nil) }
-
- context 'with anonymous user' do
- let(:cluster) { create(:cluster, :project) }
-
- it_behaves_like 'prevented annotation deletion', 'You are not authorized to delete this annotation'
- end
-
- context 'with maintainer user' do
- let(:cluster) { create(:cluster, :project) }
-
- before do
- cluster.project.add_maintainer(user)
- end
-
- it_behaves_like 'executed annotation deletion'
- end
-
- context 'with owner user' do
- let(:cluster) { create(:cluster, :group) }
-
- before do
- cluster.group.add_owner(user)
- end
-
- it_behaves_like 'executed annotation deletion'
- end
- end
- end
-end
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 9ff3d9208fa..4cd78bc3b9c 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -203,15 +203,6 @@ RSpec.describe Users::UpdateService, feature_category: :user_profile do
expect(user.enabled_following).to eq(false)
end
- it 'does not remove followers/followees if feature flag is off' do
- stub_feature_flags(disable_follow_users: false)
-
- expect do
- update_user(user, enabled_following: false)
- end.to not_change { user.followed_users.count }
- .and not_change { user.following_users.count }
- end
-
context 'when there is more followers/followees then batch limit' do
before do
stub_env('BATCH_SIZE', 1)
diff --git a/spec/support/database/auto_explain.rb b/spec/support/database/auto_explain.rb
index 799457034a1..108d88e37b9 100644
--- a/spec/support/database/auto_explain.rb
+++ b/spec/support/database/auto_explain.rb
@@ -115,16 +115,11 @@ module AutoExplain
private
def record_auto_explain?(connection)
- return false unless ENV['CI']
- return false if ENV['CI_JOB_NAME_SLUG'] == 'db-migrate-non-superuser'
- return false if connection.database_version.to_s[0..1].to_i < 14
- return false if connection.select_one('SHOW is_superuser')['is_superuser'] != 'on'
-
- # This condition matches the pipeline rules for if-merge-request-labels-record-queries
- return true if ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries')
-
- # This condition matches the pipeline rules for if-default-branch-refs
- ENV['CI_COMMIT_REF_NAME'] == ENV['CI_DEFAULT_BRANCH'] && !ENV['CI_MERGE_REQUEST_IID']
+ ENV['CI'] \
+ && ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries') \
+ && ENV['CI_JOB_NAME_SLUG'] != 'db-migrate-non-superuser' \
+ && connection.database_version.to_s[0..1].to_i >= 14 \
+ && connection.select_one('SHOW is_superuser')['is_superuser'] == 'on'
end
end
end
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
index cd0cbe733d1..77fa7feacd4 100644
--- a/spec/support/database/prevent_cross_database_modification.rb
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -25,7 +25,7 @@ RSpec.configure do |config|
end
# Reset after execution to preferred state
- config.after do |example_file|
+ config.after do |_example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress_in_rspec = true
::ApplicationRecord.gitlab_transactions_stack.clear
diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
index 28eac52256f..fdb31fa5d9d 100644
--- a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
+++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
@@ -3,12 +3,13 @@
RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:|
let(:migration) { described_class.new }
let(:work_item_definitions) { table(:work_item_widget_definitions) }
+ let(:work_item_type_count) { 7 }
describe '#up' do
it "creates widget definition in all types" do
work_item_definitions.where(name: widget_name).delete_all
- expect { migrate! }.to change { work_item_definitions.count }.by(7)
+ expect { migrate! }.to change { work_item_definitions.count }.by(work_item_type_count)
expect(work_item_definitions.all.pluck(:name)).to include(widget_name)
end
@@ -26,7 +27,7 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do
it "removes definitions for widget" do
migrate!
- expect { migration.down }.to change { work_item_definitions.count }.by(-7)
+ expect { migration.down }.to change { work_item_definitions.count }.by(-work_item_type_count)
expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name)
end
end