Commit 63ddc629 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 02b70495 798f5542
......@@ -287,7 +287,7 @@ db:migrate-from-v12.10.0:
- '[[ -d "ee/" ]] || export TAG_TO_CHECKOUT="v12.10.0"'
- git fetch https://gitlab.com/gitlab-org/$PROJECT_TO_CHECKOUT.git $TAG_TO_CHECKOUT
- git checkout -f FETCH_HEAD
- sed -i -e "s/gem 'grpc', '~> 1.24.0'/gem 'grpc', '~> 1.30.2'/" Gemfile # Update gRPC for Ruby 2.7
- sed -i -e "s/gem 'grpc', '~> 1.24.0'/gem 'grpc', '~> 1.30.2'/" Gemfile # Update gRPC for Ruby 2.7
- sed -i -e "s/gem 'google-protobuf', '~> 3.8.0'/gem 'google-protobuf', '~> 3.12.0'/" Gemfile
- bundle update google-protobuf grpc bootsnap
- bundle install $BUNDLE_INSTALL_FLAGS
......@@ -563,7 +563,7 @@ rspec-ee system pg12 geo:
# EE: Canonical MR pipelines
rspec fail-fast:
extends:
- .rspec-ee-base-pg11 # This job also runs EE spec which needs elasticsearch
- .rspec-ee-base-pg11 # This job also runs EE spec which needs elasticsearch
- .rails:rules:rspec fail-fast
stage: test
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
......
......@@ -103,8 +103,11 @@
- ".gitlab/ci/build-images.gitlab-ci.yml"
- ".gitlab/ci/qa.gitlab-ci.yml"
.yaml-patterns: &yaml-patterns
- "**/*.yml"
.yaml-lint-patterns: &yaml-lint-patterns
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*.yml"
- "lib/gitlab/ci/templates/**/*.yml"
- "{,ee/}changelogs/**/*.yml"
.docs-patterns: &docs-patterns
- ".gitlab/route-map.yml"
......@@ -142,8 +145,8 @@
- "{,ee/}{,spec/}lib/{,ee/}gitlab/database{,_spec}.rb"
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration/**/*"
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration{,_spec}.rb"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
.backstage-patterns: &backstage-patterns
- "Dangerfile"
......@@ -771,7 +774,7 @@
.review:rules:review-performance:
rules:
- if: '$DAST_RUN == "true"' # Skip this job when DAST is run
- if: '$DAST_RUN == "true"' # Skip this job when DAST is run
when: never
- <<: *if-not-ee
when: never
......@@ -905,10 +908,10 @@
- <<: *if-dot-com-ee-schedule
changes: *code-backstage-patterns
##############
# YAML rules #
##############
.yaml:rules:
###################
# yaml-lint rules #
###################
.yaml-lint:rules:
rules:
- <<: *if-default-refs
changes: *yaml-patterns
changes: *yaml-lint-patterns
# Yamllint of *.yml for .gitlab-ci.yml.
# Yamllint of CI-related yaml and changelogs.
# This uses rules from project root `.yamllint`.
lint-ci-gitlab:
lint-yaml:
extends:
- .default-retry
- .yaml:rules
- .yaml-lint:rules
image: pipelinecomponents/yamllint:latest
stage: test
needs: []
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import '~/lib/utils/datetime_utility';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { formatTime } from '~/lib/utils/datetime_utility';
export default {
directives: {
......@@ -27,24 +27,7 @@ export default {
return this.finishedTime !== '';
},
durationFormatted() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
return formatTime(this.duration);
},
},
};
......
<script>
import { formatTime } from '~/lib/utils/datetime_utility';
import { s__, n__ } from '~/locale';
export default {
props: {
counts: {
......@@ -6,25 +9,44 @@ export default {
required: true,
},
},
computed: {
totalDuration() {
return formatTime(this.counts.totalDuration);
},
statistics() {
return [
{
title: s__('PipelineCharts|Total:'),
value: n__('1 pipeline', '%d pipelines', this.counts.total),
},
{
title: s__('PipelineCharts|Successful:'),
value: n__('1 pipeline', '%d pipelines', this.counts.success),
},
{
title: s__('PipelineCharts|Failed:'),
value: n__('1 pipeline', '%d pipelines', this.counts.failed),
},
{
title: s__('PipelineCharts|Success ratio:'),
value: `${this.counts.successRatio}%`,
},
{
title: s__('PipelineCharts|Total duration:'),
value: this.totalDuration,
},
];
},
},
};
</script>
<template>
<ul>
<li>
<span>{{ s__('PipelineCharts|Total:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.total) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Successful:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.success) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Failed:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.failed) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Success ratio:') }}</span>
<strong>{{ counts.successRatio }}%</strong>
</li>
<template v-for="({ title, value }, index) in statistics">
<li :key="index">
<span>{{ title }}</span>
<strong>{{ value }}</strong>
</li>
</template>
</ul>
</template>
......@@ -7,6 +7,7 @@ export default () => {
countsFailed,
countsSuccess,
countsTotal,
countsTotalDuration,
successRatio,
timesChartLabels,
timesChartValues,
......@@ -41,6 +42,7 @@ export default () => {
success: countsSuccess,
total: countsTotal,
successRatio,
totalDuration: countsTotalDuration,
},
timesChartData: {
labels: JSON.parse(timesChartLabels),
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import { GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'MrWidgetAuthor',
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
author: {
......@@ -16,11 +16,6 @@ export default {
required: false,
default: true,
},
showAuthorTooltip: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
authorUrl() {
......@@ -33,12 +28,7 @@ export default {
};
</script>
<template>
<a
:href="authorUrl"
:v-tooltip="showAuthorTooltip"
:title="author.name"
class="author-link inline"
>
<a v-gl-tooltip :href="authorUrl" :title="author.name" class="author-link inline">
<img :src="avatarUrl" class="avatar avatar-inline s16" />
<span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a>
......
......@@ -193,6 +193,7 @@ class Projects::PipelinesController < Projects::ApplicationController
@counts[:total] = @project.all_pipelines.count(:all)
@counts[:success] = @project.all_pipelines.success.count(:all)
@counts[:failed] = @project.all_pipelines.failed.count(:all)
@counts[:total_duration] = @project.all_pipelines.total_duration
end
def test_report
......
......@@ -12,12 +12,25 @@ module Types
field :self_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the release'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page filtered by this release'
field :issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page filtered by this release'
field :edit_url, GraphQL::STRING_TYPE, null: true,
description: "HTTP URL of the release's edit page",
authorize: :update_release
field :open_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`'
field :merged_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`'
field :closed_merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`'
field :open_issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`'
field :closed_issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true, method: :open_merge_requests_url,
description: 'HTTP URL of the merge request page filtered by this release',
deprecated: { reason: 'Use `open_merge_requests_url`', milestone: '13.6' }
field :issues_url, GraphQL::STRING_TYPE, null: true, method: :open_issues_url,
description: 'HTTP URL of the issues page filtered by this release',
deprecated: { reason: 'Use `open_issues_url`', milestone: '13.6' }
end
end
......@@ -926,7 +926,7 @@ module Ci
def accessibility_reports
Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports|
builds.latest.with_reports(Ci::JobArtifact.accessibility_reports).each do |build|
latest_report_builds(Ci::JobArtifact.accessibility_reports).each do |build|
build.collect_accessibility_reports!(accessibility_reports)
end
end
......
......@@ -23,18 +23,36 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
project_release_url(project, release)
end
def merge_requests_url
def open_merge_requests_url
return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs)
end
def issues_url
def merged_merge_requests_url
return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs(state: 'merged'))
end
def closed_merge_requests_url
return unless release_mr_issue_urls_available?
project_merge_requests_url(project, params_for_issues_and_mrs(state: 'closed'))
end
def open_issues_url
return unless release_mr_issue_urls_available?
project_issues_url(project, params_for_issues_and_mrs)
end
def closed_issues_url
return unless release_mr_issue_urls_available?
project_issues_url(project, params_for_issues_and_mrs(state: 'closed'))
end
def edit_url
return unless release_edit_page_available?
......@@ -59,8 +77,8 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
can?(current_user, :download_code, project)
end
def params_for_issues_and_mrs
{ scope: 'all', state: 'opened', release_tag: release.tag }
def params_for_issues_and_mrs(state: 'opened')
{ scope: 'all', state: state, release_tag: release.tag }
end
def release_mr_issue_urls_available?
......
---
title: Add Total Duration to CI/CD Analytics Page
merge_request: 44863
author: Kev @KevSlashNull
type: added
---
title: Add links to GraphQL release object for searching related issues and merge
requests
merge_request: 46161
author:
type: added
---
title: Migrate tooltip in app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
merge_request: 46034
author:
type: other
......@@ -16855,20 +16855,45 @@ type ReleaseEvidenceEdge {
}
type ReleaseLinks {
"""
HTTP URL of the issues page, filtered by this release and `state=closed`
"""
closedIssuesUrl: String
"""
HTTP URL of the merge request page , filtered by this release and `state=closed`
"""
closedMergeRequestsUrl: String
"""
HTTP URL of the release's edit page
"""
editUrl: String
"""
HTTP URL of the issues page filtered by this release
HTTP URL of the issues page filtered by this release. Deprecated in 13.6: Use `open_issues_url`
"""
issuesUrl: String @deprecated(reason: "Use `open_issues_url`. Deprecated in 13.6")
"""
HTTP URL of the merge request page filtered by this release. Deprecated in 13.6: Use `open_merge_requests_url`
"""
mergeRequestsUrl: String @deprecated(reason: "Use `open_merge_requests_url`. Deprecated in 13.6")
"""
HTTP URL of the merge request page , filtered by this release and `state=merged`
"""
mergedMergeRequestsUrl: String
"""
HTTP URL of the issues page, filtered by this release and `state=open`
"""
issuesUrl: String
openIssuesUrl: String
"""
HTTP URL of the merge request page filtered by this release
HTTP URL of the merge request page, filtered by this release and `state=open`
"""
mergeRequestsUrl: String
openMergeRequestsUrl: String
"""
HTTP URL of the release
......
......@@ -48541,6 +48541,34 @@
"name": "ReleaseLinks",
"description": null,
"fields": [
{
"name": "closedIssuesUrl",
"description": "HTTP URL of the issues page, filtered by this release and `state=closed`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "closedMergeRequestsUrl",
"description": "HTTP URL of the merge request page , filtered by this release and `state=closed`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "editUrl",
"description": "HTTP URL of the release's edit page",
......@@ -48557,7 +48585,35 @@
},
{
"name": "issuesUrl",
"description": "HTTP URL of the issues page filtered by this release",
"description": "HTTP URL of the issues page filtered by this release. Deprecated in 13.6: Use `open_issues_url`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use `open_issues_url`. Deprecated in 13.6"
},
{
"name": "mergeRequestsUrl",
"description": "HTTP URL of the merge request page filtered by this release. Deprecated in 13.6: Use `open_merge_requests_url`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": true,
"deprecationReason": "Use `open_merge_requests_url`. Deprecated in 13.6"
},
{
"name": "mergedMergeRequestsUrl",
"description": "HTTP URL of the merge request page , filtered by this release and `state=merged`",
"args": [
],
......@@ -48570,8 +48626,22 @@
"deprecationReason": null
},
{
"name": "mergeRequestsUrl",
"description": "HTTP URL of the merge request page filtered by this release",
"name": "openIssuesUrl",
"description": "HTTP URL of the issues page, filtered by this release and `state=open`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "openMergeRequestsUrl",
"description": "HTTP URL of the merge request page, filtered by this release and `state=open`",
"args": [
],
......@@ -2230,9 +2230,14 @@ Evidence for a release.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `closedIssuesUrl` | String | HTTP URL of the issues page, filtered by this release and `state=closed` |
| `closedMergeRequestsUrl` | String | HTTP URL of the merge request page , filtered by this release and `state=closed` |
| `editUrl` | String | HTTP URL of the release's edit page |
| `issuesUrl` | String | HTTP URL of the issues page filtered by this release |
| `mergeRequestsUrl` | String | HTTP URL of the merge request page filtered by this release |
| `issuesUrl` **{warning-solid}** | String | **Deprecated:** Use `open_issues_url`. Deprecated in 13.6 |
| `mergeRequestsUrl` **{warning-solid}** | String | **Deprecated:** Use `open_merge_requests_url`. Deprecated in 13.6 |
| `mergedMergeRequestsUrl` | String | HTTP URL of the merge request page , filtered by this release and `state=merged` |
| `openIssuesUrl` | String | HTTP URL of the issues page, filtered by this release and `state=open` |
| `openMergeRequestsUrl` | String | HTTP URL of the merge request page, filtered by this release and `state=open` |
| `selfUrl` | String | HTTP URL of the release |
### ReleaseSource
......
......@@ -198,47 +198,33 @@ Following you'll find some general common practices you will find as part of our
When it comes to querying DOM elements in your tests, it is best to uniquely and semantically target
the element.
Preferentially, this is done by targeting text the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro).
Preferentially, this is done by targeting what the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro).
When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/dom-testing-library/api-queries#byrole)
as these enforce accessibility best practices as well. The examples below demonstrate the order of preference.
Sometimes this cannot be done feasibly. In these cases, adding test attributes to simplify the
selectors might be the best option.
When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage
rather than dealing with the complexity of a child component's behavior.
Sometimes, neither of the above are feasible. In these cases, adding test attributes to simplify the selectors might be the best option. A list of
possible selectors include:
- A semantic attribute like `name` (also verifies that `name` was setup properly)
- A `data-testid` attribute ([recommended by maintainers of `@vue/test-utils`](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465))
- a Vue `ref` (if using `@vue/test-utils`)
```javascript
import { mount, shallowMount } from '@vue/test-utils'
import { getByRole, getByText } from '@testing-library/dom'
let wrapper
let el
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(Component)
el = wrapper.vm.$el // reference to the container element
}
beforeEach(() => {
createComponent()
})
// In this example, `wrapper` is a `@vue/test-utils` wrapper returned from `mount` or `shallowMount`.
it('exists', () => {
// Best
// NOTE: both mount and shallowMount work as long as a DOM element is available
// Finds a properly formatted link with an accessible name of "Click Me"
getByRole(el, 'link', { name: /Click Me/i })
getByRole(el, 'link', { name: 'Click Me' })
// Finds any element with the text "Click Me"
getByText(el, 'Click Me')
// Regex is also available
getByText(el, /Click Me/i)
// Good
// Best (especially for integration tests)
getByRole(wrapper.element, 'link', { name: /Click Me/i })
getByRole(wrapper.element, 'link', { name: 'Click Me' })
getByText(wrapper.element, 'Click Me')
getByText(wrapper.element, /Click Me/i)
// Good (especially for unit tests)
wrapper.find(FooComponent);
wrapper.find('input[name=foo]');
wrapper.find('[data-testid="foo"]');
wrapper.find({ ref: 'foo'});
......@@ -249,14 +235,6 @@ it('exists', () => {
wrapper.find('.qa-foo-component');
wrapper.find('[data-qa-selector="foo"]');
});
// Good
it('exists', () => {
wrapper.find(FooComponent);
wrapper.find('input[name=foo]');
wrapper.find('[data-testid="foo"]');
wrapper.find({ ref: 'foo'});
});
```
It is not recommended that you add `.js-*` classes just for testing purposes. Only do this if there are no other feasible options available.
......
......@@ -30,69 +30,55 @@ module Mutations
def resolve(full_path:, dast_site_profile_id:, **args)
project = authorized_find_project!(full_path: full_path)
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
dast_site_profile_id = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(dast_site_profile_id)
dast_site_profile = find_dast_site_profile(project, dast_site_profile_id)
dast_scanner_profile = find_dast_scanner_profile(project, args[:dast_scanner_profile_id])
response = create_on_demand_dast_scan(project, dast_site_profile, dast_scanner_profile)
dast_site_profile = find_dast_site_profile(project: project, dast_site_profile_id: dast_site_profile_id)
dast_site = dast_site_profile.dast_site
dast_scanner_profile = find_dast_scanner_profile(project: project, dast_scanner_profile_id: args[:dast_scanner_profile_id])
result = ::Ci::RunDastScanService.new(
project, current_user
).execute(
branch: project.default_branch,
target_url: dast_site.url,
spider_timeout: dast_scanner_profile&.spider_timeout,
target_timeout: dast_scanner_profile&.target_timeout,
full_scan_enabled: dast_scanner_profile&.full_scan_enabled?,
use_ajax_spider: dast_scanner_profile&.use_ajax_spider,
show_debug_messages: dast_scanner_profile&.show_debug_messages
)
if result.success?
success_response(project: project, pipeline: result.payload)
else
error_response(result)
end
return { errors: response.errors } if response.error?
{ errors: [], pipeline_url: response.payload.fetch(:pipeline_url) }
end
private
# rubocop: disable CodeReuse/ActiveRecord
def find_dast_site_profile(project:, dast_site_profile_id:)
def find_dast_site_profile(project, dast_site_profile_id)
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
dast_site_profile_id = ::Types::GlobalIDType[::DastSiteProfile].coerce_isolated_input(dast_site_profile_id)
DastSiteProfilesFinder.new(project_id: project.id, id: dast_site_profile_id.model_id)
.execute
.first!
end
# rubocop: enable CodeReuse/ActiveRecord
def find_dast_scanner_profile(project:, dast_scanner_profile_id:)
# rubocop: disable CodeReuse/ActiveRecord
def find_dast_scanner_profile(project, dast_scanner_profile_id)
return unless dast_scanner_profile_id
# TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
dast_scanner_profile_id = ::Types::GlobalIDType[::DastScannerProfile]
.coerce_isolated_input(dast_scanner_profile_id)
.coerce_isolated_input(dast_scanner_profile_id)
project
.dast_scanner_profiles
.find(dast_scanner_profile_id.model_id)
end
def success_response(project:, pipeline:)
pipeline_url = Rails.application.routes.url_helpers.project_pipeline_url(
project,
pipeline
)
{
errors: [],
pipeline_url: pipeline_url
}
DastScannerProfilesFinder.new(
project_ids: [project.id],
ids: [dast_scanner_profile_id.model_id]
).execute.first!
end
# rubocop: enable CodeReuse/ActiveRecord
def error_response(result)
{ errors: result.errors }
def create_on_demand_dast_scan(project, dast_site_profile, dast_scanner_profile)
::DastOnDemandScans::CreateService.new(
container: project,
current_user: current_user,
params: {
dast_site_profile: dast_site_profile,
dast_scanner_profile: dast_scanner_profile
}
).execute
end
end
end
......
......@@ -107,7 +107,7 @@ module EE
reports_scope = report_types.empty? ? ::Ci::JobArtifact.security_reports : ::Ci::JobArtifact.security_reports(file_types: report_types)
::Gitlab::Ci::Reports::Security::Reports.new(self).tap do |security_reports|
builds.latest.with_reports(reports_scope).each do |build|
latest_report_builds(reports_scope).each do |build|
build.collect_security_reports!(security_reports)
end
end
......@@ -115,7 +115,7 @@ module EE
def license_scanning_report
::Gitlab::Ci::Reports::LicenseScanning::Report.new.tap do |license_management_report|
builds.latest.with_reports(::Ci::JobArtifact.license_scanning_reports).each do |build|
latest_report_builds(::Ci::JobArtifact.license_scanning_reports).each do |build|
build.collect_license_scanning_reports!(license_management_report)
end
end
......@@ -123,10 +123,10 @@ module EE
def dependency_list_report
::Gitlab::Ci::Reports::DependencyList::Report.new.tap do |dependency_list_report|
builds.latest.with_reports(::Ci::JobArtifact.dependency_list_reports).each do |build|
latest_report_builds(::Ci::JobArtifact.dependency_list_reports).each do |build|
build.collect_dependency_list_reports!(dependency_list_report)
end
builds.latest.with_reports(::Ci::JobArtifact.license_scanning_reports).each do |build|
latest_report_builds(::Ci::JobArtifact.license_scanning_reports).each do |build|
build.collect_licenses_for_dependency_list!(dependency_list_report)
end
end
......@@ -134,7 +134,7 @@ module EE
def metrics_report
::Gitlab::Ci::Reports::Metrics::Report.new.tap do |metrics_report|
builds.latest.with_reports(::Ci::JobArtifact.metrics_reports).each do |build|
latest_report_builds(::Ci::JobArtifact.metrics_reports).each do |build|
build.collect_metrics_reports!(metrics_report)
end
end
......@@ -168,7 +168,7 @@ module EE
end
def license_scan_completed?
builds.latest.with_reports(::Ci::JobArtifact.license_scanning_reports).exists?
latest_report_builds(::Ci::JobArtifact.license_scanning_reports).exists?
end
def can_store_security_reports?
......
# frozen_string_literal: true
module DastOnDemandScans
class CreateService < BaseContainerService
def execute
return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
return ServiceResponse.error(message: 'Cannot run active scan against unvalidated target') unless active_scan_allowed?
create_pipeline
rescue KeyError => err
ServiceResponse.error(message: err.message.capitalize)
end
private
def allowed?
container.feature_available?(:security_on_demand_scans)
end
def active_scan_allowed?
return true unless dast_scanner_profile&.full_scan_enabled?
dast_site_validation = DastSiteValidationsFinder.new(
project_id: container.id,
state: :passed,
url_base: url_base
).execute.first
dast_site_validation.present?
end
def dast_site
@dast_site ||= params.fetch(:dast_site_profile).dast_site
end
def dast_scanner_profile
@dast_scanner_profile ||= params[:dast_scanner_profile]
end
def url_base
@url_base ||= DastSiteValidation.get_normalized_url_base(dast_site.url)
end
def default_config
{
branch: container.default_branch,
target_url: dast_site.url
}
end
def scanner_profile_config
return {} unless dast_scanner_profile
{
spider_timeout: dast_scanner_profile.spider_timeout,
target_timeout: dast_scanner_profile.target_timeout,
full_scan_enabled: dast_scanner_profile.full_scan_enabled?,
use_ajax_spider: dast_scanner_profile.use_ajax_spider,
show_debug_messages: dast_scanner_profile.show_debug_messages
}
end
def success_response(pipeline)
pipeline_url = Rails.application.routes.url_helpers.project_pipeline_url(
container,
pipeline
)
ServiceResponse.success(
payload: {
pipeline: pipeline,
pipeline_url: pipeline_url
}
)
end
def create_pipeline
params = default_config.merge(scanner_profile_config)
result = ::Ci::RunDastScanService.new(container, current_user).execute(params)
return success_response(result.payload) if result.success?
result
end
end
end
- size_checker = project.repository_size_checker
- if size_checker.above_size_limit?
- if size_checker.above_size_limit? && !project&.namespace&.additional_repo_storage_by_namespace_enabled?
.gl-alert.gl-alert-warning.gl-display-none.gl-display-sm-block
= sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
= size_checker.error_message.above_size_limit_message
---
title: Reject active on-demand DAST scan when unvalidated
merge_request: 45423
author:
type: changed
......@@ -20,21 +20,42 @@ RSpec.describe ProjectsController do
subject { get :show, params: { namespace_id: public_project.namespace.path, id: public_project.path } }
it 'shows the over size limit warning message if above_size_limit' do
allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
expect(checker).to receive(:above_size_limit?).and_return(true)
context 'with additional_repo_storage_by_namespace_enabled? enabled' do
before do
allow_any_instance_of(EE::Namespace).to receive(:additional_repo_storage_by_namespace_enabled?).and_return(true)
end
allow(controller).to receive(:current_user).and_return(user)
subject
it 'does not show over size limit warning when above_size_limit' do
allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
expect(checker).to receive(:above_size_limit?).and_return(true)
end
subject
expect(response.body).to match(/The size of this repository.+exceeds the limit/)
expect(response.body).not_to match(/The size of this repository.+exceeds the limit/)
end
end
it 'does not show an over size warning if not above_size_limit' do
subject
context 'with additional_repo_storage_by_namespace_enabled? disabled' do
before do
allow_any_instance_of(EE::Namespace).to receive(:additional_repo_storage_by_namespace_enabled?).and_return(false)
end
expect(response.body).not_to match(/The size of this repository.+exceeds the limit/)
it 'shows the over size limit warning message if above_size_limit' do
allow_next_instance_of(Gitlab::RepositorySizeChecker) do |checker|
expect(checker).to receive(:above_size_limit?).and_return(true)
end
subject
expect(response.body).to match(/The size of this repository.+exceeds the limit/)
end
it 'does not show an over size warning if not above_size_limit' do
subject
expect(response.body).not_to match(/The size of this repository.+exceeds the limit/)
end
end
context 'namespace storage limit' do
......
......@@ -103,7 +103,7 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
end
context 'when dast_scanner_profile_id is provided' do
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, target_timeout: 200, spider_timeout: 5000, use_ajax_spider: true, show_debug_messages: true, scan_type: 'active') }
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, target_timeout: 200, spider_timeout: 5000, use_ajax_spider: true, show_debug_messages: true, scan_type: 'passive') }
let(:dast_scanner_profile_id) { dast_scanner_profile.to_global_id }
subject do
......@@ -133,6 +133,24 @@ RSpec.describe Mutations::DastOnDemandScans::Create do
subject
end
context 'when scan_type=active' do
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, scan_type: 'active') }
context 'when target is not validated' do
it 'communicates failure' do
expect(subject[:errors]).to include('Cannot run active scan against unvalidated target')
end
end
context 'when target is validated' do
it 'has no errors' do
create(:dast_site_validation, state: :passed, dast_site_token: create(:dast_site_token, project: project, url: dast_site_profile.dast_site.url))
expect(subject[:errors]).to be_empty
end
end
end
end
context 'when on demand scan licensed feature is not available' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastOnDemandScans::CreateService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:dast_site_profile) { create(:dast_site_profile, project: project) }
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project) }
subject do
described_class.new(
container: project,
current_user: user,
params: {
dast_site_profile: dast_site_profile,
dast_scanner_profile: dast_scanner_profile
}
).execute
end
describe 'execute' do
context 'when on demand scan licensed feature is not available' do
context 'when the user cannot run an on demand scan' do
it 'communicates failure' do
stub_licensed_features(security_on_demand_scans: false)
aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq('Insufficient permissions')
end
end
end
end
context 'when the feature is enabled' do
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when user can run an on demand scan' do
before do
project.add_developer(user)
end
it 'communicates success' do
expect(subject.status).to eq(:success)
end
it 'returns a pipeline and pipeline_url' do
aggregate_failures do
expect(subject.payload[:pipeline]).to be_a(Ci::Pipeline)
expect(subject.payload[:pipeline_url]).to be_a(String)
end
end
it 'delegates pipeline creation to Ci::RunDastScanService' do
expected_params = {
branch: 'master',
full_scan_enabled: false,
show_debug_messages: false,
spider_timeout: nil,
target_timeout: nil,
target_url: dast_site_profile.dast_site.url,
use_ajax_spider: false
}
service = double(Ci::RunDastScanService)
response = ServiceResponse.error(message: 'Stubbed response')
aggregate_failures do
expect(Ci::RunDastScanService).to receive(:new).and_return(service)
expect(service).to receive(:execute).with(expected_params).and_return(response)
end
subject
end
context 'when dast_scanner_profile is nil' do
let(:dast_scanner_profile) { nil }
it 'communicates success' do
expect(subject.status).to eq(:success)
end
end
context 'when target is not validated and an active scan is requested' do
let(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, scan_type: 'active') }
it 'communicates failure' do
aggregate_failures do
expect(subject.status).to eq(:error)
expect(subject.message).to eq('Cannot run active scan against unvalidated target')
end
end
end
end
end
end
end
......@@ -30,8 +30,8 @@ module API
expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do
expose :self_url, as: :self, expose_nil: false
expose :merge_requests_url, expose_nil: false
expose :issues_url, expose_nil: false
expose :open_merge_requests_url, as: :merge_requests_url, expose_nil: false
expose :open_issues_url, as: :issues_url, expose_nil: false
expose :edit_url, expose_nil: false
end
......
......@@ -160,9 +160,9 @@ include:
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
- template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
- template: Jobs/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml
- template: Jobs/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
- template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
- template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
- template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
- template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
......
......@@ -19402,6 +19402,9 @@ msgstr ""
msgid "PipelineCharts|Successful:"
msgstr ""
msgid "PipelineCharts|Total duration:"
msgstr ""
msgid "PipelineCharts|Total:"
msgstr ""
......
......@@ -11,7 +11,6 @@ exports[`StatisticsList matches the snapshot 1`] = `
4 pipelines
</strong>
</li>
<li>
<span>
Successful:
......@@ -21,7 +20,6 @@ exports[`StatisticsList matches the snapshot 1`] = `
2 pipelines
</strong>
</li>
<li>
<span>
Failed:
......@@ -31,7 +29,6 @@ exports[`StatisticsList matches the snapshot 1`] = `
2 pipelines
</strong>
</li>
<li>
<span>
Success ratio:
......@@ -41,5 +38,14 @@ exports[`StatisticsList matches the snapshot 1`] = `
50%
</strong>
</li>
<li>
<span>
Total duration:
</span>
<strong>
00:01:56
</strong>
</li>
</ul>
`;
......@@ -3,6 +3,7 @@ export const counts = {
success: 2,
total: 4,
successRatio: 50,
totalDuration: 116158,
};
export const timesChartData = {
......
......@@ -8,9 +8,14 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
it 'has the expected fields' do
expected_fields = %w[
selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
editUrl
mergeRequestsUrl
issuesUrl
editUrl
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
......@@ -12,6 +12,11 @@ RSpec.describe ReleasePresenter do
let(:release) { create(:release, project: project) }
let(:presenter) { described_class.new(release, current_user: user) }
let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
before do
project.add_developer(developer)
project.add_guest(guest)
......@@ -55,15 +60,63 @@ RSpec.describe ReleasePresenter do
subject { presenter.self_url }
it 'returns its own url' do
is_expected.to match /#{project_release_url(project, release)}/
is_expected.to eq(project_release_url(project, release))
end
end
describe '#open_merge_requests_url' do
subject { presenter.open_merge_requests_url }
it 'returns merge requests url with state=open' do
is_expected.to eq(project_merge_requests_url(project, opened_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end
end
describe '#merged_merge_requests_url' do
subject { presenter.merged_merge_requests_url }
it 'returns merge requests url with state=merged' do
is_expected.to eq(project_merge_requests_url(project, merged_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end
end
describe '#closed_merge_requests_url' do
subject { presenter.closed_merge_requests_url }
it 'returns merge requests url with state=closed' do
is_expected.to eq(project_merge_requests_url(project, closed_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
before do
stub_feature_flags(release_mr_issue_urls: false)
end
it { is_expected.to be_nil }
end
end
describe '#merge_requests_url' do
subject { presenter.merge_requests_url }
describe '#open_issues_url' do
subject { presenter.open_issues_url }
it 'returns merge requests url' do
is_expected.to match /#{project_merge_requests_url(project)}/
it 'returns issues url with state=open' do
is_expected.to eq(project_issues_url(project, opened_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
......@@ -75,11 +128,11 @@ RSpec.describe ReleasePresenter do
end
end
describe '#issues_url' do
subject { presenter.issues_url }
describe '#closed_issues_url' do
subject { presenter.closed_issues_url }
it 'returns merge requests url' do
is_expected.to match /#{project_issues_url(project)}/
it 'returns issues url with state=closed' do
is_expected.to eq(project_issues_url(project, closed_url_params))
end
context 'when release_mr_issue_urls feature flag is disabled' do
......@@ -95,7 +148,7 @@ RSpec.describe ReleasePresenter do
subject { presenter.edit_url }
it 'returns release edit url' do
is_expected.to match /#{edit_project_release_url(project, release)}/
is_expected.to eq(edit_project_release_url(project, release))
end
context 'when a user is not allowed to update a release' do
......
......@@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:link_filepath) { '/direct/asset/link/path' }
let_it_be(:released_at) { Time.now - 1.day }
let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) }
......@@ -180,6 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:release_fields) do
query_graphql_field(:links, nil, %{
selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
mergeRequestsUrl
issuesUrl
})
......@@ -190,8 +199,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
expect(data).to eq(
'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
'openMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
'openIssuesUrl' => project_issues_url(project, opened_url_params),
'closedIssuesUrl' => project_issues_url(project, closed_url_params),
'mergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, opened_url_params)
)
end
end
......
......@@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
let(:opened_url_params) { { state: 'opened', **base_url_params } }
let(:merged_url_params) { { state: 'merged', **base_url_params } }
let(:closed_url_params) { { state: 'closed', **base_url_params } }
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
......@@ -37,6 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
}
links {
selfUrl
openMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openIssuesUrl
closedIssuesUrl
mergeRequestsUrl
issuesUrl
}
......@@ -101,8 +111,13 @@ RSpec.describe 'Query.project(fullPath).releases()' do
},
'links' => {
'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
'openMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
'openIssuesUrl' => project_issues_url(project, opened_url_params),
'closedIssuesUrl' => project_issues_url(project, closed_url_params),
'mergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
'issuesUrl' => project_issues_url(project, opened_url_params)
}
)
end
......
# frozen_string_literal: true
# Expects the calling spec to define:
# - model_class
# - mounted_as
# - to_store
RSpec.shared_examples 'uploads migration worker' do
def perform(uploads, store = nil)
described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, store || to_store)
rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
# swallow
end
describe '.enqueue!' do
def enqueue!
described_class.enqueue!(uploads, model_class, mounted_as, to_store)
end
it 'is guarded by .sanity_check!' do
expect(described_class).to receive(:perform_async)
expect(described_class).to receive(:sanity_check!)
enqueue!
end
context 'sanity_check! fails' do
include_context 'sanity_check! fails'
it 'does not enqueue a job' do
expect(described_class).not_to receive(:perform_async)
expect { enqueue! }.to raise_error(described_class::SanityCheckError)
end
end
end
describe '.sanity_check!' do
shared_examples 'raises a SanityCheckError' do |expected_message|
let(:mount_point) { nil }
it do
expect { described_class.sanity_check!(uploads, model_class, mount_point) }
.to raise_error(described_class::SanityCheckError).with_message(expected_message)
end
end
context 'uploader types mismatch' do
let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
include_examples 'raises a SanityCheckError', /Multiple uploaders found/
end
context 'mount point not found' do
include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do
let(:mount_point) { :potato }
end
end
end
describe '#perform' do
shared_examples 'outputs correctly' do |success: 0, failures: 0|
total = success + failures
if success > 0
it 'outputs the reports' do
expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
perform(uploads)
end
end
if failures > 0
it 'outputs upload failures' do
expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
perform(uploads)
end
end
end
it_behaves_like 'outputs correctly', success: 10
it 'migrates files to remote storage' do
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
end
context 'reversed' do
let(:to_store) { ObjectStorage::Store::LOCAL }
before do
perform(uploads, ObjectStorage::Store::REMOTE)
end
it 'migrates files to local storage' do
expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(10)
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(10)
end
end
context 'migration is unsuccessful' do
before do
allow_any_instance_of(ObjectStorage::Concern)
.to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.')
end
it_behaves_like 'outputs correctly', failures: 10
end
end
end
RSpec.shared_context 'sanity_check! fails' do
before do
expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError)
end
end
......@@ -13,8 +13,103 @@ RSpec.describe ObjectStorage::MigrateUploadsWorker do
# swallow
end
# Expects the calling spec to define:
# - model_class
# - mounted_as
# - to_store
RSpec.shared_examples 'uploads migration worker' do
describe '.enqueue!' do
def enqueue!
described_class.enqueue!(uploads, model_class, mounted_as, to_store)
end
it 'is guarded by .sanity_check!' do
expect(described_class).to receive(:perform_async)
expect(described_class).to receive(:sanity_check!)
enqueue!
end
context 'sanity_check! fails' do
before do
expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError)
end
it 'does not enqueue a job' do
expect(described_class).not_to receive(:perform_async)
expect { enqueue! }.to raise_error(described_class::SanityCheckError)
end
end
end
describe '.sanity_check!' do
shared_examples 'raises a SanityCheckError' do |expected_message|
let(:mount_point) { nil }
it do
expect { described_class.sanity_check!(uploads, model_class, mount_point) }
.to raise_error(described_class::SanityCheckError).with_message(expected_message)
end
end
context 'uploader types mismatch' do
let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
include_examples 'raises a SanityCheckError', /Multiple uploaders found/
end
context 'mount point not found' do
include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do
let(:mount_point) { :potato }
end
end
end
describe '#perform' do
it 'migrates files to remote storage' do
expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated 1/1 files})
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
end
context 'reversed' do
let(:to_store) { ObjectStorage::Store::LOCAL }
before do
perform(uploads, ObjectStorage::Store::REMOTE)
end
it 'migrates files to local storage' do
expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(1)
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(1)
end
end
context 'migration is unsuccessful' do
before do
allow_any_instance_of(ObjectStorage::Concern)
.to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.')
end
it 'does not migrate files to remote storage' do
expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
perform(uploads)
expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(1)
end
end
end
end
context "for AvatarUploader" do
let!(:projects) { create_list(:project, 10, :with_avatar) }
let!(:project_with_avatar) { create(:project, :with_avatar) }
let(:mounted_as) { :avatar }
before do
......@@ -27,16 +122,15 @@ RSpec.describe ObjectStorage::MigrateUploadsWorker do
it "to N*5" do
query_count = ActiveRecord::QueryRecorder.new { perform(uploads) }
more_projects = create_list(:project, 3, :with_avatar)
create(:project, :with_avatar)
expected_queries_per_migration = 5 * more_projects.count
expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration)
expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(5)
end
end
end
context "for FileUploader" do
let!(:projects) { create_list(:project, 10) }
let!(:project_with_file) { create(:project) }
let(:secret) { SecureRandom.hex }
let(:mounted_as) { nil }
......@@ -48,7 +142,7 @@ RSpec.describe ObjectStorage::MigrateUploadsWorker do
before do
stub_uploads_object_storage(FileUploader)
projects.map(&method(:upload_file))
upload_file(project_with_file)
end
it_behaves_like "uploads migration worker"
......@@ -57,18 +151,16 @@ RSpec.describe ObjectStorage::MigrateUploadsWorker do
it "to N*5" do
query_count = ActiveRecord::QueryRecorder.new { perform(uploads) }
more_projects = create_list(:project, 3)
more_projects.map(&method(:upload_file))
upload_file(create(:project))
expected_queries_per_migration = 5 * more_projects.count
expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration)
expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(5)
end
end
end
context 'for DesignManagement::DesignV432x230Uploader' do
let(:model_class) { DesignManagement::Action }
let!(:design_actions) { create_list(:design_action, 10, :with_image_v432x230) }
let!(:design_action) { create(:design_action, :with_image_v432x230) }
let(:mounted_as) { :image_v432x230 }
before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment