summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-21 09:10:08 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-21 09:10:08 +0000
commit202268ad93e9a1556f5700326be5ec89bd641a97 (patch)
treedb1faf51de8859ccd812e4a6ed61284cc3bb6a9a
parent3c63ea4631f629f83c7d35e65963ffc1acf83161 (diff)
downloadgitlab-ce-202268ad93e9a1556f5700326be5ec89bd641a97.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss1
-rw-r--r--app/controllers/concerns/issuable_links.rb41
-rw-r--r--app/controllers/projects/issue_links_controller.rb48
-rw-r--r--app/controllers/projects/pipelines_controller.rb22
-rw-r--r--app/models/audit_event.rb9
-rw-r--r--app/models/clusters/applications/prometheus.rb2
-rw-r--r--app/models/security_event.rb4
-rw-r--r--app/services/audit_event_service.rb4
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml6
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml12
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/sherlock/transactions/index.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--changelogs/unreleased/213372-update-prometheus-version.yml5
-rw-r--r--changelogs/unreleased/225926-replace-fa-tag-and-sidebar-icons.yml5
-rw-r--r--changelogs/unreleased/225950-replace-fa-trash-and-fa-trash-o-icons-with-gitlab-svg-remove-icon.yml5
-rw-r--r--changelogs/unreleased/231344-json-result-for-project-pipeline-create.yml5
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities/issue_link.rb11
-rw-r--r--lib/api/entities/related_issue.rb10
-rw-r--r--lib/api/helpers/services_helpers.rb12
-rw-r--r--lib/api/issue_links.rb82
-rw-r--r--qa/qa/page/project/job/show.rb12
-rwxr-xr-xscripts/trigger-build18
-rw-r--r--spec/controllers/projects/issue_links_controller_spec.rb71
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb56
-rw-r--r--spec/controllers/sessions_controller_spec.rb12
-rw-r--r--spec/factories/audit_events.rb2
-rw-r--r--spec/features/boards/boards_spec.rb60
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issue_link.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issue_links.json9
-rw-r--r--spec/frontend/sidebar/assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb661
-rw-r--r--spec/models/audit_event_partitioned_spec.rb13
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb6
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb2
-rw-r--r--spec/requests/api/issue_links_spec.rb207
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb156
-rw-r--r--spec/services/audit_event_service_spec.rb6
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb2
51 files changed, 1162 insertions, 486 deletions
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 6183779acd4..396bf3a824e 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -71,6 +71,7 @@ export default {
:href="artifact.browse_path"
class="btn btn-sm btn-default"
data-testid="browse-artifacts"
+ data-qa-selector="browse_artifacts_button"
>{{ s__('Job|Browse') }}</gl-link
>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 7375855f899..eabd4d88d52 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedAssignee from './collapsed_assignee.vue';
@@ -12,6 +12,7 @@ export default {
},
components: {
CollapsedAssignee,
+ GlIcon,
},
props: {
users: {
@@ -102,7 +103,7 @@ export default {
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-assignee
v-for="user in collapsedUsers"
:key="user.id"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index d2904f4157c..e7dbc47aea1 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@@ -10,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlIcon,
GlLoadingIcon,
},
props: {
@@ -94,7 +95,7 @@ export default {
data-boundary="viewport"
@click="onClickCollapsedIcon"
>
- <i class="fa fa-users" aria-hidden="true"> </i>
+ <gl-icon name="users" />
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index cc24fedceed..0ed5a050fe4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
containerClass: {
type: String,
@@ -47,7 +51,7 @@ export default {
data-boundary="viewport"
@click="click"
>
- <i v-if="showIcon" class="fa fa-calendar" aria-hidden="true"> </i>
+ <gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 05446903286..c2ebf78d541 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -6,6 +7,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ GlIcon,
+ },
props: {
labels: {
type: Array,
@@ -49,7 +53,7 @@ export default {
data-boundary="viewport"
@click="handleClick"
>
- <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
+ <gl-icon name="labels" />
<span>{{ labels.length }}</span>
</div>
</template>
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2f28361f62c..1c7a6535c12 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -234,8 +234,8 @@
.title {
color: $gl-text-color;
- margin-bottom: $gl-padding-8;
- line-height: 1;
+ margin-bottom: $gl-padding-4;
+ line-height: $gl-line-height-20;
.avatar {
margin-left: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index fc3b786b365..cc8ec891755 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -193,7 +193,6 @@
.icon-container {
display: inline-block;
- width: 10px;
&.commit-icon {
width: 15px;
diff --git a/app/controllers/concerns/issuable_links.rb b/app/controllers/concerns/issuable_links.rb
new file mode 100644
index 00000000000..2bdb190f1d5
--- /dev/null
+++ b/app/controllers/concerns/issuable_links.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module IssuableLinks
+ def index
+ render json: issuables
+ end
+
+ def create
+ result = create_service.execute
+
+ render json: { message: result[:message], issuables: issuables }, status: result[:http_status]
+ end
+
+ def destroy
+ result = destroy_service.execute
+
+ render json: { issuables: issuables }, status: result[:http_status]
+ end
+
+ private
+
+ def issuables
+ list_service.execute
+ end
+
+ def list_service
+ raise NotImplementedError
+ end
+
+ def create_params
+ params.permit(issuable_references: [])
+ end
+
+ def create_service
+ raise NotImplementedError
+ end
+
+ def destroy_service
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb
new file mode 100644
index 00000000000..2f7489373ed
--- /dev/null
+++ b/app/controllers/projects/issue_links_controller.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Projects
+ class IssueLinksController < Projects::ApplicationController
+ include IssuableLinks
+
+ before_action :authorize_admin_issue_link!, only: [:create, :destroy]
+ before_action :authorize_issue_link_association!, only: :destroy
+
+ private
+
+ def authorize_admin_issue_link!
+ render_403 unless can?(current_user, :admin_issue_link, @project)
+ end
+
+ def authorize_issue_link_association!
+ render_404 if link.target != issue && link.source != issue
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def issue
+ @issue ||=
+ IssuesFinder.new(current_user, project_id: @project.id)
+ .find_by!(iid: params[:issue_id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def list_service
+ IssueLinks::ListService.new(issue, current_user)
+ end
+
+ def create_service
+ IssueLinks::CreateService.new(issue, current_user, create_params)
+ end
+
+ def destroy_service
+ IssueLinks::DestroyService.new(link, current_user)
+ end
+
+ def link
+ @link ||= IssueLink.find(params[:id])
+ end
+
+ def create_params
+ params.permit(:link_type, issuable_references: [])
+ end
+ end
+end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index bfe23eb1035..10fc5d399b7 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -63,10 +63,24 @@ class Projects::PipelinesController < Projects::ApplicationController
.new(project, current_user, create_params)
.execute(:web, ignore_skip_ci: true, save_on_errors: false)
- if @pipeline.created_successfully?
- redirect_to project_pipeline_path(project, @pipeline)
- else
- render 'new', status: :bad_request
+ respond_to do |format|
+ format.html do
+ if @pipeline.created_successfully?
+ redirect_to project_pipeline_path(project, @pipeline)
+ else
+ render 'new', status: :bad_request
+ end
+ end
+ format.json do
+ if @pipeline.created_successfully?
+ render json: PipelineSerializer
+ .new(project: project, current_user: current_user)
+ .represent(@pipeline),
+ status: :created
+ else
+ render json: @pipeline.errors, status: :bad_request
+ end
+ end
end
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index e7cfa30a892..c496e152995 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -7,6 +7,7 @@ class AuditEvent < ApplicationRecord
PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details].freeze
+ ignore_column :type, remove_with: '13.6', remove_after: '2020-11-22'
ignore_column :updated_at, remove_with: '13.4', remove_after: '2020-09-22'
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -29,6 +30,14 @@ class AuditEvent < ApplicationRecord
# https://gitlab.com/groups/gitlab-org/-/epics/2765
after_validation :parallel_persist
+ # Note: After loading records, do not attempt to type cast objects it finds.
+ # We are in the process of deprecating STI (i.e. SecurityEvent) out of AuditEvent.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/216845
+ def self.inheritance_column
+ :_type_disabled
+ end
+
def self.order_by(method)
case method.to_s
when 'created_asc'
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 216bbbc1c5a..c1558b256f8 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -5,7 +5,7 @@ module Clusters
class Prometheus < ApplicationRecord
include PrometheusAdapter
- VERSION = '9.5.2'
+ VERSION = '10.4.1'
self.table_name = 'clusters_applications_prometheus'
diff --git a/app/models/security_event.rb b/app/models/security_event.rb
deleted file mode 100644
index 3fe4cc99c9b..00000000000
--- a/app/models/security_event.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-class SecurityEvent < AuditEvent
-end
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index fef733a7d09..c5d28317685 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -37,7 +37,7 @@ class AuditEventService
# Writes event to a file and creates an event record in DB
#
- # @return [SecurityEvent] persited if saves and non-persisted if fails
+ # @return [AuditEvent] persited if saves and non-persisted if fails
def security_event
log_security_event_to_file
log_security_event_to_database
@@ -81,7 +81,7 @@ class AuditEventService
def log_security_event_to_database
return if Gitlab::Database.read_only?
- SecurityEvent.create(base_payload.merge(details: @details))
+ AuditEvent.create(base_payload.merge(details: @details))
end
end
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index e05f121c5d9..f1abafa4149 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -21,7 +21,7 @@
= s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
= link_to profile_gpg_key_path(key), data: { confirm: _('Are you sure? Removing this GPG key does not affect already signed commits.') }, method: :delete, class: "btn btn-danger gl-ml-3" do
%span.sr-only= _('Remove')
- = icon('trash')
+ = sprite_icon('remove')
= link_to revoke_profile_gpg_key_path(key), data: { confirm: _('Are you sure? All commits that were signed with this GPG key will be unverified.') }, method: :put, class: "btn btn-danger gl-ml-3" do
%span.sr-only= _('Revoke')
= _('Revoke')
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index 1a9ce8d0508..c68cc19f6c1 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
- = link_to path_to_directory, class: 'str-truncated' do
+ = link_to path_to_directory, class: 'str-truncated', data: { qa_selector: 'directory_name_link', qa_directory_name: directory.name } do
%span= directory.name
%td
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index c8a6168edfc..dba9b20fcff 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -2,8 +2,8 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row.allow-wrap
.row-main-content
- = icon('tag')
- = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name gl-ml-2'
+ = sprite_icon('tag')
+ = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
- if protected_tag?(@project, tag)
%span.badge.badge-success.gl-ml-2
@@ -39,4 +39,4 @@
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= sprite_icon("pencil")
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip gl-ml-3 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
- = icon("trash-o")
+ = sprite_icon("remove")
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index ff973e2922f..25a560da5c6 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -10,7 +10,7 @@
.nav-text
.title
%span.item-title.ref-name{ data: { qa_selector: 'tag_name_content' } }
- = icon('tag')
+ = sprite_icon('tag')
= @tag.name
- if protected_tag?(@project, @tag)
%span.badge.badge-success
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7cdebdb646d..994777e6d4a 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -40,7 +40,7 @@
= _('None')
.title.hide-collapsed
= _('Milestone')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
@@ -65,12 +65,12 @@
- if issuable_sidebar.has_key?(:due_date)
.block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
- = icon('calendar', 'aria-hidden': 'true')
+ = sprite_icon('calendar')
%span.js-due-date-sidebar-value
= issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.title.hide-collapsed
= _('Due date')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
@@ -101,12 +101,12 @@
- selected_labels = issuable_sidebar[:labels]
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
- = icon('tags', 'aria-hidden': 'true')
+ = sprite_icon('labels')
%span
= selected_labels.size
.title.hide-collapsed
= _('Labels')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_labels_link", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?), data: { qa_selector: 'labels_block' } }
@@ -175,7 +175,7 @@
= dropdown_footer add_content_class: true do
%button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
= _('Move')
- = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index cf239a5d04c..175713751ef 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
- .spinner.spinner-sm.align-bottom
+ = loading_icon(css_class: 'gl-vertical-align-text-bottom')
.selectbox.hide-collapsed
- if assignees.none?
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index 4d9df01ae31..1e16c88571e 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -6,7 +6,7 @@
= link_to(destroy_all_sherlock_transactions_path,
class: 'btn btn-danger',
method: :delete) do
- %i.fa.fa-trash
+ = sprite_icon('remove')
= t('sherlock.delete_all_transactions')
.oneline= t('sherlock.introduction')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index e1d1df9de1a..b85c3d862cd 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -30,7 +30,7 @@
- if current_user && current_user.admin?
= link_to [:admin, @user], class: link_classes + 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('users')
+ = sprite_icon('user')
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
.avatar-holder
diff --git a/changelogs/unreleased/213372-update-prometheus-version.yml b/changelogs/unreleased/213372-update-prometheus-version.yml
new file mode 100644
index 00000000000..835e2ab1360
--- /dev/null
+++ b/changelogs/unreleased/213372-update-prometheus-version.yml
@@ -0,0 +1,5 @@
+---
+title: Update Prometheus helm chart version to 10.4.1
+merge_request: 39681
+author:
+type: changed
diff --git a/changelogs/unreleased/225926-replace-fa-tag-and-sidebar-icons.yml b/changelogs/unreleased/225926-replace-fa-tag-and-sidebar-icons.yml
new file mode 100644
index 00000000000..832a5646471
--- /dev/null
+++ b/changelogs/unreleased/225926-replace-fa-tag-and-sidebar-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-tag(s) icons with GitLab SVG icons
+merge_request: 38979
+author:
+type: changed
diff --git a/changelogs/unreleased/225950-replace-fa-trash-and-fa-trash-o-icons-with-gitlab-svg-remove-icon.yml b/changelogs/unreleased/225950-replace-fa-trash-and-fa-trash-o-icons-with-gitlab-svg-remove-icon.yml
new file mode 100644
index 00000000000..d3886ab5c0a
--- /dev/null
+++ b/changelogs/unreleased/225950-replace-fa-trash-and-fa-trash-o-icons-with-gitlab-svg-remove-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace some fa-trash icons with GitLab SVG remove icon
+merge_request: 39991
+author:
+type: other
diff --git a/changelogs/unreleased/231344-json-result-for-project-pipeline-create.yml b/changelogs/unreleased/231344-json-result-for-project-pipeline-create.yml
new file mode 100644
index 00000000000..e3052763a37
--- /dev/null
+++ b/changelogs/unreleased/231344-json-result-for-project-pipeline-create.yml
@@ -0,0 +1,5 @@
+---
+title: Implement JSON response for project/pipelines create
+merge_request: 39839
+author:
+type: other
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 2be6792af5f..cf367438ff4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -167,6 +167,7 @@ module API
mount ::API::GroupVariables
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
+ mount ::API::IssueLinks
mount ::API::Issues
mount ::API::JobArtifacts
mount ::API::Jobs
diff --git a/lib/api/entities/issue_link.rb b/lib/api/entities/issue_link.rb
new file mode 100644
index 00000000000..8e24b046325
--- /dev/null
+++ b/lib/api/entities/issue_link.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssueLink < Grape::Entity
+ expose :source, as: :source_issue, using: ::API::Entities::IssueBasic
+ expose :target, as: :target_issue, using: ::API::Entities::IssueBasic
+ expose :link_type
+ end
+ end
+end
diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb
new file mode 100644
index 00000000000..491c606bd49
--- /dev/null
+++ b/lib/api/entities/related_issue.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class RelatedIssue < ::API::Entities::Issue
+ expose :issue_link_id
+ expose :issue_link_type, as: :link_type
+ end
+ end
+end
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index ff938358439..94d773367a8 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -631,12 +631,6 @@ module API
name: :issues_url,
type: String,
desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
}
],
'youtrack' => [
@@ -651,12 +645,6 @@ module API
name: :issues_url,
type: String,
desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
}
],
'slack' => [
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
new file mode 100644
index 00000000000..6cc5b344f47
--- /dev/null
+++ b/lib/api/issue_links.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module API
+ class IssueLinks < Grape::API::Instance
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get related issues' do
+ success Entities::RelatedIssue
+ end
+ get ':id/issues/:issue_iid/links' do
+ source_issue = find_project_issue(params[:issue_iid])
+ related_issues = source_issue.related_issues(current_user)
+
+ present related_issues,
+ with: Entities::RelatedIssue,
+ current_user: current_user,
+ project: user_project
+ end
+
+ desc 'Relate issues' do
+ success Entities::IssueLink
+ end
+ params do
+ requires :target_project_id, type: String, desc: 'The ID of the target project'
+ requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
+ optional :link_type, type: String, values: IssueLink.link_types.keys,
+ desc: 'The type of the relation'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ post ':id/issues/:issue_iid/links' do
+ source_issue = find_project_issue(params[:issue_iid])
+ target_issue = find_project_issue(declared_params[:target_issue_iid],
+ declared_params[:target_project_id])
+
+ create_params = { target_issuable: target_issue, link_type: declared_params[:link_type] }
+
+ result = ::IssueLinks::CreateService
+ .new(source_issue, current_user, create_params)
+ .execute
+
+ if result[:status] == :success
+ issue_link = IssueLink.find_by!(source: source_issue, target: target_issue)
+
+ present issue_link, with: Entities::IssueLink
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Remove issues relation' do
+ success Entities::IssueLink
+ end
+ params do
+ requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ end
+ delete ':id/issues/:issue_iid/links/:issue_link_id' do
+ issue_link = IssueLink.find(declared_params[:issue_link_id])
+
+ find_project_issue(params[:issue_iid])
+ find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s)
+
+ result = ::IssueLinks::DestroyService
+ .new(issue_link, current_user)
+ .execute
+
+ if result[:status] == :success
+ present issue_link, with: Entities::IssueLink
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 6243dc92b45..6a657b4ab39 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -19,6 +19,10 @@ module QA
element :retry_button
end
+ view 'app/assets/javascripts/jobs/components/artifacts_block.vue' do
+ element :browse_artifacts_button
+ end
+
def successful?(timeout: 60)
raise "Timed out waiting for the build trace to load" unless loaded?
raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout)
@@ -42,6 +46,14 @@ module QA
result
end
+ def has_browse_button?
+ has_element? :browse_artifacts_button
+ end
+
+ def click_browse_button
+ click_element :browse_artifacts_button
+ end
+
def retry!
click_element :retry_button
end
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 633e4dda808..8edf4bb57f7 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -321,8 +321,6 @@ module Trigger
INTERVAL = 60 # seconds
MAX_DURATION = 3600 * 3 # 3 hours
- attr_reader :project, :id
-
def self.unscoped_class_name
name.split('::').last
end
@@ -334,13 +332,11 @@ module Trigger
def initialize(project, id)
@project = project
@id = id
- @start = Time.now.to_i
+ @start_time = Time.now.to_i
end
def wait!
- loop do
- raise "#{self.class.unscoped_class_name} timed out after waiting for #{duration} minutes!" if timeout?
-
+ (MAX_DURATION / INTERVAL).times do
case status
when :created, :pending, :running
print "."
@@ -354,14 +350,12 @@ module Trigger
STDOUT.flush
end
- end
- def timeout?
- Time.now.to_i > (@start + MAX_DURATION)
+ raise "#{self.class.unscoped_class_name} timed out after waiting for #{duration} minutes!"
end
def duration
- (Time.now.to_i - @start) / 60
+ (Time.now.to_i - start_time) / 60
end
def status
@@ -372,6 +366,10 @@ module Trigger
# timeout anyway.
:running
end
+
+ private
+
+ attr_reader :project, :id, :start_time
end
Job = Class.new(Pipeline)
diff --git a/spec/controllers/projects/issue_links_controller_spec.rb b/spec/controllers/projects/issue_links_controller_spec.rb
new file mode 100644
index 00000000000..bce109b7c79
--- /dev/null
+++ b/spec/controllers/projects/issue_links_controller_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::IssueLinksController do
+ let_it_be(:namespace) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue1) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project) }
+
+ describe 'GET #index' do
+ let_it_be(:issue_link) { create(:issue_link, source: issue1, target: issue2, link_type: 'relates_to') }
+
+ def get_link(user, issue)
+ sign_in(user)
+
+ params = {
+ namespace_id: issue.project.namespace.to_param,
+ project_id: issue.project,
+ issue_id: issue.iid
+ }
+
+ get :index, params: params, as: :json
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns success response' do
+ get_link(user, issue1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ link = json_response.first
+ expect(link['id']).to eq(issue2.id)
+ expect(link['link_type']).to eq('relates_to')
+ end
+ end
+
+ describe 'POST #create' do
+ def create_link(user, issue, target)
+ sign_in(user)
+
+ post_params = {
+ namespace_id: issue.project.namespace.to_param,
+ project_id: issue.project,
+ issue_id: issue.iid,
+ issuable_references: [target.to_reference],
+ link_type: 'relates_to'
+ }
+
+ post :create, params: post_params, as: :json
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns success response' do
+ create_link(user, issue1, issue2)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ link = json_response['issuables'].first
+ expect(link['id']).to eq(issue2.id)
+ expect(link['link_type']).to eq('relates_to')
+ end
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index ef560f6426b..f295834d1b6 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -763,6 +763,62 @@ RSpec.describe Projects::PipelinesController do
end
end
+ describe 'POST create.json' do
+ let(:project) { create(:project, :public, :repository) }
+
+ subject do
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ pipeline: { ref: 'master' }
+ },
+ format: :json
+ end
+
+ before do
+ project.add_developer(user)
+ project.project_feature.update(builds_access_level: feature)
+ end
+
+ context 'with a valid .gitlab-ci.yml file' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump({
+ test: {
+ stage: 'test',
+ script: 'echo'
+ }
+ }))
+ end
+
+ it 'creates a pipeline' do
+ expect { subject }.to change { project.ci_pipelines.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['id']).to eq(project.ci_pipelines.last.id)
+ end
+ end
+
+ context 'with an invalid .gitlab-ci.yml file' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump({
+ test: {
+ stage: 'invalid',
+ script: 'echo'
+ }
+ }))
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['base']).to include(
+ 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
+ )
+ end
+ end
+ end
+
describe 'POST retry.json' do
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 257dcce0899..9ba64a4d207 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -130,8 +130,8 @@ RSpec.describe SessionsController do
end
it 'creates an audit log record' do
- expect { post(:create, params: { user: user_params }) }.to change { SecurityEvent.count }.by(1)
- expect(SecurityEvent.last.details[:with]).to eq('standard')
+ expect { post(:create, params: { user: user_params }) }.to change { AuditEvent.count }.by(1)
+ expect(AuditEvent.last.details[:with]).to eq('standard')
end
include_examples 'user login request with unique ip limit', 302 do
@@ -396,8 +396,8 @@ RSpec.describe SessionsController do
end
it "creates an audit log record" do
- expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
- expect(SecurityEvent.last.details[:with]).to eq("two-factor")
+ expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { AuditEvent.count }.by(1)
+ expect(AuditEvent.last.details[:with]).to eq("two-factor")
end
end
@@ -433,8 +433,8 @@ RSpec.describe SessionsController do
it "creates an audit log record" do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
- expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
- expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
+ expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { AuditEvent.count }.by(1)
+ expect(AuditEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
end
end
end
diff --git a/spec/factories/audit_events.rb b/spec/factories/audit_events.rb
index 38414400282..9a450797df7 100644
--- a/spec/factories/audit_events.rb
+++ b/spec/factories/audit_events.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :audit_event, class: 'SecurityEvent', aliases: [:user_audit_event] do
+ factory :audit_event, class: 'AuditEvent', aliases: [:user_audit_event] do
user
transient { target_user { create(:user) } }
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 8e2a9381aa0..e36378bd34e 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe 'Issue Boards', :js do
include DragTo
include MobileHelpers
- let(:group) { create(:group, :nested) }
- let(:project) { create(:project, :public, namespace: group) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let!(:user2) { create(:user) }
+ let_it_be(:group) { create(:group, :nested) }
+ let_it_be(:project) { create(:project, :public, namespace: group) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
before do
project.add_maintainer(user)
@@ -62,30 +62,30 @@ RSpec.describe 'Issue Boards', :js do
end
context 'with lists' do
- let(:milestone) { create(:milestone, project: project) }
-
- let(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
- let(:development) { create(:label, project: project, name: 'Development') }
- let(:testing) { create(:label, project: project, name: 'Testing') }
- let(:bug) { create(:label, project: project, name: 'Bug') }
- let!(:backlog) { create(:label, project: project, name: 'Backlog') }
- let!(:closed) { create(:label, project: project, name: 'Closed') }
- let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
- let!(:a_plus) { create(:label, project: project, name: 'A+') }
- let!(:list1) { create(:list, board: board, label: planning, position: 0) }
- let!(:list2) { create(:list, board: board, label: development, position: 1) }
-
- let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
- let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
- let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
- let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
- let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
- let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
- let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
- let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
- let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
- let!(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
+ let_it_be(:development) { create(:label, project: project, name: 'Development') }
+ let_it_be(:testing) { create(:label, project: project, name: 'Testing') }
+ let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
+ let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') }
+ let_it_be(:closed) { create(:label, project: project, name: 'Closed') }
+ let_it_be(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
+ let_it_be(:a_plus) { create(:label, project: project, name: 'A+') }
+ let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
+
+ let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
+ let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
+ let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
+ let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
+ let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
+ let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
+ let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
+ let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
+ let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
+ let_it_be(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
+ let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
before do
visit project_board_path(project, board)
@@ -636,7 +636,7 @@ RSpec.describe 'Issue Boards', :js do
end
context 'as guest user' do
- let(:user_guest) { create(:user) }
+ let_it_be(:user_guest) { create(:user) }
before do
project.add_guest(user_guest)
diff --git a/spec/fixtures/api/schemas/public_api/v4/issue_link.json b/spec/fixtures/api/schemas/public_api/v4/issue_link.json
new file mode 100644
index 00000000000..000e8485aca
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/issue_link.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties" : {
+ "source_issue": {
+ "allOf": [
+ { "$ref": "../../../../../../spec/fixtures/api/schemas/public_api/v4/issue.json" }
+ ]
+ },
+ "target_issue": {
+ "allOf": [
+ { "$ref": "../../../../../../spec/fixtures/api/schemas/public_api/v4/issue.json" }
+ ]
+ },
+ "link_type": {
+ "type": "string",
+ "enum": ["relates_to", "blocks", "is_blocked_by"]
+ }
+ },
+ "required" : [ "source_issue", "target_issue", "link_type" ]
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issue_links.json b/spec/fixtures/api/schemas/public_api/v4/issue_links.json
new file mode 100644
index 00000000000..d254615dd58
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/issue_links.json
@@ -0,0 +1,9 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties" : {
+ "$ref": "./issue_link.json"
+ }
+ }
+}
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 3418680f8ea..d1810ada97a 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
+import { GlIcon } from '@gitlab/ui';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import UsersMock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
@@ -29,10 +30,12 @@ describe('Assignee component', () => {
it('displays no assignee icon when collapsed', () => {
createWrapper();
const collapsedChildren = findCollapsedChildren();
+ const userIcon = collapsedChildren.at(0).find(GlIcon);
expect(collapsedChildren.length).toBe(1);
expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
- expect(collapsedChildren.at(0).classes()).toContain('fa', 'fa-user');
+ expect(userIcon.exists()).toBe(true);
+ expect(userIcon.props('name')).toBe('user');
});
it('displays only "None" when no users are assigned and the issue is read-only', () => {
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index a1e19c1dd8e..907d6144415 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
@@ -20,7 +21,7 @@ describe('CollapsedAssigneeList component', () => {
});
}
- const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
+ const findNoUsersIcon = () => wrapper.find(GlIcon);
const findAvatarCounter = () => wrapper.find('.avatar-counter');
const findAssignees = () => wrapper.findAll(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
@@ -38,6 +39,7 @@ describe('CollapsedAssigneeList component', () => {
it('has no users', () => {
expect(findNoUsersIcon().exists()).toBe(true);
+ expect(findNoUsersIcon().props('name')).toBe('user');
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index e09f0006359..7847e0ee71d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -87,7 +87,7 @@ describe('DropdownValueCollapsedComponent', () => {
});
it('renders tags icon element', () => {
- expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
});
it('renders labels count', () => {
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 1c81cc83cd1..99e6bdc6625 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -9,6 +9,12 @@ module Gitlab
subject { described_class.new(config, user: nil) }
+ shared_examples 'returns errors' do |error_message|
+ it 'raises exception when error encountered' do
+ expect { subject }.to raise_error(described_class::ValidationError, error_message)
+ end
+ end
+
describe '#build_attributes' do
subject { described_class.new(config, user: nil).build_attributes(:rspec) }
@@ -345,9 +351,7 @@ module Gitlab
EOYML
end
- it 'parses the workflow:rules configuration' do
- expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'workflow config contains unknown keys: variables')
- end
+ it_behaves_like 'returns errors', 'workflow config contains unknown keys: variables'
end
context 'with rules and variables' do
@@ -471,11 +475,11 @@ module Gitlab
it 'is propagated all the way up into the raised exception' do
expect { subject }.to raise_error do |error|
- expect(error).to be_a(described_class::ValidationError)
- expect(error.message).to eq('jobs:invalid:artifacts config should be a hash')
expect(error.warnings).to contain_exactly(/jobs:rspec may allow multiple pipelines to run/)
end
end
+
+ it_behaves_like 'returns errors', 'jobs:invalid:artifacts config should be a hash'
end
context 'when error is raised before composing the config' do
@@ -491,11 +495,11 @@ module Gitlab
it 'raises an exception with empty warnings array' do
expect { subject }.to raise_error do |error|
- expect(error).to be_a(described_class::ValidationError)
- expect(error.message).to eq('Local file `unknown/file.yml` does not have project!')
expect(error.warnings).to be_empty
end
end
+
+ it_behaves_like 'returns errors', 'Local file `unknown/file.yml` does not have project!'
end
context 'when error is raised after composing the config with warnings' do
@@ -585,65 +589,49 @@ module Gitlab
describe 'only / except policies validations' do
context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do
let(:only) { 1 }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only has to be either an array of conditions or a hash')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:only has to be either an array of conditions or a hash'
end
context 'when it is an array of integers' do
let(:only) { [1, 1] }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regexps'
end
context 'when it is invalid regex' do
let(:only) { ["/*invalid/"] }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:only config should be an array of strings or regexps'
end
end
context 'when `except` has an invalid value' do
let(:config) { { rspec: { script: "rspec", except: except } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do
let(:except) { 1 }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except has to be either an array of conditions or a hash')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:except has to be either an array of conditions or a hash'
end
context 'when it is an array of integers' do
let(:except) { [1, 1] }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regexps'
end
context 'when it is invalid regex' do
let(:except) { ["/*invalid/"] }
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
+ it_behaves_like 'returns errors', 'jobs:rspec:except config should be an array of strings or regexps'
end
end
end
@@ -1040,11 +1028,7 @@ module Gitlab
%w(VAR1 value1 VAR2 value2)
end
- it 'raises error' do
- expect { subject }
- .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- /jobs:rspec:variables config should be a hash of key value pairs/)
- end
+ it_behaves_like 'returns errors', /jobs:rspec:variables config should be a hash of key value pairs/
end
context 'when variables key defined but value not specified' do
@@ -1156,17 +1140,13 @@ module Gitlab
context "when an array is provided" do
let(:include_content) { ["/local.gitlab-ci.yml"] }
- it "returns a validation error" do
- expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /does not have project/)
- end
+ it_behaves_like 'returns errors', /does not have project/
end
context "when an array of wrong keyed object is provided" do
let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] }
- it "returns a validation error" do
- expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
- end
+ it_behaves_like 'returns errors', /needs to match exactly one accessor/
end
context "when an array of mixed typed objects is provided" do
@@ -1193,9 +1173,7 @@ module Gitlab
context "when the include type is incorrect" do
let(:include_content) { { name: "/local.gitlab-ci.yml" } }
- it "returns an invalid configuration error" do
- expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
- end
+ it_behaves_like 'returns errors', /needs to match exactly one accessor/
end
end
@@ -1216,12 +1194,7 @@ module Gitlab
end
context "when the included internal file is not present" do
- it "returns an error with missing file details" do
- expect { subject }.to raise_error(
- Gitlab::Ci::YamlProcessor::ValidationError,
- "Local file `#{include_content}` does not exist!"
- )
- end
+ it_behaves_like 'returns errors', "Local file `/local.gitlab-ci.yml` does not exist!"
end
end
end
@@ -1243,13 +1216,14 @@ module Gitlab
context 'delayed' do
context 'with start_in' do
- it 'creates one build and sets when:' do
- config = YAML.dump({
+ let(:config) do
+ YAML.dump({
rspec: { script: 'rspec', when: 'delayed', start_in: '1 hour' }
})
+ end
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
- builds = config_processor.stage_builds_attributes("test")
+ it 'creates one build and sets when:' do
+ builds = subject.stage_builds_attributes("test")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq('delayed')
@@ -1258,15 +1232,13 @@ module Gitlab
end
context 'without start_in' do
- it 'raises an error' do
- config = YAML.dump({
+ let(:config) do
+ YAML.dump({
rspec: { script: 'rspec', when: 'delayed' }
})
-
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(YamlProcessor::ValidationError, /start in should be a duration/)
end
+
+ it_behaves_like 'returns errors', /start in should be a duration/
end
end
end
@@ -1377,16 +1349,13 @@ module Gitlab
describe 'cache' do
context 'when cache definition has unknown keys' do
- it 'raises relevant validation error' do
- config = YAML.dump(
+ let(:config) do
+ YAML.dump(
{ cache: { untracked: true, invalid: 'key' },
rspec: { script: 'rspec' } })
-
- expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error(
- Gitlab::Ci::YamlProcessor::ValidationError,
- 'cache config contains unknown keys: invalid'
- )
end
+
+ it_behaves_like 'returns errors', 'cache config contains unknown keys: invalid'
end
it "returns cache when defined globally" do
@@ -1594,17 +1563,19 @@ module Gitlab
end
end
- it "gracefully handles errors in artifacts type" do
- config = <<~YAML
- test:
- script:
- - echo "Hello world"
- artifacts:
- - paths:
- - test/
- YAML
+ context 'when artifacts syntax is wrong' do
+ let(:config) do
+ <<~YAML
+ test:
+ script:
+ - echo "Hello world"
+ artifacts:
+ - paths:
+ - test/
+ YAML
+ end
- expect { described_class.new(config) }.to raise_error(described_class::ValidationError)
+ it_behaves_like 'returns errors', 'jobs:test:artifacts config should be a hash'
end
it 'populates a build options with complete artifacts configuration' do
@@ -1672,8 +1643,9 @@ module Gitlab
}
end
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.stage_builds_attributes('deploy') }
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ let(:builds) { subject.stage_builds_attributes('deploy') }
context 'when a production environment is specified' do
let(:environment) { 'production' }
@@ -1723,18 +1695,13 @@ module Gitlab
context 'is not a string' do
let(:environment) { 1 }
- it 'raises error' do
- expect { builds }.to raise_error(
- 'jobs:deploy_to_production:environment config should be a hash or a string')
- end
+ it_behaves_like 'returns errors', 'jobs:deploy_to_production:environment config should be a hash or a string'
end
context 'is not a valid string' do
let(:environment) { 'production:staging' }
- it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
- end
+ it_behaves_like 'returns errors', "jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}"
end
context 'when on_stop is specified' do
@@ -1753,33 +1720,25 @@ module Gitlab
context 'without matching job' do
let(:close_review) { nil }
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
- end
+ it_behaves_like 'returns errors', 'review job: on_stop job close_review is not defined'
end
context 'with close job without environment' do
let(:close_review) { { stage: 'deploy', script: 'test' } }
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
- end
+ it_behaves_like 'returns errors', 'review job: on_stop job close_review does not have environment defined'
end
context 'with close job for different environment' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
- end
+ it_behaves_like 'returns errors', 'review job: on_stop job close_review have different environment name'
end
context 'with close job without stop action' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
- end
+ it_behaves_like 'returns errors', 'review job: on_stop job close_review needs to have action stop defined'
end
end
end
@@ -1794,8 +1753,9 @@ module Gitlab
}
end
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.stage_builds_attributes('deploy') }
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ let(:builds) { subject.stage_builds_attributes('deploy') }
context 'when no timeout was provided' do
it 'does not include job_timeout' do
@@ -1809,9 +1769,7 @@ module Gitlab
config[:deploy_to_production][:timeout] = 'not-a-number'
end
- it 'raises an error for invalid number' do
- expect { builds }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:deploy_to_production:timeout config should be a duration')
- end
+ it_behaves_like 'returns errors', 'jobs:deploy_to_production:timeout config should be a duration'
end
context 'when some valid timeout was provided' do
@@ -1860,13 +1818,13 @@ module Gitlab
context 'undefined dependency' do
let(:dependencies) { ['undefined'] }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
+ it_behaves_like 'returns errors', 'test1 job: undefined dependency: undefined'
end
context 'dependencies to deploy' do
let(:dependencies) { ['deploy'] }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in prior stages'
end
context 'when a job depends on another job that references a not-yet defined stage' do
@@ -1891,7 +1849,7 @@ module Gitlab
}
end
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /is not defined in prior stages/) }
+ it_behaves_like 'returns errors', /is not defined in prior stages/
end
end
@@ -2053,20 +2011,20 @@ module Gitlab
context 'undefined need' do
let(:needs) { ['undefined'] }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined need: undefined') }
+ it_behaves_like 'returns errors', 'test1 job: undefined need: undefined'
end
context 'needs to deploy' do
let(:needs) { ['deploy'] }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: need deploy is not defined in prior stages') }
+ it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in prior stages'
end
context 'needs and dependencies that are mismatching' do
let(:needs) { %w(build1) }
let(:dependencies) { %w(build2) }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build2 should be part of needs') }
+ it_behaves_like 'returns errors', 'jobs:test1 dependencies the build2 should be part of needs'
end
context 'needs with a Hash type and dependencies with a string type that are mismatching' do
@@ -2079,28 +2037,28 @@ module Gitlab
let(:dependencies) { %w(build3) }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build3 should be part of needs') }
+ it_behaves_like 'returns errors', 'jobs:test1 dependencies the build3 should be part of needs'
end
context 'needs with an array type and dependency with a string type' do
let(:needs) { %w(build1) }
let(:dependencies) { 'deploy' }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies should be an array of strings') }
+ it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings'
end
context 'needs with a string type and dependency with an array type' do
let(:needs) { 'build1' }
let(:dependencies) { %w(deploy) }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1:needs config can only be a hash or an array') }
+ it_behaves_like 'returns errors', 'jobs:test1:needs config can only be a hash or an array'
end
context 'needs with a Hash type and dependency with a string type' do
let(:needs) { { job: 'build1' } }
let(:dependencies) { 'deploy' }
- it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies should be an array of strings') }
+ it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings'
end
end
@@ -2141,9 +2099,7 @@ module Gitlab
}
end
- it 'raises a ValidationError' do
- expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when/)
- end
+ it_behaves_like 'returns errors', /may not be used with `rules`: when/
end
context 'used with job-level when:delayed' do
@@ -2159,9 +2115,7 @@ module Gitlab
}
end
- it 'raises a ValidationError' do
- expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when, start_in/)
- end
+ it_behaves_like 'returns errors', /may not be used with `rules`: when, start_in/
end
end
@@ -2348,371 +2302,318 @@ module Gitlab
end
describe "Error handling" do
- it "fails to parse YAML" do
- expect do
- Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ subject { described_class.new(config) }
+
+ context 'when YAML syntax is invalid' do
+ let(:config) { 'invalid: yaml: test' }
+
+ it_behaves_like 'returns errors', /mapping values are not allowed/
end
- it "indicates that object is invalid" do
- expect do
- Gitlab::Ci::YamlProcessor.new("invalid_yaml")
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ context 'when object is invalid' do
+ let(:config) { 'invalid_yaml' }
+
+ it_behaves_like 'returns errors', /Invalid configuration format/
end
- it "returns errors if tags parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:tags config should be an array of strings")
+ context 'returns errors if tags parameter is invalid' do
+ let(:config) { YAML.dump({ rspec: { script: "test", tags: "mysql" } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:tags config should be an array of strings'
end
- it "returns errors if before_script parameter is invalid" do
- config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if before_script parameter is invalid' do
+ let(:config) { YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'before_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if job before_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if job before_script parameter is not an array of strings' do
+ let(:config) { YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:before_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if job before_script parameter is multi-level nested array of strings" do
- config = YAML.dump({ rspec: { script: "test", before_script: [["ls", ["pwd"]], "test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if job before_script parameter is multi-level nested array of strings' do
+ let(:config) { YAML.dump({ rspec: { script: "test", before_script: [["ls", ["pwd"]], "test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:before_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if after_script parameter is invalid" do
- config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if after_script parameter is invalid' do
+ let(:config) { YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'after_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if job after_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if job after_script parameter is not an array of strings' do
+ let(:config) { YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:after_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if job after_script parameter is multi-level nested array of strings" do
- config = YAML.dump({ rspec: { script: "test", after_script: [["ls", ["pwd"]], "test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array containing strings and arrays of strings")
+ context 'returns errors if job after_script parameter is multi-level nested array of strings' do
+ let(:config) { YAML.dump({ rspec: { script: "test", after_script: [["ls", ["pwd"]], "test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:after_script config should be an array containing strings and arrays of strings'
end
- it "returns errors if image parameter is invalid" do
- config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string")
+ context 'returns errors if image parameter is invalid' do
+ let(:config) { YAML.dump({ image: ["test"], rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'image config should be a hash or a string'
end
- it "returns errors if job name is blank" do
- config = YAML.dump({ '' => { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank")
+ context 'returns errors if job name is blank' do
+ let(:config) { YAML.dump({ '' => { script: "test" } }) }
+
+ it_behaves_like 'returns errors', "jobs:job name can't be blank"
end
- it "returns errors if job name is non-string" do
- config = YAML.dump({ 10 => { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol")
+ context 'returns errors if job name is non-string' do
+ let(:config) { YAML.dump({ 10 => { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'jobs:10 name should be a symbol'
end
- it "returns errors if job image parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
+ context 'returns errors if job image parameter is invalid' do
+ let(:config) { YAML.dump({ rspec: { script: "test", image: ["test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:image config should be a hash or a string'
end
- it "returns errors if services parameter is not an array" do
- config = YAML.dump({ services: "test", rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array")
+ context 'returns errors if services parameter is not an array' do
+ let(:config) { YAML.dump({ services: "test", rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'services config should be a array'
end
- it "returns errors if services parameter is not an array of strings" do
- config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string")
+ context 'returns errors if services parameter is not an array of strings' do
+ let(:config) { YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'services:service config should be a hash or a string'
end
- it "returns errors if job services parameter is not an array" do
- config = YAML.dump({ rspec: { script: "test", services: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
+ context 'returns errors if job services parameter is not an array' do
+ let(:config) { YAML.dump({ rspec: { script: "test", services: "test" } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:services config should be a array'
end
- it "returns errors if job services parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string")
+ context 'returns errors if job services parameter is not an array of strings' do
+ let(:config) { YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:services:service config should be a hash or a string'
end
- it "returns error if job configuration is invalid" do
- config = YAML.dump({ extra: "bundle update" })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "root config contains unknown keys: extra")
+ context 'returns error if job configuration is invalid' do
+ let(:config) { YAML.dump({ extra: "bundle update" }) }
+
+ it_behaves_like 'returns errors', 'root config contains unknown keys: extra'
end
- it "returns errors if services configuration is not correct" do
- config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array")
+ context 'returns errors if services configuration is not correct' do
+ let(:config) { YAML.dump({ extra: { script: 'rspec', services: "test" } }) }
+
+ it_behaves_like 'returns errors', 'jobs:extra:services config should be a array'
end
- it "returns errors if there are no jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"] })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ context 'returns errors if there are no jobs defined' do
+ let(:config) { YAML.dump({ before_script: ["bundle update"] }) }
+
+ it_behaves_like 'returns errors', 'jobs config should contain at least one visible job'
end
- it "returns errors if the job script is not defined" do
- config = YAML.dump({ rspec: { before_script: "test" } })
+ context 'returns errors if the job script is not defined' do
+ let(:config) { YAML.dump({ rspec: { before_script: "test" } }) }
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec script can't be blank")
+ it_behaves_like 'returns errors', "jobs:rspec script can't be blank"
end
- it "returns errors if there are no visible jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ context 'returns errors if there are no visible jobs defined' do
+ let(:config) { YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) }
+
+ it_behaves_like 'returns errors', 'jobs config should contain at least one visible job'
end
- it "returns errors if job allow_failure parameter is not an boolean" do
- config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
+ context 'returns errors if job allow_failure parameter is not an boolean' do
+ let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a boolean value'
end
- it "returns errors if job stage is not a string" do
- config = YAML.dump({ rspec: { script: "test", type: 1 } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
+ context 'returns errors if job stage is not a string' do
+ let(:config) { YAML.dump({ rspec: { script: "test", type: 1 } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:type config should be a string'
end
- it "returns errors if job stage is not a pre-defined stage" do
- config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post")
+ context 'returns errors if job stage is not a pre-defined stage' do
+ let(:config) { YAML.dump({ rspec: { script: "test", type: "acceptance" } }) }
+
+ it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
end
- it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, .post")
+ context 'returns errors if job stage is not a defined stage' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) }
+
+ it_behaves_like 'returns errors', 'rspec job: chosen stage does not exist; available stages are .pre, build, test, .post'
end
- it "returns errors if stages is not an array" do
- config = YAML.dump({ stages: "test", rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ context 'returns errors if stages is not an array' do
+ let(:config) { YAML.dump({ stages: "test", rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'stages config should be an array of strings'
end
- it "returns errors if stages is not an array of strings" do
- config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ context 'returns errors if stages is not an array of strings' do
+ let(:config) { YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'stages config should be an array of strings'
end
- it "returns errors if variables is not a map" do
- config = YAML.dump({ variables: "test", rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ context 'returns errors if variables is not a map' do
+ let(:config) { YAML.dump({ variables: "test", rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
end
- it "returns errors if variables is not a map of key-value strings" do
- config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ context 'returns errors if variables is not a map of key-value strings' do
+ let(:config) { YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'variables config should be a hash of key value pairs'
end
- it "returns errors if job when is not on_success, on_failure or always" do
- config = YAML.dump({ rspec: { script: "test", when: 1 } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be one of: #{Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN.join(', ')}")
+ context 'returns errors if job when is not on_success, on_failure or always' do
+ let(:config) { YAML.dump({ rspec: { script: "test", when: 1 } }) }
+
+ it_behaves_like 'returns errors', "jobs:rspec when should be one of: #{Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN.join(', ')}"
end
- it "returns errors if job artifacts:name is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
+ context 'returns errors if job artifacts:name is not an a string' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts name should be a string'
end
- it "returns errors if job artifacts:when is not an a predefined value" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
+ context 'returns errors if job artifacts:when is not an a predefined value' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be on_success, on_failure or always'
end
- it "returns errors if job artifacts:expire_in is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ context 'returns errors if job artifacts:expire_in is not an a string' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
- it "returns errors if job artifacts:expire_in is not an a valid duration" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ context 'returns errors if job artifacts:expire_in is not an a valid duration' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts expire in should be a duration'
end
- it "returns errors if job artifacts:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
+ context 'returns errors if job artifacts:untracked is not an array of strings' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts untracked should be a boolean value'
end
- it "returns errors if job artifacts:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
+ context 'returns errors if job artifacts:paths is not an array of strings' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:artifacts paths should be an array of strings'
end
- it "returns errors if cache:untracked is not an array of strings" do
- config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
+ context 'returns errors if cache:untracked is not an array of strings' do
+ let(:config) { YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'cache:untracked config should be a boolean value'
end
- it "returns errors if cache:paths is not an array of strings" do
- config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings")
+ context 'returns errors if cache:paths is not an array of strings' do
+ let(:config) { YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', 'cache:paths config should be an array of strings'
end
- it "returns errors if cache:key is not a string" do
- config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key should be a hash, a string or a symbol")
+ context 'returns errors if cache:key is not a string' do
+ let(:config) { YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) }
+
+ it_behaves_like 'returns errors', "cache:key should be a hash, a string or a symbol"
end
- it "returns errors if job cache:key is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key should be a hash, a string or a symbol")
+ context 'returns errors if job cache:key is not an a string' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) }
+
+ it_behaves_like 'returns errors', "jobs:rspec:cache:key should be a hash, a string or a symbol"
end
- it 'returns errors if job cache:key:files is not an array of strings' do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config should be an array of strings')
+ context 'returns errors if job cache:key:files is not an array of strings' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config should be an array of strings'
end
- it 'returns errors if job cache:key:files is an empty array' do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config requires at least 1 item')
+ context 'returns errors if job cache:key:files is an empty array' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:cache:key:files config requires at least 1 item'
end
- it 'returns errors if job defines only cache:key:prefix' do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key config missing required keys: files')
+ context 'returns errors if job defines only cache:key:prefix' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:cache:key config missing required keys: files'
end
- it 'returns errors if job cache:key:prefix is not an a string' do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:prefix config should be a string or symbol')
+ context 'returns errors if job cache:key:prefix is not an a string' do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:cache:key:prefix config should be a string or symbol'
end
- it "returns errors if job cache:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
+ context "returns errors if job cache:untracked is not an array of strings" do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) }
+
+ it_behaves_like 'returns errors', "jobs:rspec:cache:untracked config should be a boolean value"
end
- it "returns errors if job cache:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
+ context "returns errors if job cache:paths is not an array of strings" do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) }
+
+ it_behaves_like 'returns errors', "jobs:rspec:cache:paths config should be an array of strings"
end
- it "returns errors if job dependencies is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
- expect do
- Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
+ context "returns errors if job dependencies is not an array of strings" do
+ let(:config) { YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) }
+
+ it_behaves_like 'returns errors', "jobs:rspec dependencies should be an array of strings"
end
- it 'returns errors if pipeline variables expression policy is invalid' do
- config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
+ context 'returns errors if pipeline variables expression policy is invalid' do
+ let(:config) { YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } }) }
- expect { Gitlab::Ci::YamlProcessor.new(config) }
- .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only variables invalid expression syntax')
+ it_behaves_like 'returns errors', 'jobs:rspec:only variables invalid expression syntax'
end
- it 'returns errors if pipeline changes policy is invalid' do
- config = YAML.dump({ rspec: { script: 'test', only: { changes: [1] } } })
+ context 'returns errors if pipeline changes policy is invalid' do
+ let(:config) { YAML.dump({ rspec: { script: 'test', only: { changes: [1] } } }) }
- expect { Gitlab::Ci::YamlProcessor.new(config) }
- .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only changes should be an array of strings')
+ it_behaves_like 'returns errors', 'jobs:rspec:only changes should be an array of strings'
end
- it 'returns errors if extended hash configuration is invalid' do
- config = YAML.dump({ rspec: { extends: 'something', script: 'test' } })
+ context 'returns errors if extended hash configuration is invalid' do
+ let(:config) { YAML.dump({ rspec: { extends: 'something', script: 'test' } }) }
- expect { Gitlab::Ci::YamlProcessor.new(config) }
- .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'rspec: unknown keys in `extends` (something)')
+ it_behaves_like 'returns errors', 'rspec: unknown keys in `extends` (something)'
end
- it 'returns errors if parallel is invalid' do
- config = YAML.dump({ rspec: { parallel: 'test', script: 'test' } })
+ context 'returns errors if parallel is invalid' do
+ let(:config) { YAML.dump({ rspec: { parallel: 'test', script: 'test' } }) }
- expect { Gitlab::Ci::YamlProcessor.new(config) }
- .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:parallel should be an integer or a hash')
+ it_behaves_like 'returns errors', 'jobs:rspec:parallel should be an integer or a hash'
end
end
diff --git a/spec/models/audit_event_partitioned_spec.rb b/spec/models/audit_event_partitioned_spec.rb
index fe69f0083b7..ab48e291f78 100644
--- a/spec/models/audit_event_partitioned_spec.rb
+++ b/spec/models/audit_event_partitioned_spec.rb
@@ -7,7 +7,10 @@ RSpec.describe AuditEventPartitioned do
let(:partitioned_table) { described_class }
it 'has the same columns as the source table' do
- expect(partitioned_table.column_names).to match_array(source_table.column_names)
+ column_names_from_source_table = column_names(source_table)
+ column_names_from_partioned_table = column_names(partitioned_table)
+
+ expect(column_names_from_partioned_table).to match_array(column_names_from_source_table)
end
it 'has the same null constraints as the source table' do
@@ -30,6 +33,14 @@ RSpec.describe AuditEventPartitioned do
expect(event_from_partitioned_table).to eq(event_from_source_table)
end
+ def column_names(table)
+ table.connection.select_all(<<~SQL)
+ SELECT c.column_name
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table.table_name}'
+ SQL
+ end
+
def null_constraints(table)
table.connection.select_all(<<~SQL)
SELECT c.column_name, c.is_nullable
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 1215b38a9a2..aa7ba278643 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe Clusters::Applications::Prometheus do
it 'is initialized with 3 arguments' do
expect(subject.name).to eq('prometheus')
expect(subject.chart).to eq('stable/prometheus')
- expect(subject.version).to eq('9.5.2')
+ expect(subject.version).to eq('10.4.1')
expect(subject).to be_rbac
expect(subject.files).to eq(prometheus.files)
end
@@ -167,7 +167,7 @@ RSpec.describe Clusters::Applications::Prometheus do
let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('9.5.2')
+ expect(subject.version).to eq('10.4.1')
end
end
@@ -238,7 +238,7 @@ RSpec.describe Clusters::Applications::Prometheus do
it 'is initialized with 3 arguments' do
expect(patch_command.name).to eq('prometheus')
expect(patch_command.chart).to eq('stable/prometheus')
- expect(patch_command.version).to eq('9.5.2')
+ expect(patch_command.version).to eq('10.4.1')
expect(patch_command.files).to eq(prometheus.files)
end
end
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 111bc933ea4..40fa2ef425a 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -624,7 +624,7 @@ RSpec.describe API::Ci::Pipelines do
end
it 'does not log an audit event' do
- expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { SecurityEvent.count }
+ expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { AuditEvent.count }
end
context 'when the pipeline has jobs' do
diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb
new file mode 100644
index 00000000000..a4243766111
--- /dev/null
+++ b/spec/requests/api/issue_links_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::IssueLinks do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ describe 'GET /links' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/links")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns related issues' do
+ target_issue = create(:issue, project: project)
+ create(:issue_link, source: issue, target: target_issue)
+
+ get api("/projects/#{project.id}/issues/#{issue.iid}/links", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(response).to match_response_schema('public_api/v4/issue_links')
+ end
+ end
+ end
+
+ describe 'POST /links' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ target_issue = create(:issue)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links"),
+ params: { target_project_id: target_issue.project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ context 'given target project not found' do
+ it 'returns 404' do
+ target_issue = create(:issue)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: -1, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'given target issue not found' do
+ it 'returns 404' do
+ target_project = create(:project, :public)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: target_project.id, target_issue_iid: non_existing_record_iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when user does not have write access to given issue' do
+ it 'returns 404' do
+ unauthorized_project = create(:project)
+ target_issue = create(:issue, project: unauthorized_project)
+ unauthorized_project.add_guest(user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: unauthorized_project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('No Issue found for given params')
+ end
+ end
+
+ context 'when trying to relate to a confidential issue' do
+ it 'returns 404' do
+ project = create(:project, :public)
+ target_issue = create(:issue, :confidential, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when trying to relate to a private project issue' do
+ it 'returns 404' do
+ project = create(:project, :private)
+ target_issue = create(:issue, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when user has ability to create an issue link' do
+ let_it_be(:target_issue) { create(:issue, project: project) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'returns 201 status and contains the expected link response' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid, link_type: 'relates_to' }
+
+ expect_link_response(link_type: 'relates_to')
+ end
+
+ it 'returns 201 when sending full path of target project' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.full_path, target_issue_iid: target_issue.iid }
+
+ expect_link_response
+ end
+
+ def expect_link_response(link_type: 'relates_to')
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/issue_link')
+ expect(json_response['link_type']).to eq(link_type)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /links/:issue_link_id' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ issue_link = create(:issue_link)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ context 'when user does not have write access to given issue link' do
+ it 'returns 404' do
+ unauthorized_project = create(:project)
+ target_issue = create(:issue, project: unauthorized_project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+ unauthorized_project.add_guest(user)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('No Issue Link found')
+ end
+ end
+
+ context 'issue link not found' do
+ it 'returns 404' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{non_existing_record_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when trying to delete a link with a private project issue' do
+ it 'returns 404' do
+ project = create(:project, :private)
+ target_issue = create(:issue, project: project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when user has ability to delete the issue link' do
+ it 'returns 200' do
+ target_issue = create(:issue, project: project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+ project.add_reporter(user)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/issue_link')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
new file mode 100644
index 00000000000..a21c676f000
--- /dev/null
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::IssueLinksController do
+ let(:user) { create :user }
+ let(:project) { create(:project_empty_repo) }
+ let(:issue) { create :issue, project: project }
+
+ describe 'GET /*namespace_id/:project_id/issues/:issue_id/links' do
+ let(:issue_b) { create :issue, project: project }
+ let!(:issue_link) { create :issue_link, source: issue, target: issue_b }
+
+ before do
+ project.add_guest(user)
+ login_as user
+ end
+
+ it 'returns JSON response' do
+ list_service_response = IssueLinks::ListService.new(issue, user).execute
+
+ get namespace_project_issue_links_path(issue_links_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(list_service_response.as_json)
+ end
+ end
+
+ describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do
+ let(:issue_b) { create :issue, project: project }
+
+ before do
+ project.add_role(user, user_role)
+ login_as user
+ end
+
+ context 'with success' do
+ let(:user_role) { :developer }
+ let(:issuable_references) { [issue_b.to_reference] }
+
+ it 'returns success JSON' do
+ post namespace_project_issue_links_path(issue_links_params(issuable_references: issuable_references))
+
+ list_service_response = IssueLinks::ListService.new(issue, user).execute
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('message' => nil,
+ 'issuables' => list_service_response.as_json)
+ end
+ end
+
+ context 'with failure' do
+ context 'when unauthorized' do
+ let(:user_role) { :guest }
+ let(:issuable_references) { [issue_b.to_reference] }
+
+ it 'returns 403' do
+ post namespace_project_issue_links_path(issue_links_params(issuable_references: issuable_references))
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when failing service result' do
+ let(:user_role) { :developer }
+ let(:issuable_references) { ["##{non_existing_record_iid}"] }
+
+ it 'returns failure JSON' do
+ post namespace_project_issue_links_path(issue_links_params(issuable_references: issuable_references))
+
+ list_service_response = IssueLinks::ListService.new(issue, user).execute
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response).to eq('message' => 'No Issue found for given params', 'issuables' => list_service_response.as_json)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /*namespace_id/:project_id/issues/:issue_id/link/:id' do
+ let(:issue_link) { create :issue_link, source: issue, target: referenced_issue }
+
+ before do
+ project.add_role(user, user_role)
+ login_as user
+ end
+
+ context 'when unauthorized' do
+ context 'when no authorization on current project' do
+ let(:referenced_issue) { create :issue, project: project }
+ let(:user_role) { :guest }
+
+ it 'returns 403' do
+ delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when no authorization on the related issue project' do
+ # unauthorized project issue
+ let(:referenced_issue) { create :issue }
+ let(:user_role) { :developer }
+
+ it 'returns 404' do
+ delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when authorized' do
+ let(:referenced_issue) { create :issue, project: project }
+ let(:user_role) { :developer }
+
+ it 'returns success JSON' do
+ delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
+
+ list_service_response = IssueLinks::ListService.new(issue, user).execute
+
+ expect(json_response).to eq('issuables' => list_service_response.as_json)
+ end
+ end
+
+ context 'when non of issues of the link is not the issue requested in the path' do
+ let(:referenced_issue) { create(:issue, project: project) }
+ let(:another_issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user_role) { :developer }
+
+ let!(:issue_link) { create :issue_link, source: another_issue, target: referenced_issue }
+
+ subject do
+ delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not delete the link' do
+ expect { subject }.not_to change { IssueLink.count }.from(1)
+ end
+ end
+ end
+
+ def issue_links_params(opts = {})
+ opts.reverse_merge(namespace_id: issue.project.namespace,
+ project_id: issue.project,
+ issue_id: issue,
+ format: :json)
+ end
+end
diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb
index 530d3469481..5059727ac4a 100644
--- a/spec/services/audit_event_service_spec.rb
+++ b/spec/services/audit_event_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe AuditEventService do
entity_type: "Project",
action: :destroy)
- expect { service.security_event }.to change(SecurityEvent, :count).by(1)
+ expect { service.security_event }.to change(AuditEvent, :count).by(1)
end
it 'formats from and to fields' do
@@ -44,9 +44,9 @@ RSpec.describe AuditEventService do
action: :create,
target_id: 1)
- expect { service.security_event }.to change(SecurityEvent, :count).by(1)
+ expect { service.security_event }.to change(AuditEvent, :count).by(1)
- details = SecurityEvent.last.details
+ details = AuditEvent.last.details
expect(details[:from]).to be true
expect(details[:to]).to be false
expect(details[:action]).to eq(:create)
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
index 23cbe683d2f..6977c99e335 100644
--- a/spec/services/ci/destroy_pipeline_service_spec.rb
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe ::Ci::DestroyPipelineService do
end
it 'does not log an audit event' do
- expect { subject }.not_to change { SecurityEvent.count }
+ expect { subject }.not_to change { AuditEvent.count }
end
context 'when the pipeline has jobs' do