Commit e3e30055 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent a821bd6a
...@@ -483,6 +483,16 @@ export const historyPushState = newUrl => { ...@@ -483,6 +483,16 @@ export const historyPushState = newUrl => {
window.history.pushState({}, document.title, 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. * Returns true for a String value of "true" and false otherwise.
* This is the opposite of Boolean(...).toString(). * This is the opposite of Boolean(...).toString().
......
import Vue from 'vue'; 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 PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
import { parseBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate); Vue.use(Translate);
Vue.use(GlToast);
document.addEventListener( document.addEventListener(
'DOMContentLoaded', 'DOMContentLoaded',
...@@ -21,6 +29,11 @@ document.addEventListener( ...@@ -21,6 +29,11 @@ document.addEventListener(
}, },
created() { created() {
this.dataset = document.querySelector(this.$options.el).dataset; this.dataset = document.querySelector(this.$options.el).dataset;
if (doesHashExistInUrl('delete_success')) {
this.$toast.show(__('The pipeline has been deleted'));
historyReplaceState(buildUrlWithCurrentLocation());
}
}, },
render(createElement) { render(createElement) {
return createElement('pipelines-component', { return createElement('pipelines-component', {
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { __ } from '~/locale'; import { __ } from '~/locale';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
export default { export default {
name: 'PipelineHeaderSection', name: 'PipelineHeaderSection',
components: { components: {
ciHeader, ciHeader,
GlLoadingIcon, GlLoadingIcon,
GlModal,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -33,6 +36,11 @@ export default { ...@@ -33,6 +36,11 @@ export default {
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length; 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: { watch: {
...@@ -42,6 +50,13 @@ export default { ...@@ -42,6 +50,13 @@ export default {
}, },
methods: { methods: {
onActionClicked(action) {
if (action.modal) {
this.$root.$emit('bv::show::modal', action.modal);
} else {
this.postAction(action);
}
},
postAction(action) { postAction(action) {
const index = this.actions.indexOf(action); const index = this.actions.indexOf(action);
...@@ -49,6 +64,13 @@ export default { ...@@ -49,6 +64,13 @@ export default {
eventHub.$emit('headerPostAction', action); 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() { getActions() {
const actions = []; const actions = [];
...@@ -58,7 +80,6 @@ export default { ...@@ -58,7 +80,6 @@ export default {
label: __('Retry'), label: __('Retry'),
path: this.pipeline.retry_path, path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary', cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false, isLoading: false,
}); });
} }
...@@ -68,7 +89,16 @@ export default { ...@@ -68,7 +89,16 @@ export default {
label: __('Cancel running'), label: __('Cancel running'),
path: this.pipeline.cancel_path, path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger', 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, isLoading: false,
}); });
} }
...@@ -76,6 +106,7 @@ export default { ...@@ -76,6 +106,7 @@ export default {
return actions; return actions;
}, },
}, },
DELETE_MODAL_ID,
}; };
</script> </script>
<template> <template>
...@@ -88,8 +119,21 @@ export default { ...@@ -88,8 +119,21 @@ export default {
:user="pipeline.user" :user="pipeline.user"
:actions="actions" :actions="actions"
item-name="Pipeline" item-name="Pipeline"
@actionClicked="postAction" @actionClicked="onActionClicked"
/> />
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" /> <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> </div>
</template> </template>
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import Flash from '~/flash'; import Flash from '~/flash';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator'; import PipelinesMediator from './pipeline_details_mediator';
...@@ -62,9 +63,11 @@ export default () => { ...@@ -62,9 +63,11 @@ export default () => {
}, },
created() { created() {
eventHub.$on('headerPostAction', this.postAction); eventHub.$on('headerPostAction', this.postAction);
eventHub.$on('headerDeleteAction', this.deleteAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction); eventHub.$off('headerPostAction', this.postAction);
eventHub.$off('headerDeleteAction', this.deleteAction);
}, },
methods: { methods: {
postAction(action) { postAction(action) {
...@@ -73,6 +76,13 @@ export default () => { ...@@ -73,6 +76,13 @@ export default () => {
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .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) { render(createElement) {
return createElement('pipeline-header', { return createElement('pipeline-header', {
......
...@@ -35,7 +35,7 @@ export default class pipelinesMediator { ...@@ -35,7 +35,7 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.poll.restart(); this.poll.restart();
} else { } else {
this.poll.stop(); this.stopPipelinePoll();
} }
}); });
} }
...@@ -51,7 +51,7 @@ export default class pipelinesMediator { ...@@ -51,7 +51,7 @@ export default class pipelinesMediator {
} }
refreshPipeline() { refreshPipeline() {
this.poll.stop(); this.stopPipelinePoll();
return this.service return this.service
.getPipeline() .getPipeline()
...@@ -64,6 +64,10 @@ export default class pipelinesMediator { ...@@ -64,6 +64,10 @@ export default class pipelinesMediator {
); );
} }
stopPipelinePoll() {
this.poll.stop();
}
/** /**
* Backend expects paramets in the following format: `expanded[]=id&expanded[]=id` * Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
*/ */
......
...@@ -9,6 +9,11 @@ export default class PipelineService { ...@@ -9,6 +9,11 @@ export default class PipelineService {
return axios.get(this.pipeline, { params }); 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 // eslint-disable-next-line class-methods-use-this
postAction(endpoint) { postAction(endpoint) {
return axios.post(`${endpoint}.json`); return axios.post(`${endpoint}.json`);
......
...@@ -117,28 +117,7 @@ export default { ...@@ -117,28 +117,7 @@ export default {
<section v-if="actions.length" class="header-action-buttons"> <section v-if="actions.length" class="header-action-buttons">
<template v-for="(action, i) in actions"> <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 <loading-button
v-else-if="action.type === 'button'"
:key="i" :key="i"
:loading="action.isLoading" :loading="action.isLoading"
:disabled="action.isLoading" :disabled="action.isLoading"
......
...@@ -80,6 +80,12 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -80,6 +80,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
end end
def destroy
::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
redirect_to project_pipelines_path(project), status: :see_other
end
def builds def builds
render_show render_show
end end
......
...@@ -17,7 +17,7 @@ class PipelinesFinder ...@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none return Ci::Pipeline.none
end end
items = pipelines.no_child items = pipelines
items = by_scope(items) items = by_scope(items)
items = by_status(items) items = by_status(items)
items = by_ref(items) items = by_ref(items)
......
...@@ -54,10 +54,6 @@ module Ci ...@@ -54,10 +54,6 @@ module Ci
def to_partial_path def to_partial_path
'projects/generic_commit_statuses/generic_commit_status' 'projects/generic_commit_statuses/generic_commit_status'
end end
def yaml_for_downstream
nil
end
end end
end end
......
...@@ -61,9 +61,7 @@ module Ci ...@@ -61,9 +61,7 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData' has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline 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 :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 :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
...@@ -215,7 +213,6 @@ module Ci ...@@ -215,7 +213,6 @@ module Ci
end end
scope :internal, -> { where(source: internal_sources) } 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 :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) } scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) } scope :for_sha, -> (sha) { where(sha: sha) }
...@@ -511,6 +508,10 @@ module Ci ...@@ -511,6 +508,10 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process) builds.skipped.after_stage(stage_idx).find_each(&:process)
end end
def child?
false
end
def latest? def latest?
return false unless git_ref && commit.present? return false unless git_ref && commit.present?
...@@ -693,24 +694,6 @@ module Ci ...@@ -693,24 +694,6 @@ module Ci
all_merge_requests.order(id: :desc) all_merge_requests.order(id: :desc)
end 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) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -23,11 +23,10 @@ module Ci ...@@ -23,11 +23,10 @@ module Ci
schedule: 4, schedule: 4,
api: 5, api: 5,
external: 6, external: 6,
cross_project_pipeline: 7, pipeline: 7,
chat: 8, chat: 8,
merge_request_event: 10, merge_request_event: 10,
external_pull_request_event: 11, external_pull_request_event: 11
parent_pipeline: 12
} }
end end
...@@ -39,8 +38,7 @@ module Ci ...@@ -39,8 +38,7 @@ module Ci
repository_source: 1, repository_source: 1,
auto_devops_source: 2, auto_devops_source: 2,
remote_source: 4, remote_source: 4,
external_project_source: 5, external_project_source: 5
bridge_source: 6
} }
end end
......
...@@ -18,8 +18,6 @@ module Ci ...@@ -18,8 +18,6 @@ module Ci
validates :source_project, presence: true validates :source_project, presence: true
validates :source_job, presence: true validates :source_job, presence: true
validates :source_pipeline, presence: true validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do expose :flags do
expose :latest?, as: :latest expose :latest?, as: :latest
end end
......
...@@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity ...@@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline) cancel_project_pipeline_path(pipeline.project, pipeline)
end 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| expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline|
pipeline.failed_builds pipeline.failed_builds
end end
...@@ -95,6 +99,10 @@ class PipelineEntity < Grape::Entity ...@@ -95,6 +99,10 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? pipeline.cancelable?
end end
def can_delete?
can?(request.current_user, :destroy_pipeline, pipeline)
end
def has_presentable_merge_request? def has_presentable_merge_request?
pipeline.triggered_by_merge_request? && pipeline.triggered_by_merge_request? &&
can?(request.current_user, :read_merge_request, pipeline.merge_request) can?(request.current_user, :read_merge_request, pipeline.merge_request)
......
...@@ -41,7 +41,6 @@ class PipelineSerializer < BaseSerializer ...@@ -41,7 +41,6 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations def preloaded_relations
[ [
:latest_statuses_ordered_by_stage, :latest_statuses_ordered_by_stage,
:project,
:stages, :stages,
{ {
failed_builds: %i(project metadata) failed_builds: %i(project metadata)
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists # 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 @pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new( command = Gitlab::Ci::Pipeline::Chain::Command.new(
...@@ -46,7 +46,6 @@ module Ci ...@@ -46,7 +46,6 @@ module Ci
current_user: current_user, current_user: current_user,
push_options: params[:push_options] || {}, push_options: params[:push_options] || {},
chat_data: params[:chat_data], chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options)) **extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
...@@ -105,14 +104,14 @@ module Ci ...@@ -105,14 +104,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true) if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled .alive_or_scheduled
.with_only_interruptible_builds .with_only_interruptible_builds
else else
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending .created_or_pending
end end
......
...@@ -44,7 +44,7 @@ module Ci ...@@ -44,7 +44,7 @@ module Ci
return error("400 Job has to be running", 400) unless job.running? return error("400 Job has to be running", 400) unless job.running?
pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]) 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 = job.sourced_pipelines.build(
source_pipeline: job.pipeline, source_pipeline: job.pipeline,
source_project: job.project, source_project: job.project,
......
---
title: Fix Delete Selected button being active after uploading designs after a deletion
merge_request: 22516
author:
type: fixed
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 20930
author:
type: added
---
title: Add pipeline deletion button to pipeline details page
merge_request: 21365
author: Fabio Huser
type: added
...@@ -343,7 +343,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -343,7 +343,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :merge_requests draw :merge_requests
end end
resources :pipelines, only: [:index, :new, :create, :show] do resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
collection do collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update] resource :pipelines_settings, path: 'settings', only: [:show, :update]
get :charts get :charts
......
...@@ -305,12 +305,14 @@ For example, the query string ...@@ -305,12 +305,14 @@ For example, the query string
### Accessing pipelines ### Accessing pipelines
You can find the current and historical pipeline runs under your project's 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 **CI/CD > Pipelines** page. You can also access pipelines for a merge request by navigating
that pipeline. to its **Pipelines** tab.
![Pipelines index page](img/pipelines_index.png) ![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 ### Accessing individual jobs
...@@ -410,6 +412,20 @@ This functionality is only available: ...@@ -410,6 +412,20 @@ This functionality is only available:
- For users with at least Developer access. - For users with at least Developer access.
- If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs). - 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 ## Most Recent Pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/50499) in GitLab 12.3. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/50499) in GitLab 12.3.
......
...@@ -98,6 +98,10 @@ This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-foss/m ...@@ -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> - 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 ## Resources
- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html) - [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html)
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request, :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted, :ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options, :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: # These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds :config_content, :config_processor, :stage_seeds
) do ) do
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers include Chain::Helpers
SOURCES = [ 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::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject, Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote, Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
].freeze ].freeze
LEGACY_SOURCES = [ 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::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze ].freeze
......
...@@ -6,15 +6,20 @@ module Gitlab ...@@ -6,15 +6,20 @@ module Gitlab
module Chain module Chain
module Config module Config
class Content class Content
class Bridge < Source class Runtime < Source
def content def content
return unless @command.bridge @command.config_content
@command.bridge.yaml_for_downstream
end end
def source def source
:bridge_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
......
...@@ -1631,6 +1631,9 @@ msgstr "" ...@@ -1631,6 +1631,9 @@ msgstr ""
msgid "An error occurred while deleting the comment" msgid "An error occurred while deleting the comment"
msgstr "" msgstr ""
msgid "An error occurred while deleting the pipeline."
msgstr ""
msgid "An error occurred while detecting host keys" msgid "An error occurred while detecting host keys"
msgstr "" msgstr ""
...@@ -2092,6 +2095,9 @@ msgstr "" ...@@ -2092,6 +2095,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?" msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "" 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?" msgid "Are you sure you want to erase this build?"
msgstr "" msgstr ""
...@@ -3266,6 +3272,9 @@ msgstr "" ...@@ -3266,6 +3272,9 @@ msgstr ""
msgid "Checkout" msgid "Checkout"
msgstr "" msgstr ""
msgid "Checkout|%{selectedPlanText} plan"
msgstr ""
msgid "Checkout|1. Your profile" msgid "Checkout|1. Your profile"
msgstr "" msgstr ""
...@@ -3278,9 +3287,36 @@ msgstr "" ...@@ -3278,9 +3287,36 @@ msgstr ""
msgid "Checkout|Checkout" msgid "Checkout|Checkout"
msgstr "" msgstr ""
msgid "Checkout|Continue to billing"
msgstr ""
msgid "Checkout|Edit" msgid "Checkout|Edit"
msgstr "" 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" msgid "Cherry-pick this commit"
msgstr "" msgstr ""
...@@ -5735,6 +5771,9 @@ msgstr "" ...@@ -5735,6 +5771,9 @@ msgstr ""
msgid "Delete list" msgid "Delete list"
msgstr "" msgstr ""
msgid "Delete pipeline"
msgstr ""
msgid "Delete snippet" msgid "Delete snippet"
msgstr "" msgstr ""
...@@ -18098,6 +18137,9 @@ msgstr "" ...@@ -18098,6 +18137,9 @@ msgstr ""
msgid "The phase of the development lifecycle." msgid "The phase of the development lifecycle."
msgstr "" 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." 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 "" msgstr ""
......
...@@ -740,4 +740,51 @@ describe Projects::PipelinesController do ...@@ -740,4 +740,51 @@ describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
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 end
...@@ -24,17 +24,17 @@ describe 'Import multiple repositories by uploading a manifest file', :js do ...@@ -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') expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end 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 visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml')) attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories' click_on 'List available repositories'
page.within(first_row) do page.within(second_row) do
click_on 'Import' click_on 'Import'
expect(page).to have_content 'Done' 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
end end
...@@ -47,7 +47,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js do ...@@ -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.' expect(page).to have_content 'The uploaded file is not a valid XML file.'
end end
def first_row def second_row
page.all('table.import-jobs tbody tr')[0] page.all('table.import-jobs tbody tr')[1]
end end
end end
...@@ -59,7 +59,8 @@ describe 'Pipeline', :js do ...@@ -59,7 +59,8 @@ describe 'Pipeline', :js do
describe 'GET /:project/pipelines/:id' do describe 'GET /:project/pipelines/:id' do
include_context 'pipeline builds' 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) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) } subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
...@@ -329,6 +330,32 @@ describe 'Pipeline', :js do ...@@ -329,6 +330,32 @@ describe 'Pipeline', :js do
end end
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 context 'when pipeline ref does not exist in repository anymore' do
let(:pipeline) do let(:pipeline) do
create(:ci_empty_pipeline, project: project, create(:ci_empty_pipeline, project: project,
......
...@@ -64,19 +64,6 @@ describe PipelinesFinder do ...@@ -64,19 +64,6 @@ describe PipelinesFinder do
end end
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| HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do context "when status is #{target}" do
let(:params) { { status: target } } let(:params) { { status: target } }
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue'; import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
const localVue = createLocalVue();
const mockData = [ const mockData = [
{ {
id: 1, id: 1,
...@@ -30,7 +29,6 @@ function factory(projects = mockData) { ...@@ -30,7 +29,6 @@ function factory(projects = mockData) {
mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects); mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
vm = shallowMount(ProjectFormGroup, { vm = shallowMount(ProjectFormGroup, {
localVue,
propsData: { propsData: {
namespacePath: 'gitlab-org', namespacePath: 'gitlab-org',
projectPath: 'gitlab-org/gitlab-ce', projectPath: 'gitlab-org/gitlab-ce',
...@@ -49,7 +47,7 @@ describe('Confidential merge request project form group component', () => { ...@@ -49,7 +47,7 @@ describe('Confidential merge request project form group component', () => {
it('renders fork dropdown', () => { it('renders fork dropdown', () => {
factory(); factory();
return localVue.nextTick(() => { return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
}); });
...@@ -57,7 +55,7 @@ describe('Confidential merge request project form group component', () => { ...@@ -57,7 +55,7 @@ describe('Confidential merge request project form group component', () => {
it('sets selected project as first fork', () => { it('sets selected project as first fork', () => {
factory(); factory();
return localVue.nextTick(() => { return vm.vm.$nextTick(() => {
expect(vm.vm.selectedProject).toEqual({ expect(vm.vm.selectedProject).toEqual({
id: 1, id: 1,
name: 'root / gitlab-ce', name: 'root / gitlab-ce',
...@@ -70,7 +68,7 @@ describe('Confidential merge request project form group component', () => { ...@@ -70,7 +68,7 @@ describe('Confidential merge request project form group component', () => {
it('renders empty state when response is empty', () => { it('renders empty state when response is empty', () => {
factory([]); factory([]);
return localVue.nextTick(() => { return vm.vm.$nextTick(() => {
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { createStore } from '~/contributors/stores'; import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import ContributorsCharts from '~/contributors/components/contributors.vue'; import ContributorsCharts from '~/contributors/components/contributors.vue';
const localVue = createLocalVue();
let wrapper; let wrapper;
let mock; let mock;
let store; let store;
...@@ -52,7 +51,7 @@ describe('Contributors charts', () => { ...@@ -52,7 +51,7 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', () => { it('should display loader whiled loading data', () => {
wrapper.vm.$store.state.loading = true; wrapper.vm.$store.state.loading = true;
return localVue.nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(true); expect(wrapper.find('.contributors-loader').exists()).toBe(true);
}); });
}); });
...@@ -60,7 +59,7 @@ describe('Contributors charts', () => { ...@@ -60,7 +59,7 @@ describe('Contributors charts', () => {
it('should render charts when loading completed and there is chart data', () => { it('should render charts when loading completed and there is chart data', () => {
wrapper.vm.$store.state.loading = false; wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData; 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-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true); expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions'; import discussionsMockData from '../mock_data/diff_discussions';
const localVue = createLocalVue();
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
describe('DiffGutterAvatars', () => { describe('DiffGutterAvatars', () => {
...@@ -14,7 +13,6 @@ describe('DiffGutterAvatars', () => { ...@@ -14,7 +13,6 @@ describe('DiffGutterAvatars', () => {
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(DiffGutterAvatars, { wrapper = shallowMount(DiffGutterAvatars, {
localVue,
propsData: { propsData: {
...props, ...props,
}, },
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import EditButton from '~/diffs/components/edit_button.vue'; import EditButton from '~/diffs/components/edit_button.vue';
const localVue = createLocalVue();
const editPath = 'test-path'; const editPath = 'test-path';
describe('EditButton', () => { describe('EditButton', () => {
...@@ -9,7 +8,6 @@ describe('EditButton', () => { ...@@ -9,7 +8,6 @@ describe('EditButton', () => {
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(EditButton, { wrapper = shallowMount(EditButton, {
localVue,
propsData: { ...props }, propsData: { ...props },
sync: false, sync: false,
attachToDocument: true, attachToDocument: true,
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue'; import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
const localVue = createLocalVue();
const propsData = { const propsData = {
total: '10', total: '10',
visible: 5, visible: 5,
...@@ -14,7 +13,6 @@ describe('HiddenFilesWarning', () => { ...@@ -14,7 +13,6 @@ describe('HiddenFilesWarning', () => {
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(HiddenFilesWarning, { wrapper = shallowMount(HiddenFilesWarning, {
localVue,
sync: false, sync: false,
propsData, propsData,
}); });
......
...@@ -13,7 +13,7 @@ describe('Diff no changes empty state', () => { ...@@ -13,7 +13,7 @@ describe('Diff no changes empty state', () => {
const store = createStore(); const store = createStore();
extendStore(store); extendStore(store);
vm = shallowMount(localVue.extend(NoChanges), { vm = shallowMount(NoChanges, {
localVue, localVue,
store, store,
propsData: { propsData: {
......
...@@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => { ...@@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => {
}, },
}); });
wrapper = shallowMount(localVue.extend(IdeStatusList), { wrapper = shallowMount(IdeStatusList, {
localVue, localVue,
sync: false, sync: false,
store, store,
......
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; 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 { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -18,8 +18,6 @@ const TEST_ENDPOINT = '/issues'; ...@@ -18,8 +18,6 @@ const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue'; const TEST_CREATE_ISSUES_PATH = '/createIssue';
const TEST_EMPTY_SVG_PATH = '/emptySvg'; const TEST_EMPTY_SVG_PATH = '/emptySvg';
const localVue = createLocalVue();
const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL) const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
.fill(0) .fill(0)
.map((_, i) => ({ .map((_, i) => ({
...@@ -40,14 +38,13 @@ describe('Issuables list component', () => { ...@@ -40,14 +38,13 @@ describe('Issuables list component', () => {
}; };
const factory = (props = { sortKey: 'priority' }) => { const factory = (props = { sortKey: 'priority' }) => {
wrapper = shallowMount(localVue.extend(IssuablesListApp), { wrapper = shallowMount(IssuablesListApp, {
propsData: { propsData: {
endpoint: TEST_ENDPOINT, endpoint: TEST_ENDPOINT,
createIssuePath: TEST_CREATE_ISSUES_PATH, createIssuePath: TEST_CREATE_ISSUES_PATH,
emptySvgPath: TEST_EMPTY_SVG_PATH, emptySvgPath: TEST_EMPTY_SVG_PATH,
...props, ...props,
}, },
localVue,
sync: false, sync: false,
attachToDocument: true, attachToDocument: true,
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue'; import PinnedLinks from '~/issue_show/components/pinned_links.vue';
const localVue = createLocalVue();
const plainZoomUrl = 'https://zoom.us/j/123456789'; const plainZoomUrl = 'https://zoom.us/j/123456789';
describe('PinnedLinks', () => { describe('PinnedLinks', () => {
...@@ -19,8 +17,7 @@ describe('PinnedLinks', () => { ...@@ -19,8 +17,7 @@ describe('PinnedLinks', () => {
}; };
const createComponent = props => { const createComponent = props => {
wrapper = shallowMount(localVue.extend(PinnedLinks), { wrapper = shallowMount(PinnedLinks, {
localVue,
sync: false, sync: false,
propsData: { propsData: {
zoomMeetingUrl: null, zoomMeetingUrl: null,
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ColumnChart from '~/monitoring/components/charts/column.vue'; import ColumnChart from '~/monitoring/components/charts/column.vue';
const localVue = createLocalVue();
jest.mock('~/lib/utils/icon_utils', () => ({ jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'), getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
})); }));
...@@ -12,7 +10,7 @@ describe('Column component', () => { ...@@ -12,7 +10,7 @@ describe('Column component', () => {
let columnChart; let columnChart;
beforeEach(() => { beforeEach(() => {
columnChart = shallowMount(localVue.extend(ColumnChart), { columnChart = shallowMount(ColumnChart, {
propsData: { propsData: {
graphData: { graphData: {
metrics: [ metrics: [
...@@ -35,7 +33,6 @@ describe('Column component', () => { ...@@ -35,7 +33,6 @@ describe('Column component', () => {
containerWidth: 100, containerWidth: 100,
}, },
sync: false, sync: false,
localVue,
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
const localVue = createLocalVue();
describe('Empty Chart component', () => { describe('Empty Chart component', () => {
let emptyChart; let emptyChart;
const graphTitle = 'Memory Usage'; const graphTitle = 'Memory Usage';
beforeEach(() => { beforeEach(() => {
emptyChart = shallowMount(localVue.extend(EmptyChart), { emptyChart = shallowMount(EmptyChart, {
propsData: { propsData: {
graphTitle, graphTitle,
}, },
sync: false, sync: false,
localVue,
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue'; import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { graphDataPrometheusQuery } from '../../mock_data'; import { graphDataPrometheusQuery } from '../../mock_data';
const localVue = createLocalVue();
describe('Single Stat Chart component', () => { describe('Single Stat Chart component', () => {
let singleStatChart; let singleStatChart;
beforeEach(() => { beforeEach(() => {
singleStatChart = shallowMount(localVue.extend(SingleStatChart), { singleStatChart = shallowMount(SingleStatChart, {
propsData: { propsData: {
graphData: graphDataPrometheusQuery, graphData: graphDataPrometheusQuery,
}, },
sync: false, sync: false,
localVue,
}); });
}); });
......
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui'; import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue'; import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
...@@ -15,12 +15,10 @@ describe('operation settings external dashboard component', () => { ...@@ -15,12 +15,10 @@ describe('operation settings external dashboard component', () => {
const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`; const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`; const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`; const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
const localVue = createLocalVue();
const mountComponent = (shallow = true) => { const mountComponent = (shallow = true) => {
const config = [ const config = [
ExternalDashboard, ExternalDashboard,
{ {
localVue,
store: store({ store: store({
operationsSettingsEndpoint, operationsSettingsEndpoint,
externalDashboardUrl, externalDashboardUrl,
......
import Vue from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import registry from '~/registry/list/components/app.vue'; import registry from '~/registry/list/components/app.vue';
...@@ -35,12 +34,8 @@ describe('Registry List', () => { ...@@ -35,12 +34,8 @@ describe('Registry List', () => {
}; };
beforeEach(() => { 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, { wrapper = mount(registry, {
attachToDocument: true, attachToDocument: true,
sync: false,
propsData, propsData,
computed: { computed: {
repos() { repos() {
...@@ -52,7 +47,6 @@ describe('Registry List', () => { ...@@ -52,7 +47,6 @@ describe('Registry List', () => {
}); });
afterEach(() => { afterEach(() => {
Vue.config.silent = false;
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -138,7 +132,7 @@ describe('Registry List', () => { ...@@ -138,7 +132,7 @@ describe('Registry List', () => {
wrapper = mount(registry, { wrapper = mount(registry, {
propsData: { propsData: {
...propsData, ...propsData,
endpoint: null, endpoint: '',
isGroupPage, isGroupPage,
}, },
methods, methods,
...@@ -146,7 +140,7 @@ describe('Registry List', () => { ...@@ -146,7 +140,7 @@ describe('Registry List', () => {
}); });
it('call the right vuex setters', () => { it('call the right vuex setters', () => {
expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null); expect(methods.setMainEndpoint).toHaveBeenLastCalledWith('');
expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true); expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
}); });
......
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -28,14 +27,10 @@ describe('collapsible registry container', () => { ...@@ -28,14 +27,10 @@ describe('collapsible registry container', () => {
store, store,
localVue, localVue,
attachToDocument: true, attachToDocument: true,
sync: false,
}); });
beforeEach(() => { beforeEach(() => {
createFlash.mockClear(); 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({ store = new Vuex.Store({
state: { state: {
isDeleteDisabled: false, isDeleteDisabled: false,
...@@ -51,7 +46,6 @@ describe('collapsible registry container', () => { ...@@ -51,7 +46,6 @@ describe('collapsible registry container', () => {
}); });
afterEach(() => { afterEach(() => {
Vue.config.silent = false;
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -72,25 +66,23 @@ describe('collapsible registry container', () => { ...@@ -72,25 +66,23 @@ describe('collapsible registry container', () => {
expectIsClosed(); 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(); const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click'); toggleRepos.at(0).trigger('click');
Vue.nextTick(() => { return wrapper.vm.$nextTick().then(() => {
const container = findContainerImageTags(); const container = findContainerImageTags();
expect(container.exists()).toBe(true); expect(container.exists()).toBe(true);
expect(wrapper.vm.fetchList).toHaveBeenCalled(); 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(); const toggleRepos = findToggleRepos();
toggleRepos.at(0).trigger('click'); toggleRepos.at(0).trigger('click');
Vue.nextTick(() => { return wrapper.vm.$nextTick().then(() => {
toggleRepos.at(0).trigger('click'); toggleRepos.at(0).trigger('click');
Vue.nextTick(() => { wrapper.vm.$nextTick(() => {
expectIsClosed(); expectIsClosed();
done();
}); });
}); });
}); });
......
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -29,13 +28,9 @@ describe('table registry', () => { ...@@ -29,13 +28,9 @@ describe('table registry', () => {
const bulkDeletePath = 'path'; const bulkDeletePath = 'path';
const mountWithStore = config => const mountWithStore = config =>
mount(tableRegistry, { ...config, store, localVue, attachToDocument: true, sync: false }); mount(tableRegistry, { ...config, store, localVue, attachToDocument: true });
beforeEach(() => { 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({ store = new Vuex.Store({
state: { state: {
isDeleteDisabled: false, isDeleteDisabled: false,
...@@ -52,7 +47,6 @@ describe('table registry', () => { ...@@ -52,7 +47,6 @@ describe('table registry', () => {
}); });
afterEach(() => { afterEach(() => {
Vue.config.silent = false;
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -82,53 +76,53 @@ describe('table registry', () => { ...@@ -82,53 +76,53 @@ describe('table registry', () => {
}); });
describe('multi select', () => { describe('multi select', () => {
it('selecting a row should enable delete button', done => { it('selecting a row should enable delete button', () => {
const deleteBtn = findDeleteButton(); const deleteBtn = findDeleteButton();
const checkboxes = findSelectCheckboxes(); const checkboxes = findSelectCheckboxes();
expect(deleteBtn.attributes('disabled')).toBe('disabled'); expect(deleteBtn.attributes('disabled')).toBe('disabled');
checkboxes.at(0).trigger('click'); checkboxes.at(0).trigger('click');
Vue.nextTick(() => { return wrapper.vm.$nextTick().then(() => {
expect(deleteBtn.attributes('disabled')).toEqual(undefined); 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(); const selectAll = findSelectAllCheckbox();
selectAll.trigger('click'); selectAll.trigger('click');
Vue.nextTick(() => { return wrapper.vm.$nextTick().then(() => {
const checkboxes = findSelectCheckboxes(); const checkboxes = findSelectCheckboxes();
const checked = checkboxes.filter(w => w.element.checked); const checked = checkboxes.filter(w => w.element.checked);
expect(checked.length).toBe(checkboxes.length); 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 checkboxes = findSelectCheckboxes();
const selectAll = findSelectAllCheckbox(); const selectAll = findSelectAllCheckbox();
selectAll.trigger('click'); selectAll.trigger('click');
selectAll.trigger('click'); selectAll.trigger('click');
Vue.nextTick(() => { return wrapper.vm.$nextTick().then(() => {
const checked = checkboxes.filter(w => !w.element.checked); const checked = checkboxes.filter(w => !w.element.checked);
expect(checked.length).toBe(checkboxes.length); 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(); const multiDeleteItems = jest.fn().mockResolvedValue();
wrapper.setMethods({ multiDeleteItems }); wrapper.setMethods({ multiDeleteItems });
Vue.nextTick(() => { return wrapper.vm
const selectAll = findSelectAllCheckbox(); .$nextTick()
selectAll.trigger('click'); .then(() => {
const selectAll = findSelectAllCheckbox();
Vue.nextTick(() => { selectAll.trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
const deleteBtn = findDeleteButton(); const deleteBtn = findDeleteButton();
expect(wrapper.vm.selectedItems).toEqual([0, 1]); expect(wrapper.vm.selectedItems).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined); expect(deleteBtn.attributes('disabled')).toEqual(undefined);
...@@ -140,9 +134,7 @@ describe('table registry', () => { ...@@ -140,9 +134,7 @@ describe('table registry', () => {
path: bulkDeletePath, path: bulkDeletePath,
items: [firstImage.tag, secondImage.tag], items: [firstImage.tag, secondImage.tag],
}); });
done();
}); });
});
}); });
it('should show an error message if bulkDeletePath is not set', () => { it('should show an error message if bulkDeletePath is not set', () => {
......
import { mount, createLocalVue } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -10,10 +10,7 @@ describe('Evidence Block', () => { ...@@ -10,10 +10,7 @@ describe('Evidence Block', () => {
let wrapper; let wrapper;
const factory = (options = {}) => { const factory = (options = {}) => {
const localVue = createLocalVue(); wrapper = mount(EvidenceBlock, {
wrapper = mount(localVue.extend(EvidenceBlock), {
localVue,
...options, ...options,
}); });
}; };
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import environmentRowComponent from '~/serverless/components/environment_row.vue'; import environmentRowComponent from '~/serverless/components/environment_row.vue';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
import { translate } from '~/serverless/utils'; import { translate } from '~/serverless/utils';
const createComponent = (localVue, env, envName) => const createComponent = (env, envName) =>
shallowMount(environmentRowComponent, { localVue, propsData: { env, envName }, sync: false }).vm; shallowMount(environmentRowComponent, { propsData: { env, envName }, sync: false }).vm;
describe('environment row component', () => { describe('environment row component', () => {
describe('default global cluster case', () => { describe('default global cluster case', () => {
let localVue;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
localVue = createLocalVue(); vm = createComponent(translate(mockServerlessFunctions.functions)['*'], '*');
vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
}); });
afterEach(() => vm.$destroy()); afterEach(() => vm.$destroy());
...@@ -44,15 +42,9 @@ describe('environment row component', () => { ...@@ -44,15 +42,9 @@ describe('environment row component', () => {
describe('default named cluster case', () => { describe('default named cluster case', () => {
let vm; let vm;
let localVue;
beforeEach(() => { beforeEach(() => {
localVue = createLocalVue(); vm = createComponent(translate(mockServerlessFunctionsDiffEnv.functions).test, 'test');
vm = createComponent(
localVue,
translate(mockServerlessFunctionsDiffEnv.functions).test,
'test',
);
}); });
afterEach(() => vm.$destroy()); afterEach(() => vm.$destroy());
......
...@@ -34,6 +34,7 @@ describe('Pipeline details header', () => { ...@@ -34,6 +34,7 @@ describe('Pipeline details header', () => {
avatar_url: 'link', avatar_url: 'link',
}, },
retry_path: 'path', retry_path: 'path',
delete_path: 'path',
}, },
isLoading: false, isLoading: false,
}; };
...@@ -55,12 +56,22 @@ describe('Pipeline details header', () => { ...@@ -55,12 +56,22 @@ describe('Pipeline details header', () => {
}); });
describe('action buttons', () => { 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 => { eventHub.$on('headerPostAction', action => {
expect(action.path).toEqual('path'); 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();
}); });
}); });
}); });
...@@ -31,17 +31,9 @@ describe('Header CI Component', () => { ...@@ -31,17 +31,9 @@ describe('Header CI Component', () => {
{ {
label: 'Retry', label: 'Retry',
path: 'path', path: 'path',
type: 'button',
cssClass: 'btn', cssClass: 'btn',
isLoading: false, isLoading: false,
}, },
{
label: 'Go',
path: 'path',
type: 'link',
cssClass: 'link',
isLoading: false,
},
], ],
hasSidebarButton: true, hasSidebarButton: true,
}; };
...@@ -77,11 +69,10 @@ describe('Header CI Component', () => { ...@@ -77,11 +69,10 @@ describe('Header CI Component', () => {
}); });
it('should render provided actions', () => { it('should render provided actions', () => {
expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON'); const btn = vm.$el.querySelector('.btn');
expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
expect(vm.$el.querySelector('.link').tagName).toEqual('A'); expect(btn.tagName).toEqual('BUTTON');
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); expect(btn.textContent.trim()).toEqual(props.actions[0].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
}); });
it('should show loading icon', done => { it('should show loading icon', done => {
......
...@@ -15,42 +15,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -15,42 +15,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false) stub_feature_flags(ci_root_config_content: false)
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
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
...@@ -171,23 +135,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -171,23 +135,6 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end end
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do let(:config_content_result) do
......
...@@ -201,8 +201,6 @@ ci_pipelines: ...@@ -201,8 +201,6 @@ ci_pipelines:
- sourced_pipelines - sourced_pipelines
- triggered_by_pipeline - triggered_by_pipeline
- triggered_pipelines - triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges - downstream_bridges
- job_artifacts - job_artifacts
- vulnerabilities_occurrence_pipelines - vulnerabilities_occurrence_pipelines
......
...@@ -2716,114 +2716,4 @@ describe Ci::Pipeline, :mailer do ...@@ -2716,114 +2716,4 @@ describe Ci::Pipeline, :mailer do
end end
end 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 end
...@@ -123,6 +123,26 @@ describe PipelineEntity do ...@@ -123,6 +123,26 @@ describe PipelineEntity do
end end
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 context 'when pipeline ref is empty' do
let(:pipeline) { create(:ci_empty_pipeline) } let(:pipeline) { create(:ci_empty_pipeline) }
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment