diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 09:10:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 09:10:08 +0000 |
commit | 202268ad93e9a1556f5700326be5ec89bd641a97 (patch) | |
tree | db1faf51de8859ccd812e4a6ed61284cc3bb6a9a | |
parent | 3c63ea4631f629f83c7d35e65963ffc1acf83161 (diff) | |
download | gitlab-ce-202268ad93e9a1556f5700326be5ec89bd641a97.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |