diff options
94 files changed, 832 insertions, 193 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1d8fb1fc5a6..c4ce702892e 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -42,7 +42,6 @@ const Api = { userPostStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits/:sha', commitsPath: '/api/:version/projects/:id/repository/commits', - applySuggestionPath: '/api/:version/suggestions/:id/apply', applySuggestionBatchPath: '/api/:version/suggestions/batch_apply', commitPipelinesPath: '/:project_id/commit/:sha/pipelines', @@ -309,10 +308,12 @@ const Api = { }); }, - projectMilestones(id) { + projectMilestones(id, params = {}) { const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); - return axios.get(url); + return axios.get(url, { + params, + }); }, mergeRequests(params = {}) { diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 231059b895e..616c1e5c254 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -25,10 +25,6 @@ export default { type: Boolean, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelsPath: { type: String, required: true, @@ -201,7 +197,6 @@ export default { :collapse-scope="isNewForm" :board="board" :can-admin-board="canAdminBoard" - :milestone-path="milestonePath" :labels-path="labelsPath" :enable-scoped-labels="enableScopedLabels" :project-id="projectId" diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 48f6ba6cfc7..708d12f46b2 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -36,10 +36,6 @@ export default { type: Object, required: true, }, - milestonePath: { - type: String, - required: true, - }, throttleDuration: { type: Number, default: 200, @@ -335,7 +331,6 @@ export default { <board-form v-if="currentPage" - :milestone-path="milestonePath" :labels-path="labelsPath" :project-id="projectId" :group-id="groupId" diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index 573284d2b44..ae811cff542 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -17,10 +17,6 @@ export default { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 20344b66140..fb2d7b6dbc5 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -38,10 +38,6 @@ export default { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, @@ -149,11 +145,7 @@ export default { class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100" > <div class="add-issues-container d-flex flex-column m-auto rounded"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath" - /> + <modal-header :project-id="projectId" :label-path="labelPath" /> <modal-list v-if="!loading && showList && !filterLoading" :issue-link-base="issueLinkBase" diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 73d37459bfe..51bb72b7657 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -27,7 +27,7 @@ export default () => { hasMissingBoards: parseBoolean(dataset.hasMissingBoards), canAdminBoard: parseBoolean(dataset.canAdminBoard), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), - projectId: Number(dataset.projectId), + projectId: dataset.projectId ? Number(dataset.projectId) : 0, groupId: Number(dataset.groupId), scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), weights: JSON.parse(dataset.weights), diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue index 5872ac39c96..636d9fc4b90 100644 --- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue +++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue @@ -9,6 +9,7 @@ import { GlNewDropdown, GlNewDropdownItem, } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { I18N_ALERT_SETTINGS_FORM, NO_ISSUE_TEMPLATE_SELECTED, @@ -27,6 +28,7 @@ export default { GlNewDropdown, GlNewDropdownItem, }, + mixins: [glFeatureFlagsMixin()], inject: ['service', 'alertSettings'], data() { return { @@ -34,6 +36,7 @@ export default { createIssueEnabled: this.alertSettings.createIssue, issueTemplate: this.alertSettings.issueTemplateKey, sendEmailEnabled: this.alertSettings.sendEmail, + autoCloseIncident: this.alertSettings.autoCloseIncident, loading: false, }; }, @@ -49,6 +52,7 @@ export default { create_issue: this.createIssueEnabled, issue_template_key: this.issueTemplate, send_email: this.sendEmailEnabled, + auto_close_incident: this.autoCloseIncident, }; }, }, @@ -123,6 +127,11 @@ export default { <span>{{ $options.i18n.sendEmail.label }}</span> </gl-form-checkbox> </gl-form-group> + <gl-form-group v-if="glFeatures.autoCloseIncident" class="gl-pl-0 gl-mb-5"> + <gl-form-checkbox v-model="autoCloseIncident"> + <span>{{ $options.i18n.autoCloseIncidents.label }}</span> + </gl-form-checkbox> + </gl-form-group> <div class="gl-display-flex gl-justify-content-end"> <gl-button ref="submitBtn" diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js index 77f7ee2c4a3..42f1f645d16 100644 --- a/app/assets/javascripts/incidents_settings/constants.js +++ b/app/assets/javascripts/incidents_settings/constants.js @@ -42,6 +42,9 @@ export const I18N_ALERT_SETTINGS_FORM = { sendEmail: { label: __('Send a separate email notification to Developers.'), }, + autoCloseIncidents: { + label: __('Automatically close incident issues when the associated Prometheus alert resolves.'), + }, }; export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') }; diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js index 80e7d07feca..ad875d49768 100644 --- a/app/assets/javascripts/incidents_settings/index.js +++ b/app/assets/javascripts/incidents_settings/index.js @@ -20,6 +20,7 @@ export default () => { pagerdutyActive, pagerdutyWebhookUrl, pagerdutyResetKeyPath, + autoCloseIncident, }, } = el; @@ -33,6 +34,7 @@ export default () => { createIssue: parseBoolean(createIssue), issueTemplateKey, sendEmail: parseBoolean(sendEmail), + autoCloseIncident: parseBoolean(autoCloseIncident), }, pagerDutySettings: { active: parseBoolean(pagerdutyActive), diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index caa45184bfc..7b8b8f97002 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -4,10 +4,11 @@ import $ from 'jquery'; import { template, escape } from 'lodash'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import '~/gl_dropdown'; +import Api from '~/api'; import axios from './lib/utils/axios_utils'; -import { timeFor } from './lib/utils/datetime_utility'; +import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; import boardsStore, { boardStoreIssueSet, @@ -34,10 +35,10 @@ export default class MilestoneSelect { $els.each((i, dropdown) => { let milestoneLinkNoneTemplate, milestoneLinkTemplate, + milestoneExpiredLinkTemplate, selectedMilestone, selectedMilestoneDefault; const $dropdown = $(dropdown); - const milestonesUrl = $dropdown.data('milestones'); const issueUpdateURL = $dropdown.data('issueUpdate'); const showNo = $dropdown.data('showNo'); const showAny = $dropdown.data('showAny'); @@ -63,58 +64,103 @@ export default class MilestoneSelect { milestoneLinkTemplate = template( '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); + milestoneExpiredLinkTemplate = template( + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>', + ); milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; } return $dropdown.glDropdown({ showMenuAbove, - data: (term, callback) => - axios.get(milestonesUrl).then(({ data }) => { - const extraOptions = []; - if (showAny) { - extraOptions.push({ - id: null, - name: null, - title: __('Any milestone'), - }); - } - if (showNo) { - extraOptions.push({ - id: -1, - name: __('No milestone'), - title: __('No milestone'), - }); - } - if (showUpcoming) { - extraOptions.push({ - id: -2, - name: '#upcoming', - title: __('Upcoming'), - }); - } - if (showStarted) { - extraOptions.push({ - id: -3, - name: '#started', - title: __('Started'), - }); - } - if (extraOptions.length) { - extraOptions.push({ type: 'divider' }); - } + data: (term, callback) => { + let contextId = $dropdown.get(0).dataset.projectId; + let getMilestones = Api.projectMilestones; + const reqParams = { state: 'active', include_parent_milestones: true }; - callback(extraOptions.concat(data)); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active'); - }), - renderRow: milestone => ` - <li data-milestone-id="${escape(milestone.name)}"> + if (!contextId) { + contextId = $dropdown.get(0).dataset.groupId; + delete reqParams.include_parent_milestones; + getMilestones = Api.groupMilestones; + } + + // We don't use $.data() as it caches initial value and never updates! + return getMilestones(contextId, reqParams) + .then(({ data }) => + data + .map(m => ({ + ...m, + // Public API includes `title` instead of `name`. + name: m.title, + })) + .sort((mA, mB) => { + // Move all expired milestones to the bottom. + if (mA.expired) { + return 1; + } + if (mB.expired) { + return -1; + } + return 0; + }), + ) + .then(data => { + const extraOptions = []; + if (showAny) { + extraOptions.push({ + id: null, + name: null, + title: __('Any milestone'), + }); + } + if (showNo) { + extraOptions.push({ + id: -1, + name: __('No milestone'), + title: __('No milestone'), + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: __('Upcoming'), + }); + } + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: __('Started'), + }); + } + if (extraOptions.length) { + extraOptions.push({ type: 'divider' }); + } + + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } + $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); + }); + }, + renderRow: milestone => { + const milestoneName = milestone.title || milestone.name; + let milestoneDisplayName = escape(milestoneName); + + if (milestone.expired) { + milestoneDisplayName = sprintf(__('%{milestone} (expired)'), { + milestone: milestoneDisplayName, + }); + } + + return ` + <li data-milestone-id="${escape(milestoneName)}"> <a href='#' class='dropdown-menu-milestone-link'> - ${escape(milestone.title)} + ${milestoneDisplayName} </a> </li> - `, + `; + }, filterable: true, search: { fields: ['title'], @@ -149,7 +195,7 @@ export default class MilestoneSelect { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; } $('a.is-active', $el).removeClass('is-active'); - $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active'); + $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: clickEvent => { @@ -237,7 +283,16 @@ export default class MilestoneSelect { if (data.milestone != null) { data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; - $value.html(milestoneLinkTemplate(data.milestone)); + $value.html( + data.milestone.expired + ? milestoneExpiredLinkTemplate({ + ...data.milestone, + remaining: sprintf(__('%{due_date} (Past due)'), { + due_date: dateInWords(parsePikadayDate(data.milestone.due_date)), + }), + }) + : milestoneLinkTemplate(data.milestone), + ); return $sidebarCollapsedValue .attr( 'data-original-title', diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 781b850ddfe..12543e5f780 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -6,6 +6,10 @@ module Projects before_action :authorize_admin_operations! before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token] + before_action do + push_frontend_feature_flag(:auto_close_incident) + end + respond_to :json, only: [:reset_alerting_token, :reset_pagerduty_token] helper_method :error_tracking_setting diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index 37e91153710..d38bc4a9940 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -43,6 +43,7 @@ module OperationsHelper create_issue: setting.create_issue.to_s, issue_template_key: setting.issue_template_key.to_s, send_email: setting.send_email.to_s, + auto_close_incident: 'true', pagerduty_active: setting.pagerduty_active.to_s, pagerduty_token: setting.pagerduty_token.to_s, pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(@project, token: setting.pagerduty_token), diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 7762328d274..289417327e8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -229,6 +229,12 @@ module Ci end after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| + pipeline.run_after_commit do + ::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id) + end + end + + after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| next unless pipeline.bridge_triggered? next unless pipeline.bridge_waiting? @@ -539,12 +545,6 @@ module Ci end # rubocop: enable CodeReuse/ServiceClass - def mark_as_processable_after_stage(stage_idx) - builds.skipped.after_stage(stage_idx).find_each do |build| - Gitlab::OptimisticLocking.retry_lock(build, &:process) - end - end - def lazy_ref_commit return unless ::Gitlab::Ci::Features.pipeline_latest? @@ -862,6 +862,10 @@ module Ci complete? && latest_report_builds(reports_scope).exists? end + def has_coverage_reports? + self.has_reports?(Ci::JobArtifact.coverage_reports) + end + def test_report_summary Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results) end diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index e7f51977ccd..5a3bf52a43d 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -14,6 +14,11 @@ module Ci ].freeze FILE_SIZE_LIMIT = 10.megabytes.freeze + EXPIRATION_DATE = 1.week.freeze + + DEFAULT_FILE_NAMES = { + code_coverage: 'code_coverage.json' + }.freeze belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_artifacts @@ -24,14 +29,13 @@ module Ci validates :file_type, presence: true mount_file_store_uploader Ci::PipelineArtifactUploader - before_save :set_size, if: :file_changed? enum file_type: { code_coverage: 1 } - def set_size - self.size = file.size + def self.has_code_coverage? + where(file_type: :code_coverage).exists? end end end diff --git a/app/services/ci/pipelines/create_artifact_service.rb b/app/services/ci/pipelines/create_artifact_service.rb new file mode 100644 index 00000000000..179e18f22e8 --- /dev/null +++ b/app/services/ci/pipelines/create_artifact_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +module Ci + module Pipelines + class CreateArtifactService + def execute(pipeline) + return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project) + return unless pipeline.has_coverage_reports? + return if pipeline.pipeline_artifacts.has_code_coverage? + + file = build_carrierwave_file(pipeline) + + pipeline.pipeline_artifacts.create!( + project_id: pipeline.project_id, + file_type: :code_coverage, + file_format: :raw, + size: file["tempfile"].size, + file: file, + expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now + ) + end + + private + + def build_carrierwave_file(pipeline) + CarrierWaveStringFile.new_file( + file_content: pipeline.coverage_reports.to_json, + filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage), + content_type: 'application/json' + ) + end + end + end +end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 60b3d28b0c5..23f64f5699f 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -12,7 +12,7 @@ module Ci build.ensure_scheduling_type! reprocess!(build).tap do |new_build| - build.pipeline.mark_as_processable_after_stage(build.stage_idx) + mark_subsequent_stages_as_processable(build) Gitlab::OptimisticLocking.retry_lock(new_build, &:enqueue) @@ -60,5 +60,11 @@ module Ci end build end + + def mark_subsequent_stages_as_processable(build) + build.pipeline.processables.skipped.after_stage(build.stage_idx).find_each do |processable| + Gitlab::OptimisticLocking.retry_lock(processable, &:process) + end + end end end diff --git a/app/services/discussions/capture_diff_note_position_service.rb b/app/services/discussions/capture_diff_note_position_service.rb index 4e8fd90a2e7..87aa27e455f 100644 --- a/app/services/discussions/capture_diff_note_position_service.rb +++ b/app/services/discussions/capture_diff_note_position_service.rb @@ -19,13 +19,16 @@ module Discussions position = result[:position] return unless position + line_code = position.line_code(project.repository) + return unless line_code + # Currently position data is copied across all notes of a discussion # It makes sense to store a position only for the first note instead # Within the newly introduced table we can start doing just that DiffNotePosition.create_or_update_for(discussion.notes.first, diff_type: :head, position: position, - line_code: position.line_code(project.repository)) + line_code: line_code) end private diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index cf927683ce9..7eac45c00ca 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -7,6 +7,7 @@ module Packages def execute return error('Version is empty.', 400) if version.blank? return error('Package already exists.', 403) if current_package_exists? + return error('File is too large.', 400) if file_size_exceeded? ActiveRecord::Base.transaction { create_package! } end @@ -86,6 +87,10 @@ module Packages _version, versions_data = params[:versions].first versions_data end + + def file_size_exceeded? + project.actual_limits.exceeded?(:npm_max_file_size, attachment['length'].to_i) + end end end end diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml index 510e05ce888..23d63fde671 100644 --- a/app/views/shared/boards/components/sidebar/_milestone.html.haml +++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml @@ -18,7 +18,8 @@ .dropdown %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" }, ":data-selected" => "milestoneTitle", - ":data-issuable-id" => "issue.iid" } + ":data-issuable-id" => "issue.iid", + ":data-project-id" => "issue.project_id" } = _("Milestone") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 994777e6d4a..987e875674d 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -45,7 +45,8 @@ = 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 - if milestone.present? - = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] } + - milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title] + = link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] } - else %span.no-value = _('None') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 2c871c55f0a..e16032cdc50 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -899,6 +899,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: pipeline_background:ci_pipelines_create_artifact + :feature_category: :continuous_integration + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: pipeline_background:ci_ref_delete_unlock_artifacts :feature_category: :continuous_integration :has_external_dependencies: diff --git a/app/workers/ci/pipelines/create_artifact_worker.rb b/app/workers/ci/pipelines/create_artifact_worker.rb new file mode 100644 index 00000000000..220df975503 --- /dev/null +++ b/app/workers/ci/pipelines/create_artifact_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Ci + module Pipelines + class CreateArtifactWorker + include ApplicationWorker + include PipelineBackgroundQueue + + idempotent! + + def perform(pipeline_id) + Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline| + Ci::Pipelines::CreateArtifactService.new.execute(pipeline) + end + end + end + end +end diff --git a/changelogs/unreleased/196066-add-milestone-expired-info.yml b/changelogs/unreleased/196066-add-milestone-expired-info.yml new file mode 100644 index 00000000000..b9841a102a4 --- /dev/null +++ b/changelogs/unreleased/196066-add-milestone-expired-info.yml @@ -0,0 +1,5 @@ +--- +title: Show expired milestones at the bottom of the list within dropdown +merge_request: 36562 +author: +type: added diff --git a/changelogs/unreleased/213456-fix-bridge-retry-after-retry.yml b/changelogs/unreleased/213456-fix-bridge-retry-after-retry.yml new file mode 100644 index 00000000000..ba1ed46c7f0 --- /dev/null +++ b/changelogs/unreleased/213456-fix-bridge-retry-after-retry.yml @@ -0,0 +1,5 @@ +--- +title: Fix non-retrying bridges after retried builds in CI pipelines +merge_request: 39989 +author: +type: fixed diff --git a/changelogs/unreleased/217581-remove-default-column.yml b/changelogs/unreleased/217581-remove-default-column.yml new file mode 100644 index 00000000000..ea4ee0e7d28 --- /dev/null +++ b/changelogs/unreleased/217581-remove-default-column.yml @@ -0,0 +1,5 @@ +--- +title: Remove default column from services table +merge_request: 39817 +author: +type: other diff --git a/changelogs/unreleased/218017-nuget-size-limits-db.yml b/changelogs/unreleased/218017-nuget-size-limits-db.yml new file mode 100644 index 00000000000..8cc4b196745 --- /dev/null +++ b/changelogs/unreleased/218017-nuget-size-limits-db.yml @@ -0,0 +1,5 @@ +--- +title: Add package file size limits to plan limits +merge_request: 39633 +author: +type: added diff --git a/changelogs/unreleased/id-fix-nil-line-codes-for-diff-positions.yml b/changelogs/unreleased/id-fix-nil-line-codes-for-diff-positions.yml new file mode 100644 index 00000000000..1d7b495e8f0 --- /dev/null +++ b/changelogs/unreleased/id-fix-nil-line-codes-for-diff-positions.yml @@ -0,0 +1,5 @@ +--- +title: Avoid creating diff position when line-code is nil +merge_request: 40089 +author: +type: fixed diff --git a/changelogs/unreleased/jh-allow-x-envelope-to-header.yml b/changelogs/unreleased/jh-allow-x-envelope-to-header.yml new file mode 100644 index 00000000000..39e2afb29e4 --- /dev/null +++ b/changelogs/unreleased/jh-allow-x-envelope-to-header.yml @@ -0,0 +1,5 @@ +--- +title: Support X-Envelope-To header as a location for Service Desk key +merge_request: 40001 +author: +type: fixed diff --git a/changelogs/unreleased/respect_visiblity_instrument_methods.yml b/changelogs/unreleased/respect_visiblity_instrument_methods.yml new file mode 100644 index 00000000000..1d3be0bd0ea --- /dev/null +++ b/changelogs/unreleased/respect_visiblity_instrument_methods.yml @@ -0,0 +1,5 @@ +--- +title: Respect original visibility for instrumented methods +merge_request: 39951 +author: +type: fixed diff --git a/changelogs/unreleased/sh-include-redis-in-workhorse-test.yml b/changelogs/unreleased/sh-include-redis-in-workhorse-test.yml new file mode 100644 index 00000000000..36d70e9bdb0 --- /dev/null +++ b/changelogs/unreleased/sh-include-redis-in-workhorse-test.yml @@ -0,0 +1,5 @@ +--- +title: Include Redis in Workhorse-in-test-suite integration +merge_request: 40026 +author: +type: other diff --git a/db/migrate/20200818171229_add_package_max_file_size_to_plan_limits.rb b/db/migrate/20200818171229_add_package_max_file_size_to_plan_limits.rb new file mode 100644 index 00000000000..5343da6ed5e --- /dev/null +++ b/db/migrate/20200818171229_add_package_max_file_size_to_plan_limits.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddPackageMaxFileSizeToPlanLimits < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column(:plan_limits, :conan_max_file_size, :bigint, default: 50.megabytes, null: false) + add_column(:plan_limits, :maven_max_file_size, :bigint, default: 50.megabytes, null: false) + add_column(:plan_limits, :npm_max_file_size, :bigint, default: 50.megabytes, null: false) + add_column(:plan_limits, :nuget_max_file_size, :bigint, default: 50.megabytes, null: false) + add_column(:plan_limits, :pypi_max_file_size, :bigint, default: 50.megabytes, null: false) + end +end diff --git a/db/post_migrate/20200819082334_remove_default_from_services.rb b/db/post_migrate/20200819082334_remove_default_from_services.rb new file mode 100644 index 00000000000..2a990016c95 --- /dev/null +++ b/db/post_migrate/20200819082334_remove_default_from_services.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class RemoveDefaultFromServices < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + remove_column :services, :default, :boolean + end + end + + def down + with_lock_retries do + add_column :services, :default, :boolean, default: false + end + end +end diff --git a/db/schema_migrations/20200818171229 b/db/schema_migrations/20200818171229 new file mode 100644 index 00000000000..91470670c26 --- /dev/null +++ b/db/schema_migrations/20200818171229 @@ -0,0 +1 @@ +987f316571f41ad679cad54089bc523f62d04691c10e5cf1957cf60edd71f889
\ No newline at end of file diff --git a/db/schema_migrations/20200819082334 b/db/schema_migrations/20200819082334 new file mode 100644 index 00000000000..28d83ca820d --- /dev/null +++ b/db/schema_migrations/20200819082334 @@ -0,0 +1 @@ +c12f3f5b76e1065867682216348dd95c22d605c30ae54615f2596b1d84aad199
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 7fdde701fbf..eb5cc6c7433 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14103,7 +14103,12 @@ CREATE TABLE public.plan_limits ( ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL, ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL, ci_max_artifact_size_load_performance integer DEFAULT 0 NOT NULL, - ci_needs_size_limit integer DEFAULT 50 NOT NULL + ci_needs_size_limit integer DEFAULT 50 NOT NULL, + conan_max_file_size bigint DEFAULT 52428800 NOT NULL, + maven_max_file_size bigint DEFAULT 52428800 NOT NULL, + npm_max_file_size bigint DEFAULT 52428800 NOT NULL, + nuget_max_file_size bigint DEFAULT 52428800 NOT NULL, + pypi_max_file_size bigint DEFAULT 52428800 NOT NULL ); CREATE SEQUENCE public.plan_limits_id_seq @@ -15404,7 +15409,6 @@ CREATE TABLE public.services ( tag_push_events boolean DEFAULT true, note_events boolean DEFAULT true NOT NULL, category character varying DEFAULT 'common'::character varying NOT NULL, - "default" boolean DEFAULT false, wiki_page_events boolean DEFAULT true, pipeline_events boolean DEFAULT false NOT NULL, confidential_issues_events boolean DEFAULT true NOT NULL, diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index f30dba331b8..04af60ca0fa 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -514,3 +514,38 @@ Total number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. More information can be found in the [Push event activities limit and bulk push events documentation](../user/admin_area/settings/push_event_activities_limit.md). + +## Package Registry Limits + +### File Size Limits + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218017) in GitLab 13.4. + +On GitLab.com, the maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) +is 50 megabytes. + +Limits are set per package type. + +To set this limit on a self-managed installation, run the following in the +[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): + +```ruby +# File size limit is stored in bytes + +# For Conan Packages +Plan.default.actual_limits.update!(conan_max_file_size: 100.megabytes) + +# For NPM Packages +Plan.default.actual_limits.update!(npm_max_file_size: 100.megabytes) + +# For NuGet Packages +Plan.default.actual_limits.update!(nuget_max_file_size: 100.megabytes) + +# For Maven Packages +Plan.default.actual_limits.update!(maven_max_file_size: 100.megabytes) + +# For PyPI Packages +Plan.default.actual_limits.update!(pypi_max_file_size: 100.megabytes) +``` + +Set the limit to `0` to allow any file size. diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index e992637f4f0..47350442b3e 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -55,6 +55,7 @@ Example Response: "state": "active", "updated_at": "2013-10-02T09:24:18Z", "created_at": "2013-10-02T09:24:18Z", + "expired": false, "web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/42" } ] diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 7b4d1cc331d..7b26dbadad4 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -52,7 +52,8 @@ Example Response: "start_date": "2013-11-10", "state": "active", "updated_at": "2013-10-02T09:24:18Z", - "created_at": "2013-10-02T09:24:18Z" + "created_at": "2013-10-02T09:24:18Z", + "expired": false } ] ``` diff --git a/lib/api/conan_packages.rb b/lib/api/conan_packages.rb index 6923d252fbd..3a55128781b 100644 --- a/lib/api/conan_packages.rb +++ b/lib/api/conan_packages.rb @@ -293,7 +293,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end end @@ -320,7 +320,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end desc 'Upload package files' do diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb index 5a0c222d691..b191210a234 100644 --- a/lib/api/entities/milestone.rb +++ b/lib/api/entities/milestone.rb @@ -10,6 +10,7 @@ module API expose :state, :created_at, :updated_at expose :due_date expose :start_date + expose :expired?, as: :expired expose :web_url do |milestone, _options| Gitlab::UrlBuilder.build(milestone) diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index a5fde1af41e..d86b80a7e66 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -155,6 +155,7 @@ module API def upload_package_file(file_type) authorize_upload!(project) + bad_request!('File is too large') if project.actual_limits.exceeded?(:conan_max_file_size, params['file.size'].to_i) current_package = find_or_create_package diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 32a45c59cfa..caaeabf7061 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -200,7 +200,7 @@ module API status 200 content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE - ::Packages::PackageFileUploader.workhorse_authorize(has_length: true) + ::Packages::PackageFileUploader.workhorse_authorize(has_length: true, maximum_size: user_project.actual_limits.maven_max_file_size) end desc 'Upload the maven package file' do @@ -214,6 +214,7 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do authorize_upload! + bad_request!('File is too large') if user_project.actual_limits.exceeded?(:maven_max_file_size, params[:file].size) file_name, format = extract_format(params[:file_name]) diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index 56c4de2071d..87290cd07d9 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -92,6 +92,7 @@ module API put do authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) file_params = params.merge( file: params[:package], @@ -118,7 +119,11 @@ module API route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: authorized_user_project, has_length: false) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.nuget_max_file_size + ) end params do diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 739928a61ed..b3668a88204 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -120,6 +120,7 @@ module API route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true post do authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size) track_event('push_package') @@ -136,7 +137,11 @@ module API route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true post 'authorize' do - authorize_workhorse!(subject: authorized_user_project, has_length: false) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.pypi_max_file_size + ) end end end diff --git a/lib/carrier_wave_string_file.rb b/lib/carrier_wave_string_file.rb index c9a64d9e631..b6bc3d986ca 100644 --- a/lib/carrier_wave_string_file.rb +++ b/lib/carrier_wave_string_file.rb @@ -4,4 +4,12 @@ class CarrierWaveStringFile < StringIO def original_filename "" end + + def self.new_file(file_content:, filename:, content_type: "application/octet-stream") + { + "tempfile" => StringIO.new(file_content), + "filename" => filename, + "content_type" => content_type + } + end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 934d1a4c9f1..895daf65d0d 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -83,6 +83,10 @@ module Gitlab def self.project_transactionless_destroy?(project) Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false) end + + def self.coverage_report_view?(project) + ::Feature.enabled?(:coverage_report_view, project) + end end end end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index bf6c28b9f90..f5e47b43a9a 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -54,7 +54,8 @@ module Gitlab def key_from_additional_headers(mail) find_key_from_references(mail) || find_key_from_delivered_to_header(mail) || - find_key_from_envelope_to_header(mail) + find_key_from_envelope_to_header(mail) || + find_key_from_x_envelope_to_header(mail) end def ensure_references_array(references) @@ -91,6 +92,13 @@ module Gitlab end end + def find_key_from_x_envelope_to_header(mail) + Array(mail[:x_envelope_to]).find do |header| + key = Gitlab::IncomingEmail.key_from_address(header.value) + break key if key + end + end + def ignore_auto_reply!(mail) if auto_submitted?(mail) || auto_replied?(mail) raise AutoGeneratedEmailError diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index ff3fffe7b95..66361529546 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -120,9 +120,6 @@ module Gitlab def self.instrument(type, mod, name) return unless ::Gitlab::Metrics.enabled? - name = name.to_sym - target = type == :instance ? mod : mod.singleton_class - if type == :instance target = mod method_name = "##{name}" @@ -154,6 +151,8 @@ module Gitlab '*args' end + method_visibility = method_visibility_for(target, name) + proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) if trans = Gitlab::Metrics::Instrumentation.transaction @@ -163,11 +162,23 @@ module Gitlab super end end + #{method_visibility} :#{name} EOF target.prepend(proxy_module) end + def self.method_visibility_for(mod, name) + if mod.private_method_defined?(name) + :private + elsif mod.protected_method_defined?(name) + :protected + else + :public + end + end + private_class_method :method_visibility_for + # Small layer of indirection to make it easier to stub out the current # transaction. def self.transaction diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index 64a30fbe16c..47c685f1bc8 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -28,6 +28,26 @@ module Gitlab end # rubocop:enable Rails/Output + module Workhorse + extend Gitlab::SetupHelper + class << self + def configuration_toml(dir, _) + config = { redis: { URL: redis_url } } + + TomlRB.dump(config) + end + + def redis_url + data = YAML.load_file(Rails.root.join('config/resque.yml')) + data.dig(Rails.env, 'url') + end + + def get_config_path(dir) + File.join(dir, 'config.toml') + end + end + end + module Gitaly extend Gitlab::SetupHelper class << self diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 80d6d166da5..540ed589bc2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -395,6 +395,9 @@ msgstr "" msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}" msgstr "" +msgid "%{due_date} (Past due)" +msgstr "" + msgid "%{duration}ms" msgstr "" @@ -542,6 +545,12 @@ msgstr "" msgid "%{mergeLength}/%{usersLength} can merge" msgstr "" +msgid "%{milestone_name} (Past due)" +msgstr "" + +msgid "%{milestone} (expired)" +msgstr "" + msgid "%{mrText}, this issue will be closed automatically." msgstr "" @@ -3632,6 +3641,9 @@ msgstr "" msgid "Automatic certificate management using Let's Encrypt" msgstr "" +msgid "Automatically close incident issues when the associated Prometheus alert resolves." +msgstr "" + msgid "Automatically create merge requests for vulnerabilities that have fixes available." msgstr "" diff --git a/scripts/trigger-build b/scripts/trigger-build index 8edf4bb57f7..ab6dcc63e11 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -343,7 +343,7 @@ module Trigger sleep INTERVAL when :success puts "#{self.class.unscoped_class_name} succeeded in #{duration} minutes!" - break + return else raise "#{self.class.unscoped_class_name} did not succeed!" end diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index 4c1d5f07a42..aca743b9841 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -53,5 +53,10 @@ FactoryBot.define do finished status { 'failed' } end + + trait :skipped do + started + status { 'skipped' } + end end end diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb index ecfd1e79e78..e601b0bbf0e 100644 --- a/spec/factories/ci/pipeline_artifacts.rb +++ b/spec/factories/ci/pipeline_artifacts.rb @@ -13,5 +13,16 @@ FactoryBot.define do artifact.file = fixture_file_upload( Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json') end + + trait :with_multibyte_characters do + size { { "utf8" => "✓" }.to_json.size } + after(:build) do |artifact, _evaluator| + artifact.file = CarrierWaveStringFile.new_file( + file_content: { "utf8" => "✓" }.to_json, + filename: 'filename', + content_type: 'application/json' + ) + end + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 328b7f9a229..58205bb63c4 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -3,8 +3,6 @@ require_relative '../support/helpers/test_env' FactoryBot.define do - PAGES_ACCESS_LEVEL_SCHEMA_VERSION ||= 20180423204600 - # Project without repository # # Project does not have bare repository. @@ -54,13 +52,10 @@ FactoryBot.define do issues_access_level: evaluator.issues_access_level, forking_access_level: evaluator.forking_access_level, merge_requests_access_level: merge_requests_access_level, - repository_access_level: evaluator.repository_access_level + repository_access_level: evaluator.repository_access_level, + pages_access_level: evaluator.pages_access_level } - if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION - hash.store("pages_access_level", evaluator.pages_access_level) - end - project.project_feature.update!(hash) # Normally the class Projects::CreateService is used for creating diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone.json b/spec/fixtures/api/schemas/public_api/v4/milestone.json index 6ca2e88ae91..c8c6a7b6ae1 100644 --- a/spec/fixtures/api/schemas/public_api/v4/milestone.json +++ b/spec/fixtures/api/schemas/public_api/v4/milestone.json @@ -12,11 +12,13 @@ "updated_at": { "type": "date" }, "start_date": { "type": "date" }, "due_date": { "type": "date" }, + "expired": { "type": ["boolean", "null"] }, "web_url": { "type": "string" } }, "required": [ "id", "iid", "title", "description", "state", - "state", "created_at", "updated_at", "start_date", "due_date" + "state", "created_at", "updated_at", "start_date", + "due_date", "expired" ], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json b/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json index e2475545ee9..f008ed7d55f 100644 --- a/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json +++ b/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json @@ -12,6 +12,7 @@ "updated_at": { "type": "date" }, "start_date": { "type": "date" }, "due_date": { "type": "date" }, + "expired": { "type": ["boolean", "null"] }, "web_url": { "type": "string" }, "issue_stats": { "required": ["total", "closed"], @@ -24,7 +25,8 @@ }, "required": [ "id", "iid", "title", "description", "state", - "state", "created_at", "updated_at", "start_date", "due_date", "issue_stats" + "state", "created_at", "updated_at", "start_date", + "due_date", "expired", "issue_stats" ], "additionalProperties": false } diff --git a/spec/fixtures/emails/x_envelope_to_header.eml b/spec/fixtures/emails/x_envelope_to_header.eml new file mode 100644 index 00000000000..28e3d71535a --- /dev/null +++ b/spec/fixtures/emails/x_envelope_to_header.eml @@ -0,0 +1,32 @@ +Return-Path: <jake@example.com> +Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@example.com>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by myserver.example.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +From: "jake@example.com" <jake@example.com> +To: "support@example.com" <support@example.com> +Subject: Insert hilarious subject line here +Date: Tue, 26 Nov 2019 14:22:41 +0000 +Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT> +Accept-Language: de-DE, en-US +Content-Language: de-DE +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-ms-exchange-transport-fromentityheader: Hosted +x-originating-ip: [62.96.54.178] +Content-Type: multipart/alternative; + boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_" +MIME-Version: 1.0 +X-Envelope-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com + +--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Look, a message with some alternate headers! We should really support them. diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 4f4de62c229..3ae0d06162d 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -398,6 +398,29 @@ describe('Api', () => { }); }); + describe('projectMilestones', () => { + it('fetches project milestones', done => { + const projectId = 1; + const options = { state: 'active' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`; + mock.onGet(expectedUrl).reply(200, [ + { + id: 1, + title: 'milestone1', + state: 'active', + }, + ]); + + Api.projectMilestones(projectId, options) + .then(({ data }) => { + expect(data.length).toBe(1); + expect(data[0].title).toBe('milestone1'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('newLabel', () => { it('creates a new label', done => { const namespace = 'some namespace'; diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index b1d277863e8..05d9b721d96 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -11,7 +11,6 @@ describe('board_form.vue', () => { const propsData = { canAdminBoard: false, labelsPath: `${TEST_HOST}/labels/path`, - milestonePath: `${TEST_HOST}/milestone/path`, }; const findModal = () => wrapper.find(DeprecatedModal); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index f2d4de238d1..4cab044bd79 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -81,7 +81,6 @@ describe('BoardsSelector', () => { assignee_id: null, labels: [], }, - milestonePath: `${TEST_HOST}/milestone/path`, boardBaseUrl: `${TEST_HOST}/board/base/url`, hasMissingBoards: false, canAdminBoard: true, diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap index f3f610e4bb7..fd6af59eb48 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -81,6 +81,18 @@ exports[`Alert integration settings form default state should match the default </gl-form-checkbox-stub> </gl-form-group-stub> + <gl-form-group-stub + class="gl-pl-0 gl-mb-5" + > + <gl-form-checkbox-stub + checked="true" + > + <span> + Automatically close incident issues when the associated Prometheus alert resolves. + </span> + </gl-form-checkbox-stub> + </gl-form-group-stub> + <div class="gl-display-flex gl-justify-content-end" > diff --git a/spec/frontend/incidents_settings/components/alerts_form_spec.js b/spec/frontend/incidents_settings/components/alerts_form_spec.js index 04832f31e58..32d13b8edc0 100644 --- a/spec/frontend/incidents_settings/components/alerts_form_spec.js +++ b/spec/frontend/incidents_settings/components/alerts_form_spec.js @@ -10,12 +10,14 @@ describe('Alert integration settings form', () => { beforeEach(() => { wrapper = shallowMount(AlertsSettingsForm, { provide: { + glFeatures: { autoCloseIncident: true }, service, alertSettings: { issueTemplateKey: 'selecte_tmpl', createIssue: true, sendEmail: false, templates: [], + autoCloseIncident: true, }, }, }); @@ -42,6 +44,7 @@ describe('Alert integration settings form', () => { create_issue: wrapper.vm.createIssueEnabled, issue_template_key: wrapper.vm.issueTemplate, send_email: wrapper.vm.sendEmailEnabled, + auto_close_incident: wrapper.vm.autoCloseIncident, }), ); }); diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb index 8e3b1db5272..6fda2f0474d 100644 --- a/spec/helpers/operations_helper_spec.rb +++ b/spec/helpers/operations_helper_spec.rb @@ -150,6 +150,7 @@ RSpec.describe OperationsHelper do create_issue: 'false', issue_template_key: 'template-key', send_email: 'false', + auto_close_incident: 'true', pagerduty_active: 'true', pagerduty_token: operations_settings.pagerduty_token, pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(project, token: operations_settings.pagerduty_token), diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 592d3f3f0e4..ccff902d290 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -36,6 +36,12 @@ RSpec.describe Gitlab::Email::Receiver do it_behaves_like 'correctly finds the mail key' end + context 'when in an X-Envelope-To header' do + let(:email_raw) { fixture_file('emails/x_envelope_to_header.eml') } + + it_behaves_like 'correctly finds the mail key' + end + context 'when enclosed with angle brackets in an Envelope-To header' do let(:email_raw) { fixture_file('emails/envelope_to_header_with_angle_brackets.eml') } diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 2729fbce974..b15e06a0861 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -12,6 +12,11 @@ RSpec.describe Gitlab::Metrics::Instrumentation do text end + def self.wat(text = 'wat') + text + end + private_class_method :wat + class << self def buzz(text = 'buzz') text @@ -242,6 +247,7 @@ RSpec.describe Gitlab::Metrics::Instrumentation do expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true) expect(@dummy.method(:foo).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.public_methods).to include(:foo) end it 'instruments all protected class methods' do @@ -249,13 +255,16 @@ RSpec.describe Gitlab::Metrics::Instrumentation do expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true) expect(@dummy.method(:flaky).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.protected_methods).to include(:flaky) end - it 'instruments all private instance methods' do + it 'instruments all private class methods' do described_class.instrument_methods(@dummy) expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true) expect(@dummy.method(:buzz).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.private_methods).to include(:buzz) + expect(@dummy.private_methods).to include(:wat) end it 'only instruments methods directly defined in the module' do @@ -290,6 +299,7 @@ RSpec.describe Gitlab::Metrics::Instrumentation do expect(described_class.instrumented?(@dummy)).to eq(true) expect(@dummy.new.method(:bar).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.public_instance_methods).to include(:bar) end it 'instruments all protected instance methods' do @@ -297,6 +307,7 @@ RSpec.describe Gitlab::Metrics::Instrumentation do expect(described_class.instrumented?(@dummy)).to eq(true) expect(@dummy.new.method(:chaf).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.protected_instance_methods).to include(:chaf) end it 'instruments all private instance methods' do @@ -304,6 +315,7 @@ RSpec.describe Gitlab::Metrics::Instrumentation do expect(described_class.instrumented?(@dummy)).to eq(true) expect(@dummy.new.method(:wadus).source_location.first).to match(/instrumentation\.rb/) + expect(@dummy.private_instance_methods).to include(:wadus) end it 'only instruments methods directly defined in the module' do diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 9d63d74a6cc..9d2172d7572 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::PipelineArtifact, type: :model do - let_it_be(:coverage_report) { create(:ci_pipeline_artifact) } + let(:coverage_report) { create(:ci_pipeline_artifact) } describe 'associations' do it { is_expected.to belong_to(:pipeline) } @@ -44,24 +44,6 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end - describe '#set_size' do - subject { create(:ci_pipeline_artifact) } - - context 'when file is being created' do - it 'sets the size' do - expect(subject.size).to eq(85) - end - end - - context 'when file is being updated' do - it 'updates the size' do - subject.update!(file: fixture_file_upload('spec/fixtures/dk.png')) - - expect(subject.size).to eq(1062) - end - end - end - describe 'file is being stored' do subject { create(:ci_pipeline_artifact) } @@ -78,5 +60,31 @@ RSpec.describe Ci::PipelineArtifact, type: :model do it_behaves_like 'mounted file in object store' end end + + context 'when file contains multi-byte characters' do + let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_multibyte_characters) } + + it 'sets the size in bytesize' do + expect(coverage_report_multibyte.size).to eq(12) + end + end + end + + describe '.has_code_coverage?' do + subject { Ci::PipelineArtifact.has_code_coverage? } + + context 'when pipeline artifact has a code coverage' do + let!(:pipeline_artifact) { create(:ci_pipeline_artifact) } + + it 'returns true' do + expect(subject).to be_truthy + end + end + + context 'when pipeline artifact does not have a code coverage' do + it 'returns false' do + expect(subject).to be_falsey + end + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b4e80fa7588..54dad2e1840 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2948,6 +2948,38 @@ RSpec.describe Ci::Pipeline, :mailer do end end + describe '#has_coverage_reports?' do + subject { pipeline.has_coverage_reports? } + + context 'when pipeline has builds with coverage reports' do + before do + create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + end + + context 'when pipeline status is running' do + let(:pipeline) { create(:ci_pipeline, :running, project: project) } + + it { expect(subject).to be_falsey } + end + + context 'when pipeline status is success' do + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_truthy } + end + end + + context 'when pipeline does not have builds with coverage reports' do + before do + create(:ci_build, :artifacts, pipeline: pipeline, project: project) + end + + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_falsey } + end + end + describe '#test_report_summary' do subject { pipeline.test_report_summary } diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 4170bf595f0..1de68070d0c 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -482,4 +482,17 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to contain_exactly(*tags) } end end + + describe 'plan_limits' do + Packages::Package.package_types.keys.without('composer').each do |pt| + context "File size limits for #{pt}" do + let(:package) { create("#{pt}_package") } + + it "plan_limits includes column #{pt}_max_file_size" do + expect { package.project.actual_limits.send("#{pt}_max_file_size") } + .not_to raise_error(NoMethodError) + end + end + end + end end diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb index 95798b060f1..738e4977cb5 100644 --- a/spec/requests/api/conan_packages_spec.rb +++ b/spec/requests/api/conan_packages_spec.rb @@ -681,6 +681,18 @@ RSpec.describe API::ConanPackages do let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"} shared_examples 'uploads a package file' do + context 'file size above maximum limit' do + before do + params['file.size'] = project.actual_limits.conan_max_file_size + 1 + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'with object storage disabled' do context 'without a file from workhorse' do let(:params) { { file: nil } } diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index 1891300dace..1d38bb39d59 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -32,9 +32,7 @@ RSpec.describe 'Adding an AwardEmoji' do context 'when the user does not have permission' do it_behaves_like 'a mutation that does not create an AwardEmoji' - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when the user has permission' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb index 665b511abb8..c6e8800de1f 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -33,9 +33,7 @@ RSpec.describe 'Removing an AwardEmoji' do shared_examples 'a mutation that does not authorize the user' do it_behaves_like 'a mutation that does not destroy an AwardEmoji' - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when the current_user does not own the award emoji' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index ab4a213fde3..2df59ce97ca 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -143,8 +143,6 @@ RSpec.describe 'Toggling an AwardEmoji' do context 'when the user does not have permission' do it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end end diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb index 8a6d2cb3994..8e24e053211 100644 --- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb @@ -15,8 +15,7 @@ RSpec.describe 'Update of an existing board list' do let(:mutation_response) { graphql_mutation_response(:update_board_list) } context 'the user is not allowed to read board lists' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end before do diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb index 082b445bf3e..fc09f57a389 100644 --- a/spec/requests/api/graphql/mutations/branches/create_spec.rb +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -15,8 +15,7 @@ RSpec.describe 'Creation of a new branch' do let(:mutation_response) { graphql_mutation_response(:create_branch) } context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a branch' do diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb index 9e4a96700bb..ac4fa7cfe83 100644 --- a/spec/requests/api/graphql/mutations/commits/create_spec.rb +++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb @@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new commit' do let(:mutation_response) { graphql_mutation_response(:commit_create) } context 'the user is not allowed to create a commit' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a commit' do diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb index 457c37e900b..450996bf76b 100644 --- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -20,8 +20,7 @@ RSpec.describe 'Toggling the resolve status of a discussion' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permission' do diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb index f1d55430e02..4989d096925 100644 --- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb @@ -32,12 +32,7 @@ RSpec.describe 'Setting an issue as locked' do end context 'when the user is not allowed to update the issue' do - it 'returns an error' do - error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" - post_graphql_mutation(mutation, current_user: current_user) - - expect(graphql_errors).to include(a_hash_including('message' => error)) - end + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user is allowed to update the issue' do diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index fd983c683be..af52f9d57a3 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -20,8 +20,7 @@ RSpec.describe 'Update of an existing issue' do let(:mutation_response) { graphql_mutation_response(:update_issue) } context 'the user is not allowed to update issue' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to update issue' do diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb index 9297ca054c7..bf759521dc0 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb @@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new merge request' do let(:mutation_response) { graphql_mutation_response(:merge_request_create) } context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end context 'when user has permissions to create a merge request' do diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb index 6002a5b5b9d..49f09fadfea 100644 --- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb @@ -21,8 +21,7 @@ RSpec.describe 'Destroying a Note' do context 'when the user does not have permission' do let(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not destroy the Note' do expect do diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index 463a872d95d..0c00906d6bf 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -59,8 +59,7 @@ RSpec.describe 'Updating an image DiffNote' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not update the DiffNote' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index 0d93afe9434..5a92ffe61b8 100644 --- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -22,8 +22,7 @@ RSpec.describe 'Updating a Note' do context 'when the user does not have permission' do let_it_be(:current_user) { create(:user) } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' it 'does not update the Note' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb index ed5552f3e30..705ef28ffd4 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb @@ -59,7 +59,6 @@ RSpec.describe 'Marking all todos done' do context 'when user is not logged in' do let(:current_user) { nil } - it_behaves_like 'a mutation that returns top-level errors', - errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + it_behaves_like 'a mutation that returns a top-level access error' end end diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb index c1232500d79..8bf8b96aff5 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -63,14 +63,11 @@ RSpec.describe 'Marking todos done' do context 'when todo does not belong to requesting user' do let(:input) { { id: other_user_todo.to_global_id.to_s } } - let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' } - it 'contains the expected error' do - post_graphql_mutation(mutation, current_user: current_user) + it_behaves_like 'a mutation that returns a top-level access error' - errors = json_response['errors'] - expect(errors).not_to be_blank - expect(errors.first['message']).to eq(access_error) + it 'results in the correct todo states' do + post_graphql_mutation(mutation, current_user: current_user) expect(todo1.reload.state).to eq('pending') expect(todo2.reload.state).to eq('done') diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index 0797961f65f..8451dcdf587 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -63,14 +63,11 @@ RSpec.describe 'Restoring Todos' do context 'when todo does not belong to requesting user' do let(:input) { { id: other_user_todo.to_global_id.to_s } } - let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' } - it 'contains the expected error' do - post_graphql_mutation(mutation, current_user: current_user) + it_behaves_like 'a mutation that returns a top-level access error' - errors = json_response['errors'] - expect(errors).not_to be_blank - expect(errors.first['message']).to eq(access_error) + it 'results in the correct todo states' do + post_graphql_mutation(mutation, current_user: current_user) expect(todo1.reload.state).to eq('done') expect(todo2.reload.state).to eq('pending') diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index b9351308545..99588694bab 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -528,6 +528,18 @@ RSpec.describe API::MavenPackages do context 'when params from workhorse are correct' do let(:params) { { file: file_upload } } + context 'file size is too large' do + it 'rejects the request' do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.maven_max_file_size + 1) + end + + upload_file_with_token(params) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + it 'rejects a malicious request' do put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2f.ssh%2fauthorized_keys"), params: params, headers: headers_with_token diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb index ab537a61058..37170592b60 100644 --- a/spec/requests/api/nuget_packages_spec.rb +++ b/spec/requests/api/nuget_packages_spec.rb @@ -220,6 +220,18 @@ RSpec.describe API::NugetPackages do it_behaves_like 'rejects nuget access with unknown project id' it_behaves_like 'rejects nuget access with invalid project id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end end end diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index e2cfd87b507..2c33db45e93 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -185,6 +185,18 @@ RSpec.describe API::PypiPackages do it_behaves_like 'deploy token for package uploads' it_behaves_like 'rejects PyPI access with unknown project id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.pypi_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end end describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do diff --git a/spec/services/ci/pipelines/create_artifact_service_spec.rb b/spec/services/ci/pipelines/create_artifact_service_spec.rb new file mode 100644 index 00000000000..d5e9cf83a6d --- /dev/null +++ b/spec/services/ci/pipelines/create_artifact_service_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Pipelines::CreateArtifactService do + describe '#execute' do + subject { described_class.new.execute(pipeline) } + + context 'when pipeline has coverage reports' do + let(:pipeline) { create(:ci_pipeline, :with_coverage_reports) } + + context 'when pipeline is finished' do + it 'creates a pipeline artifact' do + subject + + expect(Ci::PipelineArtifact.count).to eq(1) + end + + it 'persists the default file name' do + subject + + file = Ci::PipelineArtifact.first.file + + expect(file.filename).to eq('code_coverage.json') + end + + it 'sets expire_at to 1 week' do + freeze_time do + subject + + pipeline_artifact = Ci::PipelineArtifact.first + + expect(pipeline_artifact.expire_at).to eq(1.week.from_now) + end + end + end + + context 'when feature is disabled' do + it 'does not create a pipeline artifact' do + stub_feature_flags(coverage_report_view: false) + + subject + + expect(Ci::PipelineArtifact.count).to eq(0) + end + end + + context 'when pipeline artifact has already been created' do + it 'do not raise an error and do not persist the same artifact twice' do + expect { 2.times { described_class.new.execute(pipeline) } }.not_to raise_error(ActiveRecord::RecordNotUnique) + + expect(Ci::PipelineArtifact.count).to eq(1) + end + end + end + + context 'when pipeline is running and coverage report does not exist' do + let(:pipeline) { create(:ci_pipeline, :running) } + + it 'does not persist data' do + subject + + expect(Ci::PipelineArtifact.count).to eq(0) + end + end + end +end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 5a245415b32..497c7f45ad0 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -181,17 +181,24 @@ RSpec.describe Ci::RetryBuildService do service.execute(build) end - context 'when there are subsequent builds that are skipped' do + context 'when there are subsequent processables that are skipped' do let!(:subsequent_build) do create(:ci_build, :skipped, stage_idx: 2, pipeline: pipeline, stage: 'deploy') end - it 'resumes pipeline processing in a subsequent stage' do + let!(:subsequent_bridge) do + create(:ci_bridge, :skipped, stage_idx: 2, + pipeline: pipeline, + stage: 'deploy') + end + + it 'resumes pipeline processing in the subsequent stage' do service.execute(build) expect(subsequent_build.reload).to be_created + expect(subsequent_bridge.reload).to be_created end end diff --git a/spec/services/discussions/capture_diff_note_position_service_spec.rb b/spec/services/discussions/capture_diff_note_position_service_spec.rb index 0913ddd8ef2..11614ccfd55 100644 --- a/spec/services/discussions/capture_diff_note_position_service_spec.rb +++ b/spec/services/discussions/capture_diff_note_position_service_spec.rb @@ -29,18 +29,33 @@ RSpec.describe Discussions::CaptureDiffNotePositionService do end end - context 'when position tracer returned nil position' do + context 'when position tracer returned position' do let!(:note) { create(:diff_note_on_merge_request) } let(:paths) { ['files/any_file.txt'] } - it 'does not create diff note position' do + before do expect(note.noteable).to receive(:merge_ref_head).and_return(double.as_null_object) expect_next_instance_of(Gitlab::Diff::PositionTracer) do |tracer| - expect(tracer).to receive(:trace).and_return({ position: nil }) + expect(tracer).to receive(:trace).and_return({ position: position }) end + end - expect(subject.execute(note.discussion)).to eq(nil) - expect(note.diff_note_positions).to be_empty + context 'which is nil' do + let(:position) { nil } + + it 'does not create diff note position' do + expect(subject.execute(note.discussion)).to eq(nil) + expect(note.diff_note_positions).to be_empty + end + end + + context 'which does not have a corresponding line' do + let(:position) { double(line_code: nil) } + + it 'does not create diff note position' do + expect(subject.execute(note.discussion)).to eq(nil) + expect(note.diff_note_positions).to be_empty + end end end end diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index c1391746f52..895c46735ed 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -61,6 +61,15 @@ RSpec.describe Packages::Npm::CreatePackageService do it { expect(subject[:message]).to be 'Package already exists.' } end + context 'file size above maximum limit' do + before do + params['_attachments']["#{package_name}-#{version}.tgz"]['length'] = project.actual_limits.npm_max_file_size + 1 + end + + it { expect(subject[:http_status]).to eq 400 } + it { expect(subject[:message]).to be 'File is too large.' } + end + context 'with incorrect namespace' do let(:package_name) { '@my_other_namespace/my-app' } diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index a64871ca75b..641ed24207e 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -247,8 +247,9 @@ module TestEnv 'GitLab Workhorse', install_dir: workhorse_dir, version: Gitlab::Workhorse.version, - task: "gitlab:workhorse:install[#{install_workhorse_args}]" - ) + task: "gitlab:workhorse:install[#{install_workhorse_args}]") do + Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil) + end end def workhorse_dir @@ -259,6 +260,14 @@ module TestEnv host = "[#{host}]" if host.include?(':') listen_addr = [host, port].join(':') + config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir) + + # This should be set up in setup_workhorse, but since + # component_needs_update? only checks that versions are consistent, + # we need to ensure the config file exists. This line can be removed + # later after a new Workhorse version is updated. + Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil) unless File.exist?(config_path) + workhorse_pid = spawn( { 'PATH' => "#{ENV['PATH']}:#{workhorse_dir}" }, File.join(workhorse_dir, 'gitlab-workhorse'), @@ -266,10 +275,7 @@ module TestEnv '-documentRoot', Rails.root.join('public').to_s, '-listenAddr', listen_addr, '-secretPath', Gitlab::Workhorse.secret_path.to_s, - # TODO: Needed for workhorse + redis features. - # https://gitlab.com/gitlab-org/gitlab/-/issues/209245 - # - # '-config', '', + '-config', config_path, '-logFile', 'log/workhorse-test.log', '-logFormat', 'structured', '-developmentMode' # to serve assets and rich error messages diff --git a/spec/workers/ci/pipelines/create_artifact_worker_spec.rb b/spec/workers/ci/pipelines/create_artifact_worker_spec.rb new file mode 100644 index 00000000000..31d2c4e9559 --- /dev/null +++ b/spec/workers/ci/pipelines/create_artifact_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Pipelines::CreateArtifactWorker do + describe '#perform' do + subject { described_class.new.perform(pipeline_id) } + + context 'when pipeline exists' do + let(:pipeline) { create(:ci_pipeline) } + let(:pipeline_id) { pipeline.id } + + it 'calls pipeline report result service' do + expect_next_instance_of(::Ci::Pipelines::CreateArtifactService) do |create_artifact_service| + expect(create_artifact_service).to receive(:execute) + end + + subject + end + end + + context 'when pipeline does not exist' do + let(:pipeline_id) { non_existing_record_id } + + it 'does not call pipeline create artifact service' do + expect(Ci::Pipelines::CreateArtifactService).not_to receive(:execute) + + subject + end + end + end +end |