Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-01-08 12:07:59 +00:00
parent a821bd6ad1
commit e3e300557f
59 changed files with 398 additions and 446 deletions

View File

@ -483,6 +483,16 @@ export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
/**
* Based on the current location and the string parameters provided
* overwrites the current entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyReplaceState = newUrl => {
window.history.replaceState({}, document.title, newUrl);
};
/**
* Returns true for a String value of "true" and false otherwise.
* This is the opposite of Boolean(...).toString().

View File

@ -1,10 +1,18 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import {
parseBoolean,
historyReplaceState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
import { parseBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate);
Vue.use(GlToast);
document.addEventListener(
'DOMContentLoaded',
@ -21,6 +29,11 @@ document.addEventListener(
},
created() {
this.dataset = document.querySelector(this.$options.el).dataset;
if (doesHashExistInUrl('delete_success')) {
this.$toast.show(__('The pipeline has been deleted'));
historyReplaceState(buildUrlWithCurrentLocation());
}
},
render(createElement) {
return createElement('pipelines-component', {

View File

@ -1,14 +1,17 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
import { __ } from '~/locale';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
GlLoadingIcon,
GlModal,
},
props: {
pipeline: {
@ -33,6 +36,11 @@ export default {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length;
},
deleteModalConfirmationText() {
return __(
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
);
},
},
watch: {
@ -42,6 +50,13 @@ export default {
},
methods: {
onActionClicked(action) {
if (action.modal) {
this.$root.$emit('bv::show::modal', action.modal);
} else {
this.postAction(action);
}
},
postAction(action) {
const index = this.actions.indexOf(action);
@ -49,6 +64,13 @@ export default {
eventHub.$emit('headerPostAction', action);
},
deletePipeline() {
const index = this.actions.findIndex(action => action.modal === DELETE_MODAL_ID);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerDeleteAction', this.actions[index]);
},
getActions() {
const actions = [];
@ -58,7 +80,6 @@ export default {
label: __('Retry'),
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false,
});
}
@ -68,7 +89,16 @@ export default {
label: __('Cancel running'),
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
isLoading: false,
});
}
if (this.pipeline.delete_path) {
actions.push({
label: __('Delete'),
path: this.pipeline.delete_path,
modal: DELETE_MODAL_ID,
cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted',
isLoading: false,
});
}
@ -76,6 +106,7 @@ export default {
return actions;
},
},
DELETE_MODAL_ID,
};
</script>
<template>
@ -88,8 +119,21 @@ export default {
:user="pipeline.user"
:actions="actions"
item-name="Pipeline"
@actionClicked="postAction"
@actionClicked="onActionClicked"
/>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
<gl-modal
:modal-id="$options.DELETE_MODAL_ID"
:title="__('Delete pipeline')"
:ok-title="__('Delete pipeline')"
ok-variant="danger"
@ok="deletePipeline()"
>
<p>
{{ deleteModalConfirmationText }}
</p>
</gl-modal>
</div>
</template>

View File

@ -2,6 +2,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
@ -62,9 +63,11 @@ export default () => {
},
created() {
eventHub.$on('headerPostAction', this.postAction);
eventHub.$on('headerDeleteAction', this.deleteAction);
},
beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction);
eventHub.$off('headerDeleteAction', this.deleteAction);
},
methods: {
postAction(action) {
@ -73,6 +76,13 @@ export default () => {
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
deleteAction(action) {
this.mediator.stopPipelinePoll();
this.mediator.service
.deleteAction(action.path)
.then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
.catch(() => Flash(__('An error occurred while deleting the pipeline.')));
},
},
render(createElement) {
return createElement('pipeline-header', {

View File

@ -35,7 +35,7 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
this.stopPipelinePoll();
}
});
}
@ -51,7 +51,7 @@ export default class pipelinesMediator {
}
refreshPipeline() {
this.poll.stop();
this.stopPipelinePoll();
return this.service
.getPipeline()
@ -64,6 +64,10 @@ export default class pipelinesMediator {
);
}
stopPipelinePoll() {
this.poll.stop();
}
/**
* Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
*/

View File

@ -9,6 +9,11 @@ export default class PipelineService {
return axios.get(this.pipeline, { params });
}
// eslint-disable-next-line class-methods-use-this
deleteAction(endpoint) {
return axios.delete(`${endpoint}.json`);
}
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return axios.post(`${endpoint}.json`);

View File

@ -117,28 +117,7 @@ export default {
<section v-if="actions.length" class="header-action-buttons">
<template v-for="(action, i) in actions">
<gl-link
v-if="action.type === 'link'"
:key="i"
:href="action.path"
:class="action.cssClass"
>
{{ action.label }}
</gl-link>
<gl-link
v-else-if="action.type === 'ujs-link'"
:key="i"
:href="action.path"
:class="action.cssClass"
data-method="post"
rel="nofollow"
>
{{ action.label }}
</gl-link>
<loading-button
v-else-if="action.type === 'button'"
:key="i"
:loading="action.isLoading"
:disabled="action.isLoading"

View File

@ -80,6 +80,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def destroy
::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
redirect_to project_pipelines_path(project), status: :see_other
end
def builds
render_show
end

View File

@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none
end
items = pipelines.no_child
items = pipelines
items = by_scope(items)
items = by_status(items)
items = by_ref(items)

View File

@ -54,10 +54,6 @@ module Ci
def to_partial_path
'projects/generic_commit_statuses/generic_commit_status'
end
def yaml_for_downstream
nil
end
end
end

View File

@ -61,9 +61,7 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
has_one :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
@ -215,7 +213,6 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
@ -511,6 +508,10 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process)
end
def child?
false
end
def latest?
return false unless git_ref && commit.present?
@ -693,24 +694,6 @@ module Ci
all_merge_requests.order(id: :desc)
end
# If pipeline is a child of another pipeline, include the parent
# and the siblings, otherwise return only itself.
def same_family_pipeline_ids
if (parent = parent_pipeline)
[parent.id] + parent.child_pipelines.pluck(:id)
else
[self.id]
end
end
def child?
parent_pipeline.present?
end
def parent?
child_pipelines.exists?
end
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)

View File

@ -23,11 +23,10 @@ module Ci
schedule: 4,
api: 5,
external: 6,
cross_project_pipeline: 7,
pipeline: 7,
chat: 8,
merge_request_event: 10,
external_pull_request_event: 11,
parent_pipeline: 12
external_pull_request_event: 11
}
end
@ -39,8 +38,7 @@ module Ci
repository_source: 1,
auto_devops_source: 2,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
external_project_source: 5
}
end

View File

@ -18,8 +18,6 @@ module Ci
validates :source_project, presence: true
validates :source_job, presence: true
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end
end
end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do
expose :latest?, as: :latest
end

View File

@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
expose :delete_path, if: -> (*) { can_delete? } do |pipeline|
project_pipeline_path(pipeline.project, pipeline)
end
expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline|
pipeline.failed_builds
end
@ -95,6 +99,10 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable?
end
def can_delete?
can?(request.current_user, :destroy_pipeline, pipeline)
end
def has_presentable_merge_request?
pipeline.triggered_by_merge_request? &&
can?(request.current_user, :read_merge_request, pipeline.merge_request)

View File

@ -41,7 +41,6 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations
[
:latest_statuses_ordered_by_stage,
:project,
:stages,
{
failed_builds: %i(project metadata)

View File

@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@ -46,7 +46,6 @@ module Ci
current_user: current_user,
push_options: params[:push_options] || {},
chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
@ -105,14 +104,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(id: pipeline.id)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled
.with_only_interruptible_builds
else
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.same_family_pipeline_ids)
.where.not(id: pipeline.id)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end

View File

@ -44,7 +44,7 @@ module Ci
return error("400 Job has to be running", 400) unless job.running?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
.execute(:cross_project_pipeline, ignore_skip_ci: true) do |pipeline|
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
source = job.sourced_pipelines.build(
source_pipeline: job.pipeline,
source_project: job.project,

View File

@ -0,0 +1,5 @@
---
title: Fix Delete Selected button being active after uploading designs after a deletion
merge_request: 22516
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 20930
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add pipeline deletion button to pipeline details page
merge_request: 21365
author: Fabio Huser
type: added

View File

@ -343,7 +343,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :merge_requests
end
resources :pipelines, only: [:index, :new, :create, :show] do
resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
get :charts

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -305,12 +305,14 @@ For example, the query string
### Accessing pipelines
You can find the current and historical pipeline runs under your project's
**CI/CD > Pipelines** page. Clicking on a pipeline will show the jobs that were run for
that pipeline.
**CI/CD > Pipelines** page. You can also access pipelines for a merge request by navigating
to its **Pipelines** tab.
![Pipelines index page](img/pipelines_index.png)
You can also access pipelines for a merge request by navigating to its **Pipelines** tab.
Clicking on a pipeline will bring you to the **Pipeline Details** page and show
the jobs that were run for that pipeline. From here you can cancel a running pipeline,
retry jobs on a failed pipeline, or [delete a pipeline](#deleting-a-single-pipeline).
### Accessing individual jobs
@ -410,6 +412,20 @@ This functionality is only available:
- For users with at least Developer access.
- If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs).
### Deleting a single pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/24851) in GitLab 12.7.
Users with [owner permissions](../user/permissions.md) in a project can delete a pipeline
by clicking on the pipeline in the **CI/CD > Pipelines** to get to the **Pipeline Details**
page, then using the **Delete** button.
![Pipeline Delete Button](img/pipeline-delete.png)
CAUTION: **Warning:**
Deleting a pipeline will expire all pipeline caches, and delete all related objects,
such as builds, logs, artifacts, and triggers. **This action cannot be undone.**
## Most Recent Pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/50499) in GitLab 12.3.

View File

@ -98,6 +98,10 @@ This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-foss/m
- Memory is through the roof! (TL;DR: Load images but block images requests!): <https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12003>
#### Capybara expectation times out
- [Test imports a project (via Sidekiq) that is growing over time, leading to timeouts when the import takes longer than 60 seconds](https://gitlab.com/gitlab-org/gitlab/merge_requests/22599)
## Resources
- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html)

View File

@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update, :bridge,
:chat_data, :allow_mirror_update,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do

View File

@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers
SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
@ -17,7 +17,7 @@ module Gitlab
].freeze
LEGACY_SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
module Config
class Content
class Bridge < Source
def content
return unless @command.bridge
@command.bridge.yaml_for_downstream
end
def source
:bridge_source
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
module Config
class Content
class Runtime < Source
def content
@command.config_content
end
def source
# The only case when this source is used is when the config content
# is passed in as parameter to Ci::CreatePipelineService.
# This would only occur with parent/child pipelines which is being
# implemented.
# TODO: change source to return :runtime_source
# https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
nil
end
end
end
end
end
end
end
end

View File

@ -1631,6 +1631,9 @@ msgstr ""
msgid "An error occurred while deleting the comment"
msgstr ""
msgid "An error occurred while deleting the pipeline."
msgstr ""
msgid "An error occurred while detecting host keys"
msgstr ""
@ -2092,6 +2095,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
@ -3266,6 +3272,9 @@ msgstr ""
msgid "Checkout"
msgstr ""
msgid "Checkout|%{selectedPlanText} plan"
msgstr ""
msgid "Checkout|1. Your profile"
msgstr ""
@ -3278,9 +3287,36 @@ msgstr ""
msgid "Checkout|Checkout"
msgstr ""
msgid "Checkout|Continue to billing"
msgstr ""
msgid "Checkout|Edit"
msgstr ""
msgid "Checkout|GitLab plan"
msgstr ""
msgid "Checkout|Group"
msgstr ""
msgid "Checkout|Name of company or organization using GitLab"
msgstr ""
msgid "Checkout|Need more users? Purchase GitLab for your %{company}."
msgstr ""
msgid "Checkout|Number of users"
msgstr ""
msgid "Checkout|Subscription details"
msgstr ""
msgid "Checkout|Users"
msgstr ""
msgid "Checkout|company or team"
msgstr ""
msgid "Cherry-pick this commit"
msgstr ""
@ -5735,6 +5771,9 @@ msgstr ""
msgid "Delete list"
msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete snippet"
msgstr ""
@ -18098,6 +18137,9 @@ msgstr ""
msgid "The phase of the development lifecycle."
msgstr ""
msgid "The pipeline has been deleted"
msgstr ""
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
msgstr ""

View File

@ -740,4 +740,51 @@ describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(404)
end
end
describe 'DELETE #destroy' do
let!(:project) { create(:project, :private, :repository) }
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
context 'when user has ability to delete pipeline' do
before do
sign_in(project.owner)
end
it 'deletes pipeline and redirects' do
delete_pipeline
expect(response).to have_gitlab_http_status(303)
expect(Ci::Build.exists?(build.id)).to be_falsy
expect(Ci::Pipeline.exists?(pipeline.id)).to be_falsy
end
context 'and builds are disabled' do
let(:feature) { ProjectFeature::DISABLED }
it 'fails to delete pipeline' do
delete_pipeline
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'when user has no privileges' do
it 'fails to delete pipeline' do
delete_pipeline
expect(response).to have_gitlab_http_status(403)
end
end
def delete_pipeline
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
id: pipeline.id
}
end
end
end

View File

@ -24,17 +24,17 @@ describe 'Import multiple repositories by uploading a manifest file', :js do
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
it 'imports successfully imports a project', :sidekiq_might_not_need_inline do
it 'imports successfully imports a project', :sidekiq_inline do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories'
page.within(first_row) do
page.within(second_row) do
click_on 'Import'
expect(page).to have_content 'Done'
expect(page).to have_content("#{group.full_path}/build/make")
expect(page).to have_content("#{group.full_path}/build/blueprint")
end
end
@ -47,7 +47,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js do
expect(page).to have_content 'The uploaded file is not a valid XML file.'
end
def first_row
page.all('table.import-jobs tbody tr')[0]
def second_row
page.all('table.import-jobs tbody tr')[1]
end
end

View File

@ -59,7 +59,8 @@ describe 'Pipeline', :js do
describe 'GET /:project/pipelines/:id' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
@ -329,6 +330,32 @@ describe 'Pipeline', :js do
end
end
context 'deleting pipeline' do
context 'when user can not delete' do
before do
visit_pipeline
end
it { expect(page).not_to have_button('Delete') }
end
context 'when deleting' do
before do
group.add_owner(user)
visit_pipeline
click_button 'Delete'
click_button 'Delete pipeline'
end
it 'redirects to pipeline overview page', :sidekiq_might_not_need_inline do
expect(page).to have_content('The pipeline has been deleted')
expect(current_path).to eq(project_pipelines_path(project))
end
end
end
context 'when pipeline ref does not exist in repository anymore' do
let(:pipeline) do
create(:ci_empty_pipeline, project: project,

View File

@ -64,19 +64,6 @@ describe PipelinesFinder do
end
end
context 'when project has child pipelines' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
let!(:pipeline_source) do
create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
end
it 'filters out child pipelines and show only the parents' do
is_expected.to eq([parent_pipeline])
end
end
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }

View File

@ -1,9 +1,8 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
const localVue = createLocalVue();
const mockData = [
{
id: 1,
@ -30,7 +29,6 @@ function factory(projects = mockData) {
mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
vm = shallowMount(ProjectFormGroup, {
localVue,
propsData: {
namespacePath: 'gitlab-org',
projectPath: 'gitlab-org/gitlab-ce',
@ -49,7 +47,7 @@ describe('Confidential merge request project form group component', () => {
it('renders fork dropdown', () => {
factory();
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot();
});
});
@ -57,7 +55,7 @@ describe('Confidential merge request project form group component', () => {
it('sets selected project as first fork', () => {
factory();
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.vm.selectedProject).toEqual({
id: 1,
name: 'root / gitlab-ce',
@ -70,7 +68,7 @@ describe('Confidential merge request project form group component', () => {
it('renders empty state when response is empty', () => {
factory([]);
return localVue.nextTick(() => {
return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot();
});
});

View File

@ -1,11 +1,10 @@
import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
import ContributorsCharts from '~/contributors/components/contributors.vue';
const localVue = createLocalVue();
let wrapper;
let mock;
let store;
@ -52,7 +51,7 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', () => {
wrapper.vm.$store.state.loading = true;
return localVue.nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
});
@ -60,7 +59,7 @@ describe('Contributors charts', () => {
it('should render charts when loading completed and there is chart data', () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
return localVue.nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();

View File

@ -1,8 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions';
const localVue = createLocalVue();
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
describe('DiffGutterAvatars', () => {
@ -14,7 +13,6 @@ describe('DiffGutterAvatars', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(DiffGutterAvatars, {
localVue,
propsData: {
...props,
},

View File

@ -1,7 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import EditButton from '~/diffs/components/edit_button.vue';
const localVue = createLocalVue();
const editPath = 'test-path';
describe('EditButton', () => {
@ -9,7 +8,6 @@ describe('EditButton', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(EditButton, {
localVue,
propsData: { ...props },
sync: false,
attachToDocument: true,

View File

@ -1,7 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
const localVue = createLocalVue();
const propsData = {
total: '10',
visible: 5,
@ -14,7 +13,6 @@ describe('HiddenFilesWarning', () => {
const createComponent = () => {
wrapper = shallowMount(HiddenFilesWarning, {
localVue,
sync: false,
propsData,
});

View File

@ -13,7 +13,7 @@ describe('Diff no changes empty state', () => {
const store = createStore();
extendStore(store);
vm = shallowMount(localVue.extend(NoChanges), {
vm = shallowMount(NoChanges, {
localVue,
store,
propsData: {

View File

@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => {
},
});
wrapper = shallowMount(localVue.extend(IdeStatusList), {
wrapper = shallowMount(IdeStatusList, {
localVue,
sync: false,
store,

View File

@ -1,6 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
@ -18,8 +18,6 @@ const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue';
const TEST_EMPTY_SVG_PATH = '/emptySvg';
const localVue = createLocalVue();
const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
.fill(0)
.map((_, i) => ({
@ -40,14 +38,13 @@ describe('Issuables list component', () => {
};
const factory = (props = { sortKey: 'priority' }) => {
wrapper = shallowMount(localVue.extend(IssuablesListApp), {
wrapper = shallowMount(IssuablesListApp, {
propsData: {
endpoint: TEST_ENDPOINT,
createIssuePath: TEST_CREATE_ISSUES_PATH,
emptySvgPath: TEST_EMPTY_SVG_PATH,
...props,
},
localVue,
sync: false,
attachToDocument: true,
});

View File

@ -1,9 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
const localVue = createLocalVue();
const plainZoomUrl = 'https://zoom.us/j/123456789';
describe('PinnedLinks', () => {
@ -19,8 +17,7 @@ describe('PinnedLinks', () => {
};
const createComponent = props => {
wrapper = shallowMount(localVue.extend(PinnedLinks), {
localVue,
wrapper = shallowMount(PinnedLinks, {
sync: false,
propsData: {
zoomMeetingUrl: null,

View File

@ -1,9 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ColumnChart from '~/monitoring/components/charts/column.vue';
const localVue = createLocalVue();
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
}));
@ -12,7 +10,7 @@ describe('Column component', () => {
let columnChart;
beforeEach(() => {
columnChart = shallowMount(localVue.extend(ColumnChart), {
columnChart = shallowMount(ColumnChart, {
propsData: {
graphData: {
metrics: [
@ -35,7 +33,6 @@ describe('Column component', () => {
containerWidth: 100,
},
sync: false,
localVue,
});
});

View File

@ -1,19 +1,16 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
const localVue = createLocalVue();
describe('Empty Chart component', () => {
let emptyChart;
const graphTitle = 'Memory Usage';
beforeEach(() => {
emptyChart = shallowMount(localVue.extend(EmptyChart), {
emptyChart = shallowMount(EmptyChart, {
propsData: {
graphTitle,
},
sync: false,
localVue,
});
});

View File

@ -1,19 +1,16 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { graphDataPrometheusQuery } from '../../mock_data';
const localVue = createLocalVue();
describe('Single Stat Chart component', () => {
let singleStatChart;
beforeEach(() => {
singleStatChart = shallowMount(localVue.extend(SingleStatChart), {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
graphData: graphDataPrometheusQuery,
},
sync: false,
localVue,
});
});

View File

@ -1,4 +1,4 @@
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
@ -15,12 +15,10 @@ describe('operation settings external dashboard component', () => {
const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
const localVue = createLocalVue();
const mountComponent = (shallow = true) => {
const config = [
ExternalDashboard,
{
localVue,
store: store({
operationsSettingsEndpoint,
externalDashboardUrl,

View File

@ -1,4 +1,3 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import registry from '~/registry/list/components/app.vue';
@ -35,12 +34,8 @@ describe('Registry List', () => {
};
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests.
// See https://github.com/vuejs/vue-test-utils/issues/532.
Vue.config.silent = true;
wrapper = mount(registry, {
attachToDocument: true,
sync: false,
propsData,
computed: {
repos() {
@ -52,7 +47,6 @@ describe('Registry List', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
@ -138,7 +132,7 @@ describe('Registry List', () => {
wrapper = mount(registry, {
propsData: {
...propsData,
endpoint: null,
endpoint: '',
isGroupPage,
},
methods,
@ -146,7 +140,7 @@ describe('Registry List', () => {
});
it('call the right vuex setters', () => {
expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null);
expect(methods.setMainEndpoint).toHaveBeenLastCalledWith('');
expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
});

View File

@ -1,4 +1,3 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
@ -28,14 +27,10 @@ describe('collapsible registry container', () => {
store,
localVue,
attachToDocument: true,
sync: false,
});
beforeEach(() => {
createFlash.mockClear();
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
store = new Vuex.Store({
state: {
isDeleteDisabled: false,
@ -51,7 +46,6 @@ describe('collapsible registry container', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
@ -72,25 +66,23 @@ describe('collapsible registry container', () => {
expectIsClosed();
});
it('should be open when user clicks on closed repo', done => {
it('should be open when user clicks on closed repo', () => {
const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const container = findContainerImageTags();
expect(container.exists()).toBe(true);
expect(wrapper.vm.fetchList).toHaveBeenCalled();
done();
});
});
it('should be closed when the user clicks on an opened repo', done => {
it('should be closed when the user clicks on an opened repo', () => {
const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
toggleRepos.at(0).trigger('click');
Vue.nextTick(() => {
wrapper.vm.$nextTick(() => {
expectIsClosed();
done();
});
});
});

View File

@ -1,4 +1,3 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
@ -29,13 +28,9 @@ describe('table registry', () => {
const bulkDeletePath = 'path';
const mountWithStore = config =>
mount(tableRegistry, { ...config, store, localVue, attachToDocument: true, sync: false });
mount(tableRegistry, { ...config, store, localVue, attachToDocument: true });
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
store = new Vuex.Store({
state: {
isDeleteDisabled: false,
@ -52,7 +47,6 @@ describe('table registry', () => {
});
afterEach(() => {
Vue.config.silent = false;
wrapper.destroy();
});
@ -82,53 +76,53 @@ describe('table registry', () => {
});
describe('multi select', () => {
it('selecting a row should enable delete button', done => {
it('selecting a row should enable delete button', () => {
const deleteBtn = findDeleteButton();
const checkboxes = findSelectCheckboxes();
expect(deleteBtn.attributes('disabled')).toBe('disabled');
checkboxes.at(0).trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
done();
});
});
it('selecting all checkbox should select all rows and enable delete button', done => {
it('selecting all checkbox should select all rows and enable delete button', () => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const checkboxes = findSelectCheckboxes();
const checked = checkboxes.filter(w => w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
it('deselecting select all checkbox should deselect all rows and disable delete button', () => {
const checkboxes = findSelectCheckboxes();
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm.$nextTick().then(() => {
const checked = checkboxes.filter(w => !w.element.checked);
expect(checked.length).toBe(checkboxes.length);
done();
});
});
it('should delete multiple items when multiple items are selected', done => {
it('should delete multiple items when multiple items are selected', () => {
const multiDeleteItems = jest.fn().mockResolvedValue();
wrapper.setMethods({ multiDeleteItems });
Vue.nextTick(() => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
Vue.nextTick(() => {
return wrapper.vm
.$nextTick()
.then(() => {
const selectAll = findSelectAllCheckbox();
selectAll.trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
const deleteBtn = findDeleteButton();
expect(wrapper.vm.selectedItems).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
@ -140,9 +134,7 @@ describe('table registry', () => {
path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag],
});
done();
});
});
});
it('should show an error message if bulkDeletePath is not set', () => {

View File

@ -1,4 +1,4 @@
import { mount, createLocalVue } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
@ -10,10 +10,7 @@ describe('Evidence Block', () => {
let wrapper;
const factory = (options = {}) => {
const localVue = createLocalVue();
wrapper = mount(localVue.extend(EvidenceBlock), {
localVue,
wrapper = mount(EvidenceBlock, {
...options,
});
};

View File

@ -1,20 +1,18 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import environmentRowComponent from '~/serverless/components/environment_row.vue';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
import { translate } from '~/serverless/utils';
const createComponent = (localVue, env, envName) =>
shallowMount(environmentRowComponent, { localVue, propsData: { env, envName }, sync: false }).vm;
const createComponent = (env, envName) =>
shallowMount(environmentRowComponent, { propsData: { env, envName }, sync: false }).vm;
describe('environment row component', () => {
describe('default global cluster case', () => {
let localVue;
let vm;
beforeEach(() => {
localVue = createLocalVue();
vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
vm = createComponent(translate(mockServerlessFunctions.functions)['*'], '*');
});
afterEach(() => vm.$destroy());
@ -44,15 +42,9 @@ describe('environment row component', () => {
describe('default named cluster case', () => {
let vm;
let localVue;
beforeEach(() => {
localVue = createLocalVue();
vm = createComponent(
localVue,
translate(mockServerlessFunctionsDiffEnv.functions).test,
'test',
);
vm = createComponent(translate(mockServerlessFunctionsDiffEnv.functions).test, 'test');
});
afterEach(() => vm.$destroy());

View File

@ -34,6 +34,7 @@ describe('Pipeline details header', () => {
avatar_url: 'link',
},
retry_path: 'path',
delete_path: 'path',
},
isLoading: false,
};
@ -55,12 +56,22 @@ describe('Pipeline details header', () => {
});
describe('action buttons', () => {
it('should call postAction when button action is clicked', () => {
it('should call postAction when retry button action is clicked', done => {
eventHub.$on('headerPostAction', action => {
expect(action.path).toEqual('path');
done();
});
vm.$el.querySelector('button').click();
vm.$el.querySelector('.js-retry-button').click();
});
it('should fire modal event when delete button action is clicked', done => {
vm.$root.$on('bv::modal::show', action => {
expect(action.componentId).toEqual('pipeline-delete-modal');
done();
});
vm.$el.querySelector('.js-btn-delete-pipeline').click();
});
});
});

View File

@ -31,17 +31,9 @@ describe('Header CI Component', () => {
{
label: 'Retry',
path: 'path',
type: 'button',
cssClass: 'btn',
isLoading: false,
},
{
label: 'Go',
path: 'path',
type: 'link',
cssClass: 'link',
isLoading: false,
},
],
hasSidebarButton: true,
};
@ -77,11 +69,10 @@ describe('Header CI Component', () => {
});
it('should render provided actions', () => {
expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
expect(vm.$el.querySelector('.link').tagName).toEqual('A');
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
const btn = vm.$el.querySelector('.btn');
expect(btn.tagName).toEqual('BUTTON');
expect(btn.textContent.trim()).toEqual(props.actions[0].label);
});
it('should show loading icon', done => {

View File

@ -15,42 +15,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false)
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
end
context 'when bridge job has downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when bridge job does not have downstream yaml' do
before do
allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
end
it 'returns the next available source' do
subject.perform!
expect(pipeline.config_source).to eq 'auto_devops_source'
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
expect(command.config_content).to eq(template.content)
end
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
@ -171,23 +135,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
context 'when bridge job is passed in as parameter' do
let(:ci_config_path) { nil }
let(:bridge) { create(:ci_bridge) }
before do
command.bridge = bridge
allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
end
it 'returns the content already available in command' do
subject.perform!
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
end
end
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do

View File

@ -201,8 +201,6 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines

View File

@ -2716,114 +2716,4 @@ describe Ci::Pipeline, :mailer do
end
end
end
describe '#parent_pipeline' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline is triggered by a pipeline from the same project' do
let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: project,
pipeline: pipeline,
project: project)
end
it 'returns the parent pipeline' do
expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
end
it 'is child' do
expect(pipeline).to be_child
end
end
context 'when pipeline is triggered by a pipeline from another project' do
let(:upstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: upstream_pipeline,
source_project: upstream_pipeline.project,
pipeline: pipeline,
project: project)
end
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
context 'when pipeline is not triggered by a pipeline' do
it 'returns nil' do
expect(pipeline.parent_pipeline).to be_nil
end
it 'is not child' do
expect(pipeline).not_to be_child
end
end
end
describe '#child_pipelines' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline triggered other pipelines on same project' do
let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: pipeline.project)
end
it 'returns the child pipelines' do
expect(pipeline.child_pipelines).to eq [downstream_pipeline]
end
it 'is parent' do
expect(pipeline).to be_parent
end
end
context 'when pipeline triggered other pipelines on another project' do
let(:downstream_pipeline) { create(:ci_pipeline) }
before do
create(:ci_sources_pipeline,
source_pipeline: pipeline,
source_project: pipeline.project,
pipeline: downstream_pipeline,
project: downstream_pipeline.project)
end
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
context 'when pipeline did not trigger any pipelines' do
it 'returns empty array' do
expect(pipeline.child_pipelines).to be_empty
end
it 'is not parent' do
expect(pipeline).not_to be_parent
end
end
end
end

View File

@ -123,6 +123,26 @@ describe PipelineEntity do
end
end
context 'delete path' do
context 'user has ability to delete pipeline' do
let(:project) { create(:project, namespace: user.namespace) }
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'contains delete path' do
expect(subject[:delete_path]).to be_present
end
end
context 'user does not have ability to delete pipeline' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'does not contain delete path' do
expect(subject).not_to have_key(:delete_path)
end
end
end
context 'when pipeline ref is empty' do
let(:pipeline) { create(:ci_empty_pipeline) }

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:service) { described_class.new(project, user, { ref: ref }) }
context 'custom config content' do
let(:bridge) do
double(:bridge, yaml_for_downstream: <<~YML
rspec:
script: rspec
custom:
script: custom
YML
)
end
subject { service.execute(:push, bridge: bridge) }
it 'creates a pipeline using the content passed in as param' do
expect(subject).to be_persisted
expect(subject.builds.map(&:name)).to eq %w[rspec custom]
expect(subject.config_source).to eq 'bridge_source'
end
end
end