diff options
Diffstat (limited to 'spec')
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' |