Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-11 12:08:39 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-11 12:08:39 +0300
commit1078b7bf25c2cb6e03c57da9ae25b0512858556f (patch)
treea50fbfaddb22aca89055296c4c532c7ecb2b1ca0
parent55733b19c526145cceb120e8bb874d476a84383a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/controllers/projects/pipelines_controller.rb10
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml15
-rw-r--r--changelogs/unreleased/32714-snippets-ux-improvement-empty-state.yml5
-rw-r--r--changelogs/unreleased/fe-update-empty-state-btn-x-margin.yml5
-rw-r--r--changelogs/unreleased/fix-signature-badge-popover.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-group-import-api.yml5
-rw-r--r--config/routes/project.rb1
-rw-r--r--doc/api/group_import_export.md83
-rw-r--r--doc/api/groups.md4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/group_import.rb77
-rw-r--r--lib/api/helpers/file_upload_helpers.rb15
-rw-r--r--lib/api/project_import.rb9
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb9
-rw-r--r--locale/gitlab.pot10
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb70
-rw-r--r--spec/features/dashboard/snippets_spec.rb7
-rw-r--r--spec/features/signed_commits_spec.rb28
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb5
-rw-r--r--spec/models/ci/pipeline_spec.rb34
-rw-r--r--spec/requests/api/group_import_spec.rb268
25 files changed, 638 insertions, 42 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index ba624dacf9a..72963fb08c2 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.20.0
+8.21.0
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 37cb2372b80..0e4080ce201 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -325,11 +325,11 @@
}
.btn {
- margin: $btn-side-margin 5px;
+ margin: $gl-padding-8 $gl-padding-4;
@include media-breakpoint-down(xs) {
width: 100%;
- margin: $btn-side-margin 0;
+ margin: $gl-padding-8 0;
}
}
}
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index a62eb94a3e4..6d902e099d9 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -179,6 +179,16 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def test_reports_count
+ return unless Feature.enabled?(:junit_pipeline_view, project)
+
+ begin
+ render json: { total_count: pipeline.test_reports_count }.to_json
+ rescue Gitlab::Ci::Parsers::ParserError
+ render json: { total_count: 0 }.to_json
+ end
+ end
+
private
def serialize_pipelines
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index addc8a7a2fc..94b99835a53 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -789,6 +789,12 @@ module Ci
end
end
+ def test_reports_count
+ Rails.cache.fetch(['project', project.id, 'pipeline', id, 'test_reports_count'], force: false) do
+ test_reports.total_count
+ end
+ end
+
def has_exposed_artifacts?
complete? && builds.latest.with_exposed_artifacts.exists?
end
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 776ce48d4bc..8ecaa1329fd 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -30,5 +30,5 @@
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
-%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
+%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
index 889a470d6ec..efd9bceedc5 100644
--- a/app/views/shared/empty_states/_snippets.html.haml
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -1,20 +1,19 @@
- button_path = local_assigns.fetch(:button_path, false)
-.row.empty-state
+.row.empty-state.mt-0
.col-12
.svg-content
= image_tag 'illustrations/snippets_empty.svg'
- .text-content
+ .text-content.text-center.pt-0
- if current_user
%h4
- = s_('SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep.')
- %p
- = s_('SnippetsEmptyState|They can be either public or private.')
- .text-center
+ = s_('SnippetsEmptyState|Code snippets')
+ %p.mb-0
+ = s_('SnippetsEmptyState|Store, share, and embed small pieces of code and text.')
+ .mt-2<
- if button_path
= link_to s_('SnippetsEmptyState|New snippet'), button_path, class: 'btn btn-success', title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link'
- - unless current_page?(dashboard_snippets_path)
- = link_to s_('SnippetsEmptyState|Explore public snippets'), explore_snippets_path, class: 'btn btn-default', title: s_('SnippetsEmptyState|Explore public snippets')
+ = link_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), class: 'btn btn-default', title: s_('SnippetsEmptyState|Documentation')
- else
%h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.')
diff --git a/changelogs/unreleased/32714-snippets-ux-improvement-empty-state.yml b/changelogs/unreleased/32714-snippets-ux-improvement-empty-state.yml
new file mode 100644
index 00000000000..3b224922f54
--- /dev/null
+++ b/changelogs/unreleased/32714-snippets-ux-improvement-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Update snippets empty state and remove explore snippets button
+merge_request: 24764
+author:
+type: other
diff --git a/changelogs/unreleased/fe-update-empty-state-btn-x-margin.yml b/changelogs/unreleased/fe-update-empty-state-btn-x-margin.yml
new file mode 100644
index 00000000000..bdeb183ac1a
--- /dev/null
+++ b/changelogs/unreleased/fe-update-empty-state-btn-x-margin.yml
@@ -0,0 +1,5 @@
+---
+title: Update button margin of various empty states
+merge_request: 24806
+author:
+type: other
diff --git a/changelogs/unreleased/fix-signature-badge-popover.yml b/changelogs/unreleased/fix-signature-badge-popover.yml
new file mode 100644
index 00000000000..53cb20528ab
--- /dev/null
+++ b/changelogs/unreleased/fix-signature-badge-popover.yml
@@ -0,0 +1,5 @@
+---
+title: Fix signature badge popover on Firefox
+merge_request: 24756
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-group-import-api.yml b/changelogs/unreleased/georgekoltsov-group-import-api.yml
new file mode 100644
index 00000000000..b6c497afdaf
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-group-import-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add Group Import API endpoint & update Group Import/Export documentation
+merge_request: 20353
+author:
+type: added
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 24e60be21de..73625416b4b 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -341,6 +341,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :failures
get :status
get :test_report
+ get :test_reports_count
end
member do
diff --git a/doc/api/group_import_export.md b/doc/api/group_import_export.md
new file mode 100644
index 00000000000..c97a753d298
--- /dev/null
+++ b/doc/api/group_import_export.md
@@ -0,0 +1,83 @@
+# Group Import/Export API
+
+> Introduced in GitLab 12.8 as an experimental feature. May change in future releases.
+
+Group Import/Export functionality allows to export group structure and import it at a new location.
+Used in combination with [Project Import/Export](project_import_export.md) it allows you to preserve connections with group level relations
+(e.g. a connection between a project issue and group epic).
+
+Group Export includes:
+
+1. Group Milestones
+1. Group Boards
+1. Group Labels
+1. Group Badges
+1. Group Members
+1. Sub-groups (each sub-group includes all data above)
+
+## Schedule new export
+
+Start a new group export.
+
+```text
+POST /groups/:id/export
+```
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ---------------------------------------- |
+| `id` | integer/string | yes | ID of the groupd owned by the authenticated user |
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/export
+```
+
+```json
+{
+ "message": "202 Accepted"
+}
+```
+
+## Export download
+
+Download the finished export.
+
+```text
+GET /groups/:id/export/download
+```
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ---------------------------------------- |
+| `id` | integer/string | yes | ID of the group owned by the authenticated user |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" --remote-header-name --remote-name https://gitlab.example.com/api/v4/groups/1/export/download
+```
+
+```shell
+ls *export.tar.gz
+2020-12-05_22-11-148_namespace_export.tar.gz
+```
+
+Time spent on exporting a group may vary depending on a size of the group. Export download endpoint will return exported archive once it is available. 404 is returned otherwise.
+
+## Import a file
+
+```text
+POST /groups/import
+```
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ---------------------------------------- |
+| `name` | string | yes | The name of the group to be imported |
+| `path` | string | yes | Name and path for new group |
+| `file` | string | yes | The file to be uploaded |
+| `parent_id` | integer | no | ID of a parent group that the group will be imported into. Defaults to the current user's namespace if not provided. |
+
+To upload a file from your file system, use the `--form` argument. This causes
+cURL to post data using the header `Content-Type: multipart/form-data`.
+The `file=` parameter must point to a file on your file system and be preceded
+by `@`. For example:
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "name=imported-group" --form "path=imported-group" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/groups/import
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 0a2446a8be9..3d2ac8c1e18 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -875,3 +875,7 @@ And to switch pages add:
## Group badges
Read more in the [Group Badges](group_badges.md) documentation.
+
+## Group Import/Export
+
+Read more in the [Group Import/Export](group_import_export.md) documentation.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e75fd7e88a1..9a1e0e3f8e9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -130,6 +130,7 @@ module API
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport
+ mount ::API::GroupImport
mount ::API::GroupLabels
mount ::API::GroupMilestones
mount ::API::Groups
diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb
new file mode 100644
index 00000000000..de7fdc27243
--- /dev/null
+++ b/lib/api/group_import.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module API
+ class GroupImport < Grape::API
+ MAXIMUM_FILE_SIZE = 50.megabytes.freeze
+
+ helpers do
+ def authorize_create_group!
+ parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
+
+ if parent_group
+ authorize! :create_subgroup, parent_group
+ else
+ authorize! :create_group
+ end
+ end
+ end
+
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Workhorse authorize the group import upload' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ post 'import/authorize' do
+ require_gitlab_workhorse!
+
+ Gitlab::Workhorse.verify_api_request!(headers)
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+
+ ImportExportUploader.workhorse_authorize(has_length: false, maximum_size: MAXIMUM_FILE_SIZE)
+ end
+
+ desc 'Create a new group import' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success Entities::Group
+ end
+ params do
+ requires :path, type: String, desc: 'Group path'
+ requires :name, type: String, desc: 'Group name'
+ optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace."
+ optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
+ optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
+ optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
+ optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
+ optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
+ optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
+ optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
+ end
+ post 'import' do
+ authorize_create_group!
+ require_gitlab_workhorse!
+
+ uploaded_file = UploadedFile.from_params(params, :file, ImportExportUploader.workhorse_local_upload_path)
+
+ bad_request!('Unable to process group import file') unless uploaded_file
+
+ group_params = {
+ path: params[:path],
+ name: params[:name],
+ parent_id: params[:parent_id],
+ import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
+ }
+
+ group = ::Groups::CreateService.new(current_user, group_params).execute
+
+ if group.persisted?
+ GroupImportWorker.perform_async(current_user.id, group.id)
+
+ accepted!
+ else
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/file_upload_helpers.rb b/lib/api/helpers/file_upload_helpers.rb
new file mode 100644
index 00000000000..c5fb291a2b7
--- /dev/null
+++ b/lib/api/helpers/file_upload_helpers.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module FileUploadHelpers
+ def file_is_valid?
+ params[:file] && params[:file]['tempfile'].respond_to?(:read)
+ end
+
+ def validate_file!
+ render_api_error!('Uploaded file is invalid', 400) unless file_is_valid?
+ end
+ end
+ end
+end
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index 7e0bd299761..ea793a09f6c 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -5,20 +5,13 @@ module API
include PaginationParams
helpers Helpers::ProjectsHelpers
+ helpers Helpers::FileUploadHelpers
helpers do
def import_params
declared_params(include_missing: false)
end
- def file_is_valid?
- import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read)
- end
-
- def validate_file!
- render_api_error!('The file is invalid', 400) unless file_is_valid?
- end
-
def throttled?(key, scope)
rate_limiter.throttled?(key, scope: scope)
end
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index ef2e4055c62..039e85b3f6c 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -74,6 +74,15 @@ module Gitlab
)
if response
+ # In the add_prometheus_manual_configuration method, the Prometheus
+ # listen_address config is saved as an api_url in the PrometheusService
+ # model. There are validates hooks in the PrometheusService model that
+ # check if the project associated with the PrometheusService is the
+ # self_monitoring project. It checks
+ # Gitlab::CurrentSettings.self_monitoring_project_id, which is why the
+ # Gitlab::CurrentSettings cache needs to be expired here, so that
+ # PrometheusService sees the latest self_monitoring_project_id.
+ Gitlab::CurrentSettings.expire_current_application_settings
success(result)
else
log_error("Could not save instance administration project ID, errors: %{errors}" % { errors: application_settings.errors.full_messages })
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 12716eaa888..f5630209f49 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17597,7 +17597,10 @@ msgstr ""
msgid "Snippets"
msgstr ""
-msgid "SnippetsEmptyState|Explore public snippets"
+msgid "SnippetsEmptyState|Code snippets"
+msgstr ""
+
+msgid "SnippetsEmptyState|Documentation"
msgstr ""
msgid "SnippetsEmptyState|New snippet"
@@ -17606,15 +17609,12 @@ msgstr ""
msgid "SnippetsEmptyState|No snippets found"
msgstr ""
-msgid "SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep."
+msgid "SnippetsEmptyState|Store, share, and embed small pieces of code and text."
msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
-msgid "SnippetsEmptyState|They can be either public or private."
-msgstr ""
-
msgid "Snowplow"
msgstr ""
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index b12af198986..fbb77304ce9 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -682,6 +682,76 @@ describe Projects::PipelinesController do
end
end
+ describe 'GET test_report_count.json' do
+ subject(:test_reports_count_json) do
+ get :test_reports_count, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id
+ },
+ format: :json
+ end
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: true)
+ end
+
+ context 'when pipeline does not have a test report' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'renders an empty badge counter' do
+ test_reports_count_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(0)
+ end
+ end
+
+ context 'when pipeline has a test report' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'renders the badge counter value' do
+ test_reports_count_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(4)
+ end
+ end
+
+ context 'when pipeline has corrupt test reports' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
+ end
+
+ it 'renders 0' do
+ test_reports_count_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(0)
+ end
+ end
+ end
+
+ context 'when feature is disabled' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ before do
+ stub_feature_flags(junit_pipeline_view: false)
+ end
+
+ it 'renders empty response' do
+ test_reports_count_json
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
describe 'GET latest' do
let(:branch_main) { project.repository.branches[0] }
let(:branch_secondary) { project.repository.branches[1] }
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index eb9d722e164..db5e56bdde0 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -32,7 +32,7 @@ describe 'Dashboard snippets' do
it 'shows the empty state when there are no snippets' do
element = page.find('.row.empty-state')
- expect(element).to have_content("Snippets are small pieces of code or notes that you want to keep.")
+ expect(element).to have_content("Code snippets")
expect(element.find('.svg-content img')['src']).to have_content('illustrations/snippets_empty')
end
@@ -40,6 +40,11 @@ describe 'Dashboard snippets' do
parent_element = page.find('.row.empty-state')
expect(parent_element).to have_link('New snippet')
end
+
+ it 'shows documentation button in main comment area' do
+ parent_element = page.find('.row.empty-state')
+ expect(parent_element).to have_link('Documentation', href: help_page_path('user/snippets.md'))
+ end
end
context 'filtering by visibility' do
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 851e155480b..3c7a31ac11b 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -15,8 +15,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).to have_button 'Unverified'
- expect(page).not_to have_button 'Verified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
# user changes their email which makes the gpg key verified
perform_enqueued_jobs do
@@ -26,8 +25,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).not_to have_button 'Unverified'
- expect(page).to have_button 'Verified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Verified')
end
it 'changes from unverified to verified when the user adds the missing gpg key', :sidekiq_might_not_need_inline do
@@ -36,8 +34,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).to have_button 'Unverified'
- expect(page).not_to have_button 'Verified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
# user adds the gpg key which makes the signature valid
perform_enqueued_jobs do
@@ -46,8 +43,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).not_to have_button 'Unverified'
- expect(page).to have_button 'Verified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Verified')
end
context 'shows popover badges', :js do
@@ -77,7 +73,7 @@ describe 'GPG signed commits' do
it 'unverified signature' do
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
- click_on 'Unverified'
+ page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature.'
@@ -90,7 +86,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, GpgHelpers::DIFFERING_EMAIL_SHA)
- click_on 'Unverified'
+ page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
@@ -105,7 +101,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
- click_on 'Unverified'
+ page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
expect(page).to have_content "This commit was signed with a different user's verified signature."
@@ -120,7 +116,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
- click_on 'Verified'
+ page.find('.gpg-status-box', text: 'Verified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
@@ -136,13 +132,13 @@ describe 'GPG signed commits' do
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
# wait for the signature to get generated
- expect(page).to have_button 'Verified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Verified')
user_1.destroy!
refresh
- click_on 'Verified'
+ page.find('.gpg-status-box', text: 'Verified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
@@ -160,9 +156,9 @@ describe 'GPG signed commits' do
end
it 'displays commit signature' do
- expect(page).to have_button 'Unverified'
+ expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
- click_on 'Unverified'
+ page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature'
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index 8311f3f4539..d643a2df46b 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -125,6 +125,11 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do
expect(application_setting.self_monitoring_project_id).to eq(project.id)
end
+ it 'expires application_setting cache' do
+ expect(Gitlab::CurrentSettings).to receive(:expire_current_application_settings)
+ expect(result[:status]).to eq(:success)
+ end
+
it 'creates an environment for the project' do
expect(project.default_environment.name).to eq('production')
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 18f3c4af08c..6efec87464b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2678,6 +2678,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#test_reports_count', :use_clean_rails_memory_store_caching do
+ subject { pipeline.test_reports }
+
+ context 'when pipeline has multiple builds with test reports' do
+ let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) }
+ let!(:build_java) { create(:ci_build, :success, name: 'java', pipeline: pipeline, project: project) }
+
+ before do
+ create(:ci_job_artifact, :junit, job: build_rspec, project: project)
+ create(:ci_job_artifact, :junit_with_ant, job: build_java, project: project)
+ end
+
+ it 'returns test report count equal to test reports total_count' do
+ expect(subject.total_count).to eq(7)
+ expect(subject.total_count).to eq(pipeline.test_reports_count)
+ end
+
+ it 'reads from cache when records are cached' do
+ expect(Rails.cache.fetch(['project', project.id, 'pipeline', pipeline.id, 'test_reports_count'], force: false)).to be_nil
+
+ pipeline.test_reports_count
+
+ expect(ActiveRecord::QueryRecorder.new { pipeline.test_reports_count }.count).to eq(0)
+ end
+ end
+
+ context 'when pipeline does not have any builds with test reports' do
+ it 'returns empty test report count' do
+ expect(subject.total_count).to eq(0)
+ expect(subject.total_count).to eq(pipeline.test_reports_count)
+ end
+ end
+ end
+
describe '#total_size' do
let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
new file mode 100644
index 00000000000..016ed6ff491
--- /dev/null
+++ b/spec/requests/api/group_import_spec.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::GroupImport do
+ include WorkhorseHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let(:path) { '/groups/import' }
+ let(:file) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
+ let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
+ let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |import_export|
+ expect(import_export).to receive(:storage_path).and_return(export_path)
+ end
+
+ stub_uploads_object_storage(ImportExportUploader)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ describe 'POST /groups/import' do
+ let(:file_upload) { fixture_file_upload(file) }
+ let(:params) do
+ {
+ path: 'test-import-group',
+ name: 'test-import-group',
+ file: fixture_file_upload(file)
+ }
+ end
+
+ subject { post api('/groups/import', user), params: params, headers: workhorse_header }
+
+ shared_examples 'when all params are correct' do
+ context 'when user is authorized to create new group' do
+ it 'creates new group and accepts request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(202)
+ end
+
+ context 'when importing to a parent group' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'creates new group and accepts request' do
+ params[:parent_id] = group.id
+
+ subject
+
+ expect(response).to have_gitlab_http_status(202)
+ expect(group.children.count).to eq(1)
+ end
+
+ context 'when parent group is invalid' do
+ it 'returns 404 and does not create new group' do
+ params[:parent_id] = 99999
+
+ expect { subject }.not_to change { Group.count }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Group Not Found')
+ end
+
+ context 'when user is not an owner of parent group' do
+ it 'returns 403 Forbidden HTTP status' do
+ params[:parent_id] = create(:group).id
+
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+ end
+ end
+ end
+
+ context 'when group creation failed' do
+ before do
+ allow_next_instance_of(Group) do |group|
+ allow(group).to receive(:persisted?).and_return(false)
+ end
+ end
+
+ it 'returns 400 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context 'when user is not authorized to create new group' do
+ let(:user) { create(:user, can_create_group: false) }
+
+ it 'forbids the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ shared_examples 'when some params are missing' do
+ context 'when required params are missing' do
+ shared_examples 'missing parameter' do |params, error_message|
+ it 'returns 400 HTTP status' do
+ params[:file] = file_upload
+
+ expect do
+ post api('/groups/import', user), params: params, headers: workhorse_header
+ end.not_to change { Group.count }.from(1)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq(error_message)
+ end
+ end
+
+ include_examples 'missing parameter', { name: 'test' }, 'path is missing'
+ include_examples 'missing parameter', { path: 'test' }, 'name is missing'
+ end
+ end
+
+ context 'with object storage disabled' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader, enabled: false)
+ end
+
+ context 'without a file from workhorse' do
+ it 'rejects the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'without a workhorse header' do
+ it 'rejects request without a workhorse header' do
+ post api('/groups/import', user), params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when params from workhorse are correct' do
+ let(:params) do
+ {
+ path: 'test-import-group',
+ name: 'test-import-group',
+ 'file.path' => file_upload.path,
+ 'file.name' => file_upload.original_filename
+ }
+ end
+
+ include_examples 'when all params are correct'
+ include_examples 'when some params are missing'
+ end
+
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'with object storage enabled' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader, enabled: true)
+
+ allow(ImportExportUploader).to receive(:workhorse_upload_path).and_return('/')
+ end
+
+ context 'with direct upload enabled' do
+ let(:file_name) { 'group_export.tar.gz' }
+ let!(:fog_connection) do
+ stub_uploads_object_storage(ImportExportUploader, direct_upload: true)
+ end
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'uploads').files.create(
+ key: "tmp/uploads/#{file_name}",
+ body: file_upload
+ )
+ end
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+ let(:params) do
+ {
+ path: 'test-import-group',
+ name: 'test-import-group',
+ file: fog_file,
+ 'file.remote_id' => file_name,
+ 'file.size' => fog_file.size
+ }
+ end
+
+ it 'accepts the request and stores the file' do
+ expect { subject }.to change { Group.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ include_examples 'when all params are correct'
+ include_examples 'when some params are missing'
+ end
+ end
+ end
+
+ describe 'POST /groups/import/authorize' do
+ subject { post api('/groups/import/authorize', user), headers: workhorse_header }
+
+ it 'authorizes importing group with workhorse header' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'rejects requests that bypassed gitlab-workhorse' do
+ workhorse_header.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: true)
+ end
+
+ it 'responds with status 200, location of file remote store and object details' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response).not_to have_key('TempPath')
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).to have_key('MultipartUpload')
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader, enabled: true, direct_upload: false)
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(ImportExportUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ end
+ end
+ end
+ end
+end