diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-05 15:07:43 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-05 15:07:43 +0300 |
commit | 872319738757edc0483346c75a2407f7019b963f (patch) | |
tree | d5953edec6184dda1f53c5994c3ebcebc9e815a2 /spec | |
parent | 8f764d21b0011056e1492d92afe3bd40b847b9f7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
23 files changed, 715 insertions, 350 deletions
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 349d73f13ca..ac13c0f2d9e 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -154,7 +154,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do .and_return(merge_request) end - it 'does not serialize builds in exposed stages', :sidekiq_might_not_need_inline do + it 'does not serialize builds in exposed stages' do get_show_json json_response.dig('pipeline', 'details', 'stages').tap do |stages| @@ -183,7 +183,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'job is cancelable' do let(:job) { create(:ci_build, :running, pipeline: pipeline) } - it 'cancel_path is present with correct redirect', :sidekiq_might_not_need_inline do + it 'cancel_path is present with correct redirect' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path'])) @@ -193,7 +193,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'with web terminal' do let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - it 'exposes the terminal path', :sidekiq_might_not_need_inline do + it 'exposes the terminal path' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['terminal_path']).to match(%r{/terminal}) @@ -268,7 +268,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do project.add_maintainer(user) # Need to be a maintianer to view cluster.path end - it 'exposes the deployment information', :sidekiq_might_not_need_inline do + it 'exposes the deployment information' do get_show_json expect(response).to have_gitlab_http_status(:ok) @@ -292,7 +292,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do sign_in(user) end - it 'user can edit runner', :sidekiq_might_not_need_inline do + it 'user can edit runner' do get_show_json expect(response).to have_gitlab_http_status(:ok) @@ -312,7 +312,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do sign_in(user) end - it 'user can not edit runner', :sidekiq_might_not_need_inline do + it 'user can not edit runner' do get_show_json expect(response).to have_gitlab_http_status(:ok) @@ -331,7 +331,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do sign_in(user) end - it 'user can not edit runner', :sidekiq_might_not_need_inline do + it 'user can not edit runner' do get_show_json expect(response).to have_gitlab_http_status(:ok) @@ -412,7 +412,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'when job has trace' do let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) } - it "has_trace is true", :sidekiq_might_not_need_inline do + it "has_trace is true" do get_show_json expect(response).to match_response_schema('job/job_details') @@ -458,7 +458,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') end - context 'user is a maintainer', :sidekiq_might_not_need_inline do + context 'user is a maintainer' do before do project.add_maintainer(user) @@ -512,7 +512,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do def get_show_json expect { get_show(id: job.id, format: :json) } - .not_to change { Gitlab::GitalyClient.get_request_count } + .to change { Gitlab::GitalyClient.get_request_count }.by(1) # ListCommitsByOid end def get_show(**extra_params) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 3c7f69f0e6e..b4549e4e635 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -93,7 +93,7 @@ describe Projects::PipelinesController do end context 'when performing gitaly calls', :request_store do - it 'limits the Gitaly requests', :sidekiq_might_not_need_inline do + it 'limits the Gitaly requests' do # Isolate from test preparation (Repository#exists? is also cached in RequestStore) RequestStore.end! RequestStore.clear! @@ -101,8 +101,9 @@ describe Projects::PipelinesController do expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + # ListCommitsByOid, RepositoryExists, HasLocalBranches expect { get_pipelines_index_json } - .to change { Gitlab::GitalyClient.get_request_count }.by(2) + .to change { Gitlab::GitalyClient.get_request_count }.by(3) end end diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index 5b528c3be52..6592d8d5702 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -184,19 +184,39 @@ describe Projects::ReleasesController do sign_in(user) end - it 'returns the correct evidence summary as a json' do - subject + context 'when the user is a developer' do + it 'returns the correct evidence summary as a json' do + subject + + expect(json_response).to eq(release.evidence.summary) + end - expect(json_response).to eq(release.evidence.summary) + context 'when the release was created before evidence existed' do + before do + release.evidence.destroy + end + + it 'returns an empty json' do + subject + + expect(json_response).to eq({}) + end + end end - context 'when the release was created before evidence existed' do - it 'returns an empty json' do - release.evidence.destroy + context 'when the user is a guest for the project' do + before do + project.add_guest(user) + end - subject + context 'when the project is private' do + let(:project) { private_project } + + it_behaves_like 'not found' + end - expect(json_response).to eq({}) + context 'when the project is public' do + it_behaves_like 'successful request' end end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index d4fab48b426..a42916a83a6 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -43,6 +43,7 @@ describe 'Database schema' do geo_nodes: %w[oauth_application_id], geo_repository_deleted_events: %w[project_id], geo_upload_deleted_events: %w[upload_id model_id], + gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id], import_failures: %w[project_id], identities: %w[user_id], issues: %w[last_edited_by_id state_id], diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index cdffd2ae2f6..3743ef0f25d 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -96,7 +96,7 @@ describe 'Merge request > User sees deployment widget', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page).to have_content("Failed to deploy to #{environment.name}") + expect(page).to have_content("Canceled deploy to #{environment.name}") expect(page).not_to have_css('.js-deploy-time') end end diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js index 6148f3c68f2..b6851a0e24c 100644 --- a/spec/frontend/issuables_list/components/issuable_spec.js +++ b/spec/frontend/issuables_list/components/issuable_spec.js @@ -45,6 +45,7 @@ describe('Issuable component', () => { ...props, }, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js index e598a9c5a5d..dad4e74cb40 100644 --- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js +++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js @@ -49,6 +49,7 @@ describe('Issuables list component', () => { }, localVue, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap new file mode 100644 index 00000000000..c6dbb1da8e9 --- /dev/null +++ b/spec/frontend/registry/settings/components/__snapshots__/registry_settings_app_spec.js.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Registry List renders 1`] = ` +<div> + <p> + + Tag retention policies are designed to: + + </p> + + <ul> + <li> + Keep and protect the images that matter most. + </li> + + <li> + + Automatically remove extra images that aren't designed to be kept. + + </li> + </ul> + + <p> + Read more about the + <a + href="foo" + target="_blank" + > + Container Registry tag retention policies + </a> + </p> +</div> +`; diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js new file mode 100644 index 00000000000..666d970aa6b --- /dev/null +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -0,0 +1,40 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import component from '~/registry/settings/components/registry_settings_app.vue'; +import { createStore } from '~/registry/settings/stores/'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Registry List', () => { + let wrapper; + let store; + + const helpPagePath = 'foo'; + const findHelpLink = () => wrapper.find({ ref: 'help-link' }).find('a'); + + const mountComponent = (options = {}) => + shallowMount(component, { + sync: false, + store, + ...options, + }); + + beforeEach(() => { + store = createStore(); + store.dispatch('setInitialState', { helpPagePath }); + wrapper = mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders an help link dependant on the helphPagePath', () => { + expect(findHelpLink().attributes('href')).toBe(helpPagePath); + }); +}); diff --git a/spec/frontend/registry/settings/stores/actions_spec.js b/spec/frontend/registry/settings/stores/actions_spec.js new file mode 100644 index 00000000000..205c3a6ee21 --- /dev/null +++ b/spec/frontend/registry/settings/stores/actions_spec.js @@ -0,0 +1,20 @@ +import * as actions from '~/registry/settings/stores/actions'; +import * as types from '~/registry/settings/stores/mutation_types'; +import testAction from 'helpers/vuex_action_helper'; + +jest.mock('~/flash.js'); + +describe('Actions Registry Store', () => { + describe('setInitialState', () => { + it('should set the initial state', done => { + testAction( + actions.setInitialState, + 'foo', + {}, + [{ type: types.SET_INITIAL_STATE, payload: 'foo' }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/registry/settings/stores/mutations_spec.js b/spec/frontend/registry/settings/stores/mutations_spec.js new file mode 100644 index 00000000000..421cd3f13cb --- /dev/null +++ b/spec/frontend/registry/settings/stores/mutations_spec.js @@ -0,0 +1,21 @@ +import mutations from '~/registry/settings/stores/mutations'; +import * as types from '~/registry/settings/stores/mutation_types'; +import createState from '~/registry/settings/stores/state'; + +describe('Mutations Registry Store', () => { + let mockState; + + beforeEach(() => { + mockState = createState(); + }); + + describe('SET_INITIAL_STATE', () => { + it('should set the initial state', () => { + const payload = { helpPagePath: 'foo', registrySettingsEndpoint: 'bar' }; + const expectedState = { ...mockState, ...payload }; + mutations[types.SET_INITIAL_STATE](mockState, payload); + + expect(mockState.endpoint).toEqual(expectedState.endpoint); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js new file mode 100644 index 00000000000..f8f4cb627dd --- /dev/null +++ b/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js @@ -0,0 +1,32 @@ +import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; + +const deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + details: {}, + status: SUCCESS, + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], +}; + +export default deploymentMockData; diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js new file mode 100644 index 00000000000..78e086e473d --- /dev/null +++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js @@ -0,0 +1,194 @@ +import { mount } from '@vue/test-utils'; +import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue'; +import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue'; +import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; +import DeploymentStopButton from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue'; +import { + CREATED, + RUNNING, + SUCCESS, + FAILED, + CANCELED, +} from '~/vue_merge_request_widget/components/deployment/constants'; +import deploymentMockData from './deployment_mock_data'; + +const deployDetail = { + playable_build: { + retry_path: '/root/test-deployments/-/jobs/1131/retry', + play_path: '/root/test-deployments/-/jobs/1131/play', + }, + isManual: true, +}; + +describe('Deployment component', () => { + let wrapper; + + const factory = (options = {}) => { + // This destroys any wrappers created before a nested call to factory reassigns it + if (wrapper && wrapper.destroy) { + wrapper.destroy(); + } + wrapper = mount(DeploymentComponent, { + ...options, + }); + }; + + beforeEach(() => { + factory({ + propsData: { + deployment: deploymentMockData, + showMetrics: false, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('always renders DeploymentInfo', () => { + expect(wrapper.find(DeploymentInfo).exists()).toBe(true); + }); + + describe('status message and buttons', () => { + const noActions = []; + const noDetails = { isManual: false }; + const deployGroup = [DeploymentViewButton, DeploymentStopButton]; + + describe.each` + status | previous | deploymentDetails | text | actionButtons + ${CREATED} | ${true} | ${deployDetail} | ${'Can deploy manually to'} | ${deployGroup} + ${CREATED} | ${true} | ${noDetails} | ${'Will deploy to'} | ${deployGroup} + ${CREATED} | ${false} | ${deployDetail} | ${'Can deploy manually to'} | ${noActions} + ${CREATED} | ${false} | ${noDetails} | ${'Will deploy to'} | ${noActions} + ${RUNNING} | ${true} | ${deployDetail} | ${'Deploying to'} | ${deployGroup} + ${RUNNING} | ${true} | ${noDetails} | ${'Deploying to'} | ${deployGroup} + ${RUNNING} | ${false} | ${deployDetail} | ${'Deploying to'} | ${noActions} + ${RUNNING} | ${false} | ${noDetails} | ${'Deploying to'} | ${noActions} + ${SUCCESS} | ${true} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} + ${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${deployGroup} + ${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${deployGroup} + ${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${deployGroup} + ${FAILED} | ${true} | ${deployDetail} | ${'Failed to deploy to'} | ${deployGroup} + ${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${deployGroup} + ${FAILED} | ${false} | ${deployDetail} | ${'Failed to deploy to'} | ${noActions} + ${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions} + ${CANCELED} | ${true} | ${deployDetail} | ${'Canceled deploy to'} | ${deployGroup} + ${CANCELED} | ${true} | ${noDetails} | ${'Canceled deploy to'} | ${deployGroup} + ${CANCELED} | ${false} | ${deployDetail} | ${'Canceled deploy to'} | ${noActions} + ${CANCELED} | ${false} | ${noDetails} | ${'Canceled deploy to'} | ${noActions} + `( + '$status + previous: $previous + manual: $deploymentDetails.isManual', + ({ status, previous, deploymentDetails, text, actionButtons }) => { + beforeEach(() => { + const previousOrSuccess = Boolean(previous || status === SUCCESS); + const updatedDeploymentData = { + status, + deployed_at: previous ? deploymentMockData.deployed_at : null, + deployed_at_formatted: previous ? deploymentMockData.deployed_at_formatted : null, + external_url: previousOrSuccess ? deploymentMockData.external_url : null, + external_url_formatted: previousOrSuccess + ? deploymentMockData.external_url_formatted + : null, + stop_url: previousOrSuccess ? deploymentMockData.stop_url : null, + details: deploymentDetails, + }; + + factory({ + propsData: { + showMetrics: false, + deployment: { + ...deploymentMockData, + ...updatedDeploymentData, + }, + }, + }); + }); + + it(`renders the text: ${text}`, () => { + expect(wrapper.find(DeploymentInfo).text()).toContain(text); + }); + + if (actionButtons.length > 0) { + describe('renders the expected button group', () => { + actionButtons.forEach(button => { + it(`renders ${button.name}`, () => { + expect(wrapper.find(button).exists()).toBe(true); + }); + }); + }); + } + + if (actionButtons.length === 0) { + describe('does not render the button group', () => { + [DeploymentViewButton, DeploymentStopButton].forEach(button => { + it(`does not render ${button.name}`, () => { + expect(wrapper.find(button).exists()).toBe(false); + }); + }); + }); + } + + if (actionButtons.includes(DeploymentViewButton)) { + it('renders the View button with expected text', () => { + if (status === SUCCESS) { + expect(wrapper.find(DeploymentViewButton).text()).toContain('View app'); + } else { + expect(wrapper.find(DeploymentViewButton).text()).toContain('View previous app'); + } + }); + } + }, + ); + }); + + describe('hasExternalUrls', () => { + describe('when deployment has both external_url_formatted and external_url', () => { + it('should return true', () => { + expect(wrapper.vm.hasExternalUrls).toEqual(true); + }); + + it('should render the View Button', () => { + expect(wrapper.find(DeploymentViewButton).exists()).toBe(true); + }); + }); + + describe('when deployment has no external_url_formatted', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: { ...deploymentMockData, external_url_formatted: null }, + showMetrics: false, + }, + }); + }); + + it('should return false', () => { + expect(wrapper.vm.hasExternalUrls).toEqual(false); + }); + + it('should not render the View Button', () => { + expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); + }); + }); + + describe('when deployment has no external_url', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: { ...deploymentMockData, external_url: null }, + showMetrics: false, + }, + }); + }); + + it('should return false', () => { + expect(wrapper.vm.hasExternalUrls).toEqual(false); + }); + + it('should not render the View Button', () => { + expect(wrapper.find(DeploymentViewButton).exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js new file mode 100644 index 00000000000..6e3c6f64c68 --- /dev/null +++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js @@ -0,0 +1,118 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; +import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue'; +import deploymentMockData from './deployment_mock_data'; + +describe('Deployment View App button', () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = mount(localVue.extend(DeploymentViewButton), { + localVue, + ...options, + }); + }; + + beforeEach(() => { + factory({ + propsData: { + deployment: deploymentMockData, + isCurrent: true, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('text', () => { + describe('when app is current', () => { + it('shows View app', () => { + expect(wrapper.find(ReviewAppLink).text()).toContain('View app'); + }); + }); + + describe('when app is not current', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: deploymentMockData, + isCurrent: false, + }, + }); + }); + + it('shows View Previous app', () => { + expect(wrapper.find(ReviewAppLink).text()).toContain('View previous app'); + }); + }); + }); + + describe('without changes', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: { ...deploymentMockData, changes: null }, + isCurrent: false, + }, + }); + }); + + it('renders the link to the review app without dropdown', () => { + expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false); + }); + }); + + describe('with a single change', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] }, + isCurrent: false, + }, + }); + }); + + it('renders the link to the review app without dropdown', () => { + expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false); + }); + + it('renders the link to the review app linked to to the first change', () => { + const expectedUrl = deploymentMockData.changes[0].external_url; + const deployUrl = wrapper.find('.js-deploy-url'); + + expect(deployUrl.attributes().href).not.toBeNull(); + expect(deployUrl.attributes().href).toEqual(expectedUrl); + }); + }); + + describe('with multiple changes', () => { + beforeEach(() => { + factory({ + propsData: { + deployment: deploymentMockData, + isCurrent: false, + }, + }); + }); + + it('renders the link to the review app with dropdown', () => { + expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(true); + }); + + it('renders all the links to the review apps', () => { + const allUrls = wrapper.findAll('.js-deploy-url-menu-item').wrappers; + const expectedUrls = deploymentMockData.changes.map(change => change.external_url); + + expectedUrls.forEach((expectedUrl, idx) => { + const deployUrl = allUrls[idx]; + + expect(deployUrl.attributes().href).not.toBeNull(); + expect(deployUrl.attributes().href).toEqual(expectedUrl); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js deleted file mode 100644 index 1949bee1406..00000000000 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ /dev/null @@ -1,313 +0,0 @@ -import Vue from 'vue'; -import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; -import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; -import { getTimeago } from '~/lib/utils/datetime_utility'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Deployment component', () => { - const Component = Vue.extend(deploymentComponent); - let deploymentMockData; - - beforeEach(() => { - deploymentMockData = { - id: 15, - name: 'review/diplo', - url: '/root/review-apps/environments/15', - stop_url: '/root/review-apps/environments/15/stop', - metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/review-apps/environments/15/metrics', - external_url: 'http://gitlab.com.', - external_url_formatted: 'gitlab', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], - }; - }); - - let vm; - - afterEach(() => { - vm.$destroy(); - }); - - describe('', () => { - beforeEach(() => { - vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true }); - }); - - describe('deployTimeago', () => { - it('return formatted date', () => { - const readable = getTimeago().format(deploymentMockData.deployed_at); - - expect(vm.deployTimeago).toEqual(readable); - }); - }); - - describe('hasExternalUrls', () => { - it('should return true', () => { - expect(vm.hasExternalUrls).toEqual(true); - }); - - it('should return false when deployment has no external_url_formatted', () => { - vm.deployment.external_url_formatted = null; - - expect(vm.hasExternalUrls).toEqual(false); - }); - - it('should return false when deployment has no external_url', () => { - vm.deployment.external_url = null; - - expect(vm.hasExternalUrls).toEqual(false); - }); - }); - - describe('hasDeploymentTime', () => { - it('should return true', () => { - expect(vm.hasDeploymentTime).toEqual(true); - }); - - it('should return false when deployment has no deployed_at', () => { - vm.deployment.deployed_at = null; - - expect(vm.hasDeploymentTime).toEqual(false); - }); - - it('should return false when deployment has no deployed_at_formatted', () => { - vm.deployment.deployed_at_formatted = null; - - expect(vm.hasDeploymentTime).toEqual(false); - }); - }); - - describe('hasDeploymentMeta', () => { - it('should return true', () => { - expect(vm.hasDeploymentMeta).toEqual(true); - }); - - it('should return false when deployment has no url', () => { - vm.deployment.url = null; - - expect(vm.hasDeploymentMeta).toEqual(false); - }); - - it('should return false when deployment has no name', () => { - vm.deployment.name = null; - - expect(vm.hasDeploymentMeta).toEqual(false); - }); - }); - - describe('stopEnvironment', () => { - const url = '/foo/bar'; - const returnPromise = () => - new Promise(resolve => { - resolve({ - data: { - redirect_url: url, - }, - }); - }); - const mockStopEnvironment = () => { - vm.stopEnvironment(deploymentMockData); - return vm; - }; - - it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => { - spyOn(window, 'confirm').and.returnValue(true); - spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); - const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true); - vm = mockStopEnvironment(); - - expect(window.confirm).toHaveBeenCalled(); - expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); - setTimeout(() => { - expect(visitUrl).toHaveBeenCalledWith(url); - done(); - }, 333); - }); - - it('should show a confirm dialog but should not work if the dialog is rejected', () => { - spyOn(window, 'confirm').and.returnValue(false); - spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false)); - vm = mockStopEnvironment(); - - expect(window.confirm).toHaveBeenCalled(); - expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); - }); - }); - - it('renders deployment name', () => { - expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( - deploymentMockData.url, - ); - - expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); - }); - - it('renders external URL', () => { - expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( - deploymentMockData.external_url, - ); - - expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app'); - }); - - it('renders stop button', () => { - expect(vm.$el.querySelector('.btn')).not.toBeNull(); - }); - - it('renders deployment time', () => { - expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); - }); - - it('renders metrics component', () => { - expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull(); - }); - }); - - describe('with showMetrics enabled', () => { - beforeEach(() => { - vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true }); - }); - - it('shows metrics', () => { - expect(vm.$el).toContainElement('.js-mr-memory-usage'); - }); - }); - - describe('with showMetrics disabled', () => { - beforeEach(() => { - vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: false }); - }); - - it('hides metrics', () => { - expect(vm.$el).not.toContainElement('.js-mr-memory-usage'); - }); - }); - - describe('without changes', () => { - beforeEach(() => { - delete deploymentMockData.changes; - - vm = mountComponent(Component, { deployment: { ...deploymentMockData }, showMetrics: true }); - }); - - it('renders the link to the review app without dropdown', () => { - expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); - expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull(); - }); - }); - - describe('with a single change', () => { - beforeEach(() => { - deploymentMockData.changes = deploymentMockData.changes.slice(0, 1); - - vm = mountComponent(Component, { - deployment: { ...deploymentMockData }, - showMetrics: true, - }); - }); - - it('renders the link to the review app without dropdown', () => { - expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); - expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull(); - }); - - it('renders the link to the review app linked to to the first change', () => { - const expectedUrl = deploymentMockData.changes[0].external_url; - const deployUrl = vm.$el.querySelector('.js-deploy-url'); - - expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); - expect(deployUrl).not.toBeNull(); - expect(deployUrl.href).toEqual(expectedUrl); - }); - }); - - describe('deployment status', () => { - describe('running', () => { - beforeEach(() => { - vm = mountComponent(Component, { - deployment: Object.assign({}, deploymentMockData, { status: 'running' }), - showMetrics: true, - }); - }); - - it('renders information about running deployment', () => { - expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); - }); - - it('renders disabled stop button', () => { - expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled'); - }); - }); - - describe('success', () => { - beforeEach(() => { - vm = mountComponent(Component, { - deployment: Object.assign({}, deploymentMockData, { status: 'success' }), - showMetrics: true, - }); - }); - - it('renders information about finished deployment', () => { - expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to'); - }); - }); - - describe('failed', () => { - beforeEach(() => { - vm = mountComponent(Component, { - deployment: Object.assign({}, deploymentMockData, { status: 'failed' }), - showMetrics: true, - }); - }); - - it('renders information about finished deployment', () => { - expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( - 'Failed to deploy to', - ); - }); - }); - - describe('created', () => { - beforeEach(() => { - vm = mountComponent(Component, { - deployment: Object.assign({}, deploymentMockData, { status: 'created' }), - showMetrics: true, - }); - }); - - it('renders information about created deployment', () => { - expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Will deploy to'); - }); - }); - - describe('canceled', () => { - beforeEach(() => { - vm = mountComponent(Component, { - deployment: Object.assign({}, deploymentMockData, { status: 'canceled' }), - showMetrics: true, - }); - }); - - it('renders information about canceled deployment', () => { - expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( - 'Failed to deploy to', - ); - }); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js new file mode 100644 index 00000000000..6a6d8279c2c --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/deployment_stop_button_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import deploymentStopComponent from '~/vue_merge_request_widget/components/deployment/deployment_stop_button.vue'; +import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Deployment component', () => { + const Component = Vue.extend(deploymentStopComponent); + let deploymentMockData; + + beforeEach(() => { + deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + deployment_manual_actions: [], + status: SUCCESS, + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + }; + }); + + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('', () => { + beforeEach(() => { + vm = mountComponent(Component, { + stopUrl: deploymentMockData.stop_url, + isDeployInProgress: false, + }); + }); + + describe('stopEnvironment', () => { + const url = '/foo/bar'; + const returnPromise = () => + new Promise(resolve => { + resolve({ + data: { + redirect_url: url, + }, + }); + }); + const mockStopEnvironment = () => { + vm.stopEnvironment(deploymentMockData); + return vm; + }; + + it('should show a confirm dialog and call service.stopEnvironment when confirmed', done => { + spyOn(window, 'confirm').and.returnValue(true); + spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); + const visitUrl = spyOnDependency(deploymentStopComponent, 'visitUrl').and.returnValue(true); + vm = mockStopEnvironment(); + + expect(window.confirm).toHaveBeenCalled(); + expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); + setTimeout(() => { + expect(visitUrl).toHaveBeenCalledWith(url); + done(); + }, 333); + }); + + it('should show a confirm dialog but should not work if the dialog is rejected', () => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false)); + vm = mockStopEnvironment(); + + expect(window.confirm).toHaveBeenCalled(); + expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index 99de320ca2b..d15c3552b4a 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import MemoryUsage from '~/vue_merge_request_widget/components/memory_usage.vue'; +import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js index 069bc14e01e..d8d84d087bc 100644 --- a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js +++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js @@ -8,6 +8,7 @@ describe('review app link', () => { const props = { link: '/review', cssClass: 'js-link', + isCurrent: true, }; let vm; let el; diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 40bec001ffe..ff34dafa660 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -1,3 +1,5 @@ +import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; + export default { id: 132, iid: 22, @@ -290,15 +292,20 @@ export const mockStore = { name: 'bogus', external_url: 'https://fake.com', external_url_formatted: 'https://fake.com', + status: SUCCESS, }, { id: 1, name: 'bogus-docs', external_url: 'https://fake.com', external_url_formatted: 'https://fake.com', + status: SUCCESS, }, ], - postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }], + postMergeDeployments: [ + { id: 0, name: 'prod', status: SUCCESS }, + { id: 1, name: 'prod-docs', status: SUCCESS }, + ], troubleshootingDocsPath: 'troubleshooting-docs-path', ciStatus: 'ci-status', hasCI: true, diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 30e0504e4e1..604b21e77fe 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -6,6 +6,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; +import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; const returnPromise = data => new Promise(resolve => { @@ -277,7 +278,9 @@ describe('mrWidgetOptions', () => { describe('fetchDeployments', () => { it('should fetch deployments', done => { - spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }])); + spyOn(vm.service, 'fetchDeployments').and.returnValue( + returnPromise([{ id: 1, status: SUCCESS }]), + ); vm.fetchPreMergeDeployments(); @@ -554,7 +557,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, - status: 'success', + status: SUCCESS, }; beforeEach(done => { diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb new file mode 100644 index 00000000000..729a69347cb --- /dev/null +++ b/spec/lib/api/entities/release_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::Release do + let_it_be(:project) { create(:project) } + let_it_be(:release) { create(:release, :with_evidence, project: project) } + let(:user) { create(:user) } + let(:entity) { described_class.new(release, current_user: user) } + + subject { entity.as_json } + + describe 'evidence' do + context 'when the current user can download code' do + it 'exposes the evidence sha and the json path' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :download_code, project).and_return(true) + + expect(subject[:evidence_sha]).to eq(release.evidence_sha) + expect(subject[:assets][:evidence_file_path]).to eq( + Gitlab::Routing.url_helpers.evidence_project_release_url(project, + release.tag, + format: :json) + ) + end + end + + context 'when the current user cannot download code' do + it 'does not expose any evidence data' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :download_code, project).and_return(false) + + expect(subject.keys).not_to include(:evidence_sha) + expect(subject[:assets].keys).not_to include(:evidence_file_path) + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 95435c6dabd..03340f34305 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4063,4 +4063,54 @@ describe Ci::Build do expect(job.invalid_dependencies).to eq([pre_stage_job_invalid]) end end + + describe '#execute_hooks' do + context 'with project hooks' do + before do + create(:project_hook, project: project, job_events: true) + end + + it 'execute hooks' do + expect_any_instance_of(ProjectHook).to receive(:async_execute) + + build.execute_hooks + end + end + + context 'without relevant project hooks' do + before do + create(:project_hook, project: project, job_events: false) + end + + it 'does not execute a hook' do + expect_any_instance_of(ProjectHook).not_to receive(:async_execute) + + build.execute_hooks + end + end + + context 'with project services' do + before do + create(:service, active: true, job_events: true, project: project) + end + + it 'execute services' do + expect_any_instance_of(Service).to receive(:async_execute) + + build.execute_hooks + end + end + + context 'without relevant project services' do + before do + create(:service, active: true, job_events: false, project: project) + end + + it 'execute services' do + expect_any_instance_of(Service).not_to receive(:async_execute) + + build.execute_hooks + end + end + end end diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb index 2c012f080dd..38f441fbc4d 100644 --- a/spec/services/cohorts_service_spec.rb +++ b/spec/services/cohorts_service_spec.rb @@ -22,73 +22,73 @@ describe CohortsService do expected_cohorts = [ { registration_month: month_start(11), - activity_months: Array.new(12) { { total: 0, percentage: 0 } }, + activity_months: Array.new(11) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(10), - activity_months: [{ total: 2, percentage: 100 }] + Array.new(10) { { total: 1, percentage: 50 } }, + activity_months: Array.new(10) { { total: 1, percentage: 50 } }, total: 2, inactive: 0 }, { registration_month: month_start(9), - activity_months: Array.new(10) { { total: 0, percentage: 0 } }, + activity_months: Array.new(9) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(8), - activity_months: [{ total: 2, percentage: 100 }] + Array.new(8) { { total: 1, percentage: 50 } }, + activity_months: Array.new(8) { { total: 1, percentage: 50 } }, total: 2, inactive: 0 }, { registration_month: month_start(7), - activity_months: Array.new(8) { { total: 0, percentage: 0 } }, + activity_months: Array.new(7) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(6), - activity_months: [{ total: 2, percentage: 100 }] + Array.new(6) { { total: 1, percentage: 50 } }, + activity_months: Array.new(6) { { total: 1, percentage: 50 } }, total: 2, inactive: 0 }, { registration_month: month_start(5), - activity_months: Array.new(6) { { total: 0, percentage: 0 } }, + activity_months: Array.new(5) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(4), - activity_months: [{ total: 2, percentage: 100 }] + Array.new(4) { { total: 1, percentage: 50 } }, + activity_months: Array.new(4) { { total: 1, percentage: 50 } }, total: 2, inactive: 0 }, { registration_month: month_start(3), - activity_months: Array.new(4) { { total: 0, percentage: 0 } }, + activity_months: Array.new(3) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(2), - activity_months: [{ total: 2, percentage: 100 }] + Array.new(2) { { total: 1, percentage: 50 } }, + activity_months: Array.new(2) { { total: 1, percentage: 50 } }, total: 2, inactive: 0 }, { registration_month: month_start(1), - activity_months: Array.new(2) { { total: 0, percentage: 0 } }, + activity_months: Array.new(1) { { total: 0, percentage: 0 } }, total: 0, inactive: 0 }, { registration_month: month_start(0), - activity_months: [{ total: 2, percentage: 100 }], + activity_months: [], total: 2, inactive: 1 } |