summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/issues.rb10
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb20
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb21
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb6
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb4
-rw-r--r--spec/frontend/environments/graphql/mock_data.js10
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js57
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js56
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js114
-rw-r--r--spec/frontend/environments/mock_data.js3
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js10
-rw-r--r--spec/frontend/fixtures/jobs.rb64
-rw-r--r--spec/frontend/jobs/mock_data.js12
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap113
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js25
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js61
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js106
-rw-r--r--spec/graphql/types/issue_type_spec.rb5
-rw-r--r--spec/helpers/integrations_helper_spec.rb3
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb3
-rw-r--r--spec/models/issue_spec.rb26
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb7
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb3
-rw-r--r--spec/serializers/issue_sidebar_basic_entity_spec.rb5
-rw-r--r--spec/services/boards/issues/list_service_spec.rb10
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml31
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml29
-rw-r--r--spec/services/issues/build_service_spec.rb32
-rw-r--r--spec/services/issues/create_service_spec.rb91
-rw-r--r--spec/services/issues/update_service_spec.rb26
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb4
-rw-r--r--spec/services/work_items/create_service_spec.rb31
-rw-r--r--spec/services/work_items/update_service_spec.rb27
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb6
-rw-r--r--spec/workers/incident_management/close_incident_worker_spec.rb2
37 files changed, 768 insertions, 277 deletions
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 54a3fc57a5d..67824a10288 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -86,6 +86,16 @@ FactoryBot.define do
association :work_item_type, :default, :key_result
end
+ trait :incident do
+ issue_type { :incident }
+ association :work_item_type, :default, :incident
+ end
+
+ trait :test_case do
+ issue_type { :test_case }
+ association :work_item_type, :default, :test_case
+ end
+
factory :incident do
issue_type { :incident }
association :work_item_type, :default, :incident
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index 8ff0c294b24..e3989a8a192 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
it 'when merge method is set to merge commit' do
visit(merge_request_path(merge_request))
- click_button('Merge')
+ click_merge_button
puts merge_request.short_merged_commit_sha
@@ -31,7 +31,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
visit(merge_request_path(merge_request))
- click_button('Merge')
+ click_merge_button
expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
@@ -41,7 +41,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
visit(merge_request_path(merge_request))
- click_button('Merge')
+ click_merge_button
expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
@@ -55,7 +55,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
it 'accepts a merge request' do
check('Delete source branch')
- click_button('Merge')
+ click_merge_button
expect(page).to have_content('Changes merged into')
expect(page).not_to have_selector('.js-remove-branch-button')
@@ -72,7 +72,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
end
it 'accepts a merge request' do
- click_button('Merge')
+ click_merge_button
expect(page).to have_content('Changes merged into')
expect(page).to have_selector('.js-remove-branch-button')
@@ -90,7 +90,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
it 'accepts a merge request' do
check('Delete source branch')
- click_button('Merge')
+ click_merge_button
expect(page).to have_content('Changes merged into')
expect(page).not_to have_selector('.js-remove-branch-button')
@@ -112,9 +112,15 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
find('[data-testid="widget_edit_commit_message"]').click
fill_in('merge-message-edit', with: 'wow such merge')
- click_button('Merge')
+ click_merge_button
expect(page).to have_selector('.gl-badge', text: 'Merged')
end
end
+
+ def click_merge_button
+ page.within('.mr-state-widget') do
+ click_button 'Merge'
+ end
+ end
end
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index cdc00017ab3..2a71dd5eac7 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -17,7 +17,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).to have_button 'Merge'
+ page.within('.mr-state-widget') do
+ expect(page).to have_button 'Merge'
+ end
end
end
@@ -56,7 +58,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).not_to have_button('Merge')
+ expect(page).not_to have_button('Merge', exact: true)
expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
end
end
@@ -69,7 +71,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).not_to have_button 'Merge'
+ expect(page).not_to have_button('Merge', exact: true)
expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
end
end
@@ -82,7 +84,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).to have_button 'Merge'
+ expect(page).to have_button('Merge', exact: true)
end
end
@@ -94,7 +96,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).not_to have_button 'Merge'
+ expect(page).not_to have_button('Merge', exact: true)
end
end
end
@@ -126,8 +128,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
visit project_merge_request_path(project, merge_request)
wait_for_requests
-
- expect(page).to have_button 'Merge'
+ page.within('.mr-state-widget') do
+ expect(page).to have_button 'Merge'
+ end
end
end
@@ -139,7 +142,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
- expect(page).to have_button 'Merge'
+ page.within('.mr-state-widget') do
+ expect(page).to have_button 'Merge'
+ end
end
end
end
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index e09a4569caf..da48a31abbd 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -13,7 +13,9 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
visit(merge_request_path(merge_request))
- click_button('Merge')
+ page.within('.mr-state-widget') do
+ click_button 'Merge'
+ end
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index b83580565e4..476be5ab599 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -21,7 +21,7 @@ feature_category: :code_review_workflow do
context 'with unresolved threads' do
it 'does not allow to merge' do
- expect(page).not_to have_button 'Merge'
+ expect(page).not_to have_button('Merge', exact: true)
expect(page).to have_content('all threads must be resolved')
end
end
@@ -33,7 +33,7 @@ feature_category: :code_review_workflow do
end
it 'allows MR to be merged' do
- expect(page).to have_button 'Merge'
+ expect(page).to have_button('Merge', exact: true)
end
end
end
@@ -46,7 +46,7 @@ feature_category: :code_review_workflow do
context 'with unresolved threads' do
it 'does not allow to merge' do
- expect(page).to have_button 'Merge'
+ expect(page).to have_button('Merge', exact: true)
end
end
@@ -57,7 +57,7 @@ feature_category: :code_review_workflow do
end
it 'allows MR to be merged' do
- expect(page).to have_button 'Merge'
+ expect(page).to have_button('Merge', exact: true)
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index acf2893b513..eb293fbbd20 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -396,7 +396,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
end
it 'updates the MR widget', :sidekiq_might_not_need_inline do
- click_button 'Merge'
+ page.within('.mr-state-widget') do
+ click_button 'Merge'
+ end
expect(page).to have_content('An error occurred while merging')
end
@@ -452,7 +454,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
wait_for_requests
- expect(page).not_to have_button('Merge')
+ expect(page).not_to have_button('Merge', exact: true)
expect(page).to have_content('Merging!')
end
end
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index ae3158e4270..1c754943acb 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -21,7 +21,9 @@ feature_category: :code_review_workflow do
before do
sign_in(user)
visit(project_merge_request_path(project, merge_request))
- click_button('Merge')
+ page.within('.mr-state-widget') do
+ click_button 'Merge'
+ end
wait_for_requests
end
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index b5435990042..8d91ffe5ffc 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -801,6 +801,14 @@ export const resolvedDeploymentDetails = {
export const agent = {
project: 'agent-project',
- id: '1',
+ id: 'gid://gitlab/ClusterAgent/1',
name: 'agent-name',
+ kubernetesNamespace: 'agent-namespace',
};
+
+const runningPod = { status: { phase: 'Running' } };
+const pendingPod = { status: { phase: 'Pending' } };
+const succeededPod = { status: { phase: 'Succeeded' } };
+const failedPod = { status: { phase: 'Failed' } };
+
+export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod];
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 2c223d3a1a7..c66844f5f24 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { CoreV1Api } from '@gitlab/cluster-client';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -17,6 +18,7 @@ import {
resolvedEnvironment,
folder,
resolvedFolder,
+ k8sPodsMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -143,6 +145,61 @@ describe('~/frontend/environments/graphql/resolvers', () => {
expect(environmentFolder).toEqual(resolvedFolder);
});
});
+ describe('k8sPods', () => {
+ const namespace = 'default';
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const mockPodsListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sPodsMock,
+ },
+ });
+ });
+
+ const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+ const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
describe('stopEnvironment', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 8673c657760..1912fd4a82b 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -1,19 +1,28 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlCollapse, GlButton } from '@gitlab/ui';
+import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
-
-const agent = {
- project: 'agent-project',
- id: '1',
- name: 'agent-name',
-};
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
const propsData = {
agentId: agent.id,
agentName: agent.name,
agentProjectPath: agent.project,
+ namespace: agent.kubernetesNamespace,
+};
+
+const provide = {
+ kasTunnelUrl: mockKasTunnelUrl,
+};
+
+const configuration = {
+ basePath: provide.kasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
};
describe('~/environments/components/kubernetes_overview.vue', () => {
@@ -22,10 +31,13 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
const findCollapse = () => wrapper.findComponent(GlCollapse);
const findCollapseButton = () => wrapper.findComponent(GlButton);
const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
+ const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = () => {
wrapper = shallowMount(KubernetesOverview, {
propsData,
+ provide,
});
};
@@ -57,6 +69,7 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
it("doesn't render components when the collapse is not visible", () => {
expect(findAgentInfo().exists()).toBe(false);
+ expect(findKubernetesPods().exists()).toBe(false);
});
it('opens on click', async () => {
@@ -70,15 +83,40 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
describe('when section is expanded', () => {
- it('renders kubernetes agent info', async () => {
+ beforeEach(() => {
createWrapper();
- await toggleCollapse();
+ toggleCollapse();
+ });
+ it('renders kubernetes agent info', () => {
expect(findAgentInfo().props()).toEqual({
agentName: agent.name,
agentId: agent.id,
agentProjectPath: agent.project,
});
});
+
+ it('renders kubernetes pods', () => {
+ expect(findKubernetesPods().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+ });
+
+ describe('on cluster error', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('shows alert with the error message', async () => {
+ const error = 'Error message from pods';
+
+ findKubernetesPods().vm.$emit('cluster-error', error);
+ await nextTick();
+
+ expect(findAlert().text()).toBe(error);
+ });
});
});
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
new file mode 100644
index 00000000000..137309d7853
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sPodsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_pods.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAllStats = () => wrapper.findAllComponents(GlSingleStat);
+ const findSingleStat = (at) => findAllStats().at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockReturnValue(k8sPodsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(KubernetesPods, {
+ propsData: { namespace, configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('hides the loading icon when the list of pods loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when gets pods data', () => {
+ it('renders stats', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findAllStats()).toHaveLength(4);
+ });
+
+ it.each`
+ count | title | index
+ ${2} | ${KubernetesPods.i18n.runningPods} | ${0}
+ ${1} | ${KubernetesPods.i18n.pendingPods} | ${1}
+ ${1} | ${KubernetesPods.i18n.succeededPods} | ${2}
+ ${2} | ${KubernetesPods.i18n.failedPods} | ${3}
+ `(
+ 'renders stat with title "$title" and count "$count" at index $index',
+ async ({ count, title, index }) => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findSingleStat(index).props()).toMatchObject({
+ value: count,
+ title,
+ });
+ },
+ );
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it("doesn't show pods stats", () => {
+ expect(findAllStats()).toHaveLength(0);
+ });
+
+ it('emits an error message', () => {
+ expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index a6d67c26304..bd2c6b7c892 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -313,6 +313,8 @@ const createEnvironment = (data = {}) => ({
...data,
});
+const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
+
export {
environment,
environmentsList,
@@ -321,4 +323,5 @@ export {
tableData,
deployBoardMockData,
createEnvironment,
+ mockKasTunnelUrl,
};
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 89a9ca725ba..b4f5263a151 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -12,6 +12,7 @@ import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
Vue.use(VueApollo);
@@ -26,7 +27,13 @@ describe('~/environments/components/new_environment_item.vue', () => {
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData },
+ provide: {
+ helpPagePath: '/help',
+ projectId: '1',
+ projectPath: '/1',
+ kasTunnelUrl: mockKasTunnelUrl,
+ ...provideData,
+ },
stubs: { transition: stubTransition() },
});
@@ -536,6 +543,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
agentProjectPath: agent.project,
agentName: agent.name,
agentId: agent.id,
+ namespace: agent.kubernetesNamespace,
});
});
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 3583beb83c2..a825e1af268 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -48,50 +48,48 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) }
let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) }
- fixtures_path = 'graphql/jobs/'
- get_jobs_query = 'get_jobs.query.graphql'
- full_path = 'frontend-fixtures/builds-project'
+ shared_examples 'graphql queries' do |path, jobs_query|
+ let_it_be(:variables) { {} }
- let_it_be(:query) do
- get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}")
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{path}/#{jobs_query}")
+ end
- it "#{fixtures_path}#{get_jobs_query}.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path
- })
+ fixtures_path = 'graphql/jobs/'
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{jobs_query}.json" do
+ post_graphql(query, current_user: user, variables: variables)
- it "#{fixtures_path}#{get_jobs_query}.as_guest.json" do
- guest = create(:user)
- project.add_guest(guest)
+ expect_graphql_errors_to_be_empty
+ end
- post_graphql(query, current_user: guest, variables: {
- fullPath: full_path
- })
+ it "#{fixtures_path}#{jobs_query}.as_guest.json" do
+ guest = create(:user)
+ project.add_guest(guest)
- expect_graphql_errors_to_be_empty
- end
+ post_graphql(query, current_user: guest, variables: variables)
- it "#{fixtures_path}#{get_jobs_query}.paginated.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 2
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{jobs_query}.paginated.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 2 }))
- it "#{fixtures_path}#{get_jobs_query}.empty.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 0
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{jobs_query}.empty.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 0 }))
+
+ expect_graphql_errors_to_be_empty
+ end
end
+
+ it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do
+ let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
+ end
+
+ it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql'
end
describe 'get_jobs_count.query.graphql', type: :request do
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 483b4ca711f..fb1ded7b4ef 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1,7 +1,9 @@
import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json';
import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json';
import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json';
+import mockAllJobsPaginated from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.paginated.json';
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
+import mockAllJobs from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
import { TEST_HOST } from 'spec/test_constants';
import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -11,8 +13,10 @@ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
// Fixtures generated at spec/frontend/fixtures/jobs.rb
export const mockJobsResponsePaginated = mockJobsPaginated;
+export const mockAllJobsResponsePaginated = mockAllJobsPaginated;
export const mockJobsResponseEmpty = mockJobsEmpty;
export const mockJobsNodes = mockJobs.data.project.jobs.nodes;
+export const mockAllJobsNodes = mockAllJobs.data.jobs.nodes;
export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes;
export const mockJobsCountResponse = mockJobsCount;
@@ -922,6 +926,14 @@ export const stages = [
},
];
+export const statuses = {
+ success: 'SUCCESS',
+ failed: 'FAILED',
+ canceled: 'CANCELED',
+ pending: 'PENDING',
+ running: 'RUNNING',
+};
+
export default {
id: 4757,
artifact: {
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
index dc21db39259..6fd3dce1941 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
@@ -2,99 +2,36 @@
exports[`MlCandidatesShow renders correctly 1`] = `
<div>
+ <incubation-alert-stub
+ featurename="Machine learning experiment tracking"
+ linktofeedbackissue="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
+ />
+
<div
- class="gl-alert gl-alert-warning"
+ class="detail-page-header gl-flex-wrap"
>
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-alert-icon"
- data-testid="warning-icon"
- role="img"
- >
- <use
- href="#warning"
- />
- </svg>
-
<div
- aria-live="assertive"
- class="gl-alert-content"
- role="alert"
+ class="detail-page-header-body"
>
- <h2
- class="gl-alert-title"
- >
- Machine learning experiment tracking is in incubating phase
- </h2>
-
- <div
- class="gl-alert-body"
+ <h1
+ class="page-title gl-font-size-h-display flex-fill"
>
- GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.
-
- <a
- class="gl-link"
- href="https://about.gitlab.com/handbook/engineering/incubation/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Learn more about incubating features
- </a>
- </div>
+ Model candidate details
+
+ </h1>
- <div
- class="gl-alert-actions"
- >
- <a
- class="btn gl-alert-action btn-confirm btn-md gl-button"
- href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Give feedback on this feature
-
- </span>
- </a>
- </div>
+ <delete-button-stub
+ actionprimarytext="Delete candidate"
+ deleteconfirmationtext="Deleting this candidate will delete the associated parameters, metrics, and metadata."
+ deletepath="path_to_candidate"
+ modaltitle="Delete candidate?"
+ />
</div>
-
- <button
- aria-label="Dismiss"
- class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="close-icon"
- role="img"
- >
- <use
- href="#close"
- />
- </svg>
-
- <!---->
- </button>
</div>
- <h3>
-
- Model candidate details
-
- </h3>
-
<table
- class="candidate-details"
+ class="candidate-details gl-w-full"
>
<tbody>
<tr
@@ -143,12 +80,9 @@ exports[`MlCandidatesShow renders correctly 1`] = `
</td>
<td>
- <a
- class="gl-link"
- href="#"
- >
+ <gl-link-stub>
The Experiment
- </a>
+ </gl-link-stub>
</td>
</tr>
@@ -162,12 +96,11 @@ exports[`MlCandidatesShow renders correctly 1`] = `
</td>
<td>
- <a
- class="gl-link"
+ <gl-link-stub
href="path_to_artifact"
>
Artifacts
- </a>
+ </gl-link-stub>
</td>
</tr>
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 36455339041..7d03ab3b509 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -1,6 +1,7 @@
-import { GlAlert } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
describe('MlCandidatesShow', () => {
let wrapper;
@@ -25,23 +26,31 @@ describe('MlCandidatesShow', () => {
experiment_name: 'The Experiment',
experiment_path: 'path/to/experiment',
status: 'SUCCESS',
+ path: 'path_to_candidate',
},
};
- return mountExtended(MlCandidatesShow, { propsData: { candidate } });
+ wrapper = shallowMountExtended(MlCandidatesShow, { propsData: { candidate } });
};
- const findAlert = () => wrapper.findComponent(GlAlert);
+ beforeEach(createWrapper);
- it('shows incubation warning', () => {
- wrapper = createWrapper();
+ const findAlert = () => wrapper.findComponent(IncubationAlert);
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ it('shows incubation warning', () => {
expect(findAlert().exists()).toBe(true);
});
- it('renders correctly', () => {
- wrapper = createWrapper();
+ it('shows delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+ it('passes the delete path to delete button', () => {
+ expect(findDeleteButton().props('deletePath')).toBe('path_to_candidate');
+ });
+
+ it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
new file mode 100644
index 00000000000..e7ac2576b6f
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -0,0 +1,61 @@
+import { GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import getJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql';
+import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue';
+
+import { mockAllJobsResponsePaginated, statuses } from '../../../../../jobs/mock_data';
+
+Vue.use(VueApollo);
+
+describe('Job table app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockAllJobsResponsePaginated);
+
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(JobsTable);
+
+ const createMockApolloProvider = (handler) => {
+ const requestHandlers = [[getJobsQuery, handler]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = ({
+ handler = successHandler,
+ mountFn = shallowMount,
+ data = {},
+ } = {}) => {
+ wrapper = mountFn(AdminJobsTableApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ jobStatuses: statuses,
+ },
+ apolloProvider: createMockApolloProvider(handler),
+ });
+ };
+
+ describe('loaded state', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should display the jobs table with data', () => {
+ expect(findTable().exists()).toBe(true);
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
new file mode 100644
index 00000000000..59e9eda6343
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
@@ -0,0 +1,106 @@
+import cacheConfig from '~/pages/admin/jobs/components/table/graphql/cache_config';
+import {
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+} from '../../../../../../jobs/mock_data';
+
+const firstLoadArgs = { first: 3, statuses: 'PENDING' };
+const runningArgs = { first: 3, statuses: 'RUNNING' };
+
+describe('jobs/components/table/graphql/cache_config', () => {
+ describe('when fetching data with the same statuses', () => {
+ it('should contain cache nodes and a status when merging caches on first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length);
+ expect(res.statuses).toBe('PENDING');
+ });
+
+ it('should add to existing caches when merging caches after first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(
+ CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length,
+ );
+ });
+
+ it('should not add to existing cache if the incoming elements are the same', () => {
+ // simulate that this is the last page
+ const finalExistingCache = {
+ ...CIJobConnectionExistingCache,
+ pageInfo: {
+ hasNextPage: false,
+ },
+ };
+
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ finalExistingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(CIJobConnectionExistingCache.nodes.length);
+ });
+
+ it('should contain the pageInfo key as part of the result', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.pageInfo).toEqual(
+ expect.objectContaining({
+ __typename: 'PageInfo',
+ endCursor: 'eyJpZCI6IjIwNTEifQ',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjIxNzMifQ',
+ }),
+ );
+ });
+ });
+
+ describe('when fetching data with different statuses', () => {
+ it('should reset cache when a cache already exists', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+ {
+ args: runningArgs,
+ },
+ );
+
+ expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes);
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
+ });
+ });
+
+ describe('when incoming data has no nodes', () => {
+ it('should return existing cache', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ { __typename: 'CiJobConnection', count: 500 },
+ {
+ args: { statuses: 'SUCCESS' },
+ },
+ );
+
+ const expectedResponse = {
+ ...CIJobConnectionExistingCache,
+ statuses: 'SUCCESS',
+ };
+
+ expect(res).toEqual(expectedResponse);
+ });
+ });
+});
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 7c6cf137a1e..87f99878a4d 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -265,7 +265,10 @@ RSpec.describe GitlabSchema.types['Issue'] do
context 'for an incident' do
before do
- issue.update!(issue_type: Issue.issue_types[:incident])
+ issue.update!(
+ issue_type: Issue.issue_types[:incident],
+ work_item_type: WorkItems::Type.default_by_type(:incident)
+ )
end
it { is_expected.to be_nil }
diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb
index 9822f9fac05..8be847e1c6c 100644
--- a/spec/helpers/integrations_helper_spec.rb
+++ b/spec/helpers/integrations_helper_spec.rb
@@ -165,7 +165,8 @@ RSpec.describe IntegrationsHelper do
with_them do
before do
- issue.update!(issue_type: issue_type)
+ issue.assign_attributes(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
+ issue.save!(validate: false)
end
it "return the correct i18n issue type" do
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
index 004c89e5ca7..9b3c23e1f87 100644
--- a/spec/helpers/projects/ml/experiments_helper_spec.rb
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -114,7 +114,8 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
'path_to_artifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}",
'experiment_name' => candidate.experiment.name,
'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}",
- 'status' => 'running'
+ 'status' => 'running',
+ 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}"
}
expect(subject['info']).to include(expected_info)
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index dec62815366..d238133b59e 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe Issue, feature_category: :team_planning do
it 'is possible to change type only between selected types' do
issue = create(:issue, old_type, project: reusable_project)
- issue.work_item_type_id = WorkItems::Type.default_by_type(new_type).id
+ issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(new_type), issue_type: new_type)
expect(issue.valid?).to eq(is_valid)
end
@@ -254,7 +254,7 @@ RSpec.describe Issue, feature_category: :team_planning do
describe '#ensure_work_item_type' do
let_it_be(:issue_type) { create(:work_item_type, :issue, :default) }
- let_it_be(:task_type) { create(:work_item_type, :issue, :default) }
+ let_it_be(:incident_type) { create(:work_item_type, :incident, :default) }
let_it_be(:project) { create(:project) }
context 'when a type was already set' do
@@ -271,9 +271,9 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to eq(issue_type.id)
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: task_type, issue_type: 'task')
+ issue.update!(work_item_type: incident_type, issue_type: :incident)
- expect(issue.work_item_type_id).to eq(task_type.id)
+ expect(issue.work_item_type_id).to eq(incident_type.id)
end
it 'ensures a work item type if updated to nil' do
@@ -300,13 +300,23 @@ RSpec.describe Issue, feature_category: :team_planning do
expect(issue.work_item_type_id).to be_nil
expect(WorkItems::Type).not_to receive(:default_by_type)
- issue.update!(work_item_type: task_type, issue_type: 'task')
+ issue.update!(work_item_type: incident_type, issue_type: :incident)
- expect(issue.work_item_type_id).to eq(task_type.id)
+ expect(issue.work_item_type_id).to eq(incident_type.id)
end
end
end
+ describe '#check_issue_type_in_sync' do
+ it 'raises an error if issue_type is out of sync' do
+ issue = build(:issue, issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:task))
+
+ expect do
+ issue.save!
+ end.to raise_error(Issue::IssueTypeOutOfSyncError)
+ end
+ end
+
describe '#record_create_action' do
it 'records the creation action after saving' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_created_action)
@@ -1816,7 +1826,7 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type)
+ issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
end
it do
@@ -1836,7 +1846,7 @@ RSpec.describe Issue, feature_category: :team_planning do
with_them do
before do
- issue.update!(issue_type: issue_type)
+ issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type))
end
it do
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 4cc6bbe01b6..9e5e365ab9d 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -1223,17 +1223,16 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
context 'when unsupported widget input is sent' do
- let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
- let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
+ let_it_be(:work_item) { create(:work_item, :incident, project: project) }
let(:input) do
{
- 'hierarchyWidget' => {}
+ 'assigneesWidget' => { 'assigneeIds' => [developer.to_gid.to_s] }
}
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
+ errors: ["Following widget keys are not supported by Incident type: [:assignees_widget]"]
end
end
end
diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb
index 3b36ec9fdad..78c8e99e3f3 100644
--- a/spec/requests/projects/ml/candidates_controller_spec.rb
+++ b/spec/requests/projects/ml/candidates_controller_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
describe 'DELETE #destroy' do
let_it_be(:candidate_for_deletion) do
- create(:ml_candidates, experiment: experiment, user: user)
+ create(:ml_candidates, project: project, experiment: experiment, user: user)
end
let(:candidate_iid) { candidate_for_deletion.iid }
@@ -76,6 +76,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
it 'deletes the experiment', :aggregate_failures do
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to eq('Candidate removed')
+ expect(response).to redirect_to("/#{project.full_path}/-/ml/experiments/#{experiment.iid}")
expect { Ml::Candidate.find(id: candidate_for_deletion.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
diff --git a/spec/serializers/issue_sidebar_basic_entity_spec.rb b/spec/serializers/issue_sidebar_basic_entity_spec.rb
index 64a271e359a..f24e379ec67 100644
--- a/spec/serializers/issue_sidebar_basic_entity_spec.rb
+++ b/spec/serializers/issue_sidebar_basic_entity_spec.rb
@@ -44,7 +44,10 @@ RSpec.describe IssueSidebarBasicEntity do
context 'for an incident issue' do
before do
- issue.update!(issue_type: Issue.issue_types[:incident])
+ issue.update!(
+ issue_type: Issue.issue_types[:incident],
+ work_item_type: WorkItems::Type.default_by_type(:incident)
+ )
end
it 'is present and true' do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 5e10d1d216c..4b31a041342 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -57,7 +57,15 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
end
context 'when filtering' do
- let_it_be(:incident) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1], issue_type: 'incident') }
+ let_it_be(:incident) do
+ create(
+ :labeled_issue,
+ :incident,
+ project: project,
+ milestone: m1,
+ labels: [development, p1]
+ )
+ end
context 'when filtering by type' do
it 'only returns the specified type' do
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml
new file mode 100644
index 00000000000..12c51828628
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml
@@ -0,0 +1,31 @@
+config:
+ test1:
+ stage: test
+ script: exit 0
+ needs: []
+
+ test2:
+ stage: test
+ when: on_failure
+ script: exit 0
+ needs: []
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: skipped
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml
new file mode 100644
index 00000000000..57b3aa9ae80
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml
@@ -0,0 +1,29 @@
+config:
+ test1:
+ stage: test
+ script: exit 0
+
+ test2:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: skipped
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: skipped
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index fecfc3f3d64..bca6a3cd4f9 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -172,37 +172,5 @@ RSpec.describe Issues::BuildService, feature_category: :team_planning do
end
end
end
-
- describe 'setting issue type' do
- context 'with a corresponding WorkItems::Type' do
- let_it_be(:type_issue_id) { WorkItems::Type.default_issue_type.id }
- let_it_be(:type_incident_id) { WorkItems::Type.default_by_type(:incident).id }
-
- where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
- nil | ref(:guest) | ref(:type_issue_id) | 'issue'
- 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
- 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue'
- 'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident'
- # update once support for test_case is enabled
- 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
- # update once support for requirement is enabled
- 'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue'
- 'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue'
- # ensure that we don't set a value which has a permission check but is an invalid issue type
- 'project' | ref(:guest) | ref(:type_issue_id) | 'issue'
- end
-
- with_them do
- let(:user) { current_user }
-
- it 'builds an issue' do
- issue = build_issue(issue_type: issue_type)
-
- expect(issue.issue_type).to eq(resulting_issue_type)
- expect(issue.work_item_type_id).to eq(work_item_type_id)
- end
- end
- end
- end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index df47780bc89..88dcc870ee7 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -574,36 +574,6 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
end
context 'Quick actions' do
- context 'as work item' do
- let(:opts) do
- {
- title: "My work item",
- work_item_type: work_item_type,
- description: "/shrug"
- }
- end
-
- context 'when work item type is not the default Issue' do
- let(:work_item_type) { create(:work_item_type, namespace: project.namespace) }
-
- it 'saves the work item without applying the quick action' do
- expect(result).to be_success
- expect(issue).to be_persisted
- expect(issue.description).to eq("/shrug")
- end
- end
-
- context 'when work item type is the default Issue' do
- let(:work_item_type) { WorkItems::Type.default_by_type(:issue) }
-
- it 'saves the work item and applies the quick action' do
- expect(result).to be_success
- expect(issue).to be_persisted
- expect(issue.description).to eq(" ¯\\_(ツ)_/¯")
- end
- end
- end
-
context 'with assignee, milestone, and contact in params and command' do
let_it_be(:contact) { create(:contact, group: group) }
@@ -696,6 +666,23 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
expect(issue.labels).to eq([label])
end
end
+
+ context 'when using promote_to_incident' do
+ let(:opts) { { title: 'Title', description: '/promote_to_incident' } }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates an issue with the correct issue type' do
+ expect { result }.to change(Issue, :count).by(1)
+
+ created_issue = Issue.last
+
+ expect(created_issue.issue_type).to eq('incident')
+ expect(created_issue.work_item_type).to eq(WorkItems::Type.default_by_type('incident'))
+ end
+ end
end
context 'resolving discussions' do
@@ -864,5 +851,49 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
subject.execute
end
end
+
+ describe 'setting issue type' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:guest) { user.tap { |u| project.add_guest(u) } }
+ let_it_be(:reporter) { assignee.tap { |u| project.add_reporter(u) } }
+
+ context 'with a corresponding WorkItems::Type' do
+ let_it_be(:type_issue_id) { WorkItems::Type.default_issue_type.id }
+ let_it_be(:type_incident_id) { WorkItems::Type.default_by_type(:incident).id }
+
+ where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
+ nil | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident'
+ # update once support for test_case is enabled
+ 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ # update once support for requirement is enabled
+ 'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ # ensure that we don't set a value which has a permission check but is an invalid issue type
+ 'project' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ end
+
+ with_them do
+ let(:user) { current_user }
+ let(:params) { { title: 'title', issue_type: issue_type } }
+ let(:issue) do
+ described_class.new(
+ container: project,
+ current_user: user,
+ params: params,
+ spam_params: spam_params
+ ).execute[:issue]
+ end
+
+ it 'creates an issue' do
+ expect(issue.issue_type).to eq(resulting_issue_type)
+ expect(issue.work_item_type_id).to eq(work_item_type_id)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index a5a18562ca5..167bff2a492 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1493,31 +1493,5 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
let(:existing_issue) { create(:issue, project: project) }
let(:issuable) { described_class.new(container: project, current_user: user, params: params).execute(existing_issue) }
end
-
- context 'with quick actions' do
- context 'as work item' do
- let(:opts) { { description: "/shrug" } }
-
- context 'when work item type is not the default Issue' do
- let(:issue) { create(:work_item, :task, description: "") }
-
- it 'does not apply the quick action' do
- expect do
- update_issue(opts)
- end.to change(issue, :description).to("/shrug")
- end
- end
-
- context 'when work item type is the default Issue' do
- let(:issue) { create(:work_item, :issue, description: "") }
-
- it 'does not apply the quick action' do
- expect do
- update_issue(opts)
- end.to change(issue, :description).to(" ¯\\_(ツ)_/¯")
- end
- end
- end
- end
end
end
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index b474285e67e..78a17aed707 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -182,7 +182,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
context 'on an incident' do
before do
- issue.update!(issue_type: :incident)
+ issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
end
it 'leaves the note empty' do
@@ -224,7 +224,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
context 'on an incident' do
before do
- issue.update!(issue_type: :incident)
+ issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident))
end
it 'leaves the note empty' do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 0e9e49bc877..46e598c3f11 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -81,6 +81,37 @@ RSpec.describe WorkItems::CreateService, feature_category: :team_planning do
end
end
+ context 'when applying quick actions' do
+ let(:work_item) { service_result[:work_item] }
+ let(:opts) do
+ {
+ title: 'My work item',
+ work_item_type: work_item_type,
+ description: '/shrug'
+ }
+ end
+
+ context 'when work item type is not the default Issue' do
+ let(:work_item_type) { create(:work_item_type, :task, namespace: group) }
+
+ it 'saves the work item without applying the quick action' do
+ expect(service_result).to be_success
+ expect(work_item).to be_persisted
+ expect(work_item.description).to eq('/shrug')
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:work_item_type) { WorkItems::Type.default_by_type(:issue) }
+
+ it 'saves the work item and applies the quick action' do
+ expect(service_result).to be_success
+ expect(work_item).to be_persisted
+ expect(work_item.description).to eq(' ¯\_(ツ)_/¯')
+ end
+ end
+ end
+
context 'when params are valid' do
it 'created instance is a WorkItem' do
expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute)
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 5647f8c085c..2cf52ee853a 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -44,6 +44,33 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
end
end
+ context 'when applying quick actions' do
+ let(:opts) { { description: "/shrug" } }
+
+ context 'when work item type is not the default Issue' do
+ before do
+ task_type = WorkItems::Type.default_by_type(:task)
+ work_item.update_columns(issue_type: task_type.base_type, work_item_type_id: task_type.id)
+ end
+
+ it 'does not apply the quick action' do
+ expect do
+ update_work_item
+ end.to change(work_item, :description).to('/shrug')
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:issue) { create(:work_item, :issue, description: '') }
+
+ it 'applies the quick action' do
+ expect do
+ update_work_item
+ end.to change(work_item, :description).to(' ¯\_(ツ)_/¯')
+ end
+ end
+ end
+
context 'when title is changed' do
let(:opts) { { title: 'changed' } }
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 78863fe1169..c68d53db01e 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -914,9 +914,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by item type' do
- let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) }
- let_it_be(:objective) { create(factory, issue_type: :objective, project: project1) }
- let_it_be(:key_result) { create(factory, issue_type: :key_result, project: project1) }
+ let_it_be(:incident_item) { create(factory, :incident, project: project1) }
+ let_it_be(:objective) { create(factory, :objective, project: project1) }
+ let_it_be(:key_result) { create(factory, :key_result, project: project1) }
context 'no type given' do
let(:params) { { issue_types: [] } }
diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb
index bf967a42ceb..3c2e69a4675 100644
--- a/spec/workers/incident_management/close_incident_worker_spec.rb
+++ b/spec/workers/incident_management/close_incident_worker_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker, feature_category: :incid
context 'when issue type is not incident' do
before do
- issue.update!(issue_type: :issue)
+ issue.update!(issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:issue))
end
it_behaves_like 'does not call the close issue service'