From 1078b7bf25c2cb6e03c57da9ae25b0512858556f Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 11 Feb 2020 09:08:39 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- GITLAB_WORKHORSE_VERSION | 2 +- app/assets/stylesheets/framework/blocks.scss | 4 +- app/controllers/projects/pipelines_controller.rb | 10 + app/models/ci/pipeline.rb | 6 + .../projects/commit/_signature_badge.html.haml | 2 +- app/views/shared/empty_states/_snippets.html.haml | 15 +- .../32714-snippets-ux-improvement-empty-state.yml | 5 + .../fe-update-empty-state-btn-x-margin.yml | 5 + .../unreleased/fix-signature-badge-popover.yml | 5 + .../unreleased/georgekoltsov-group-import-api.yml | 5 + config/routes/project.rb | 1 + doc/api/group_import_export.md | 83 +++++++ doc/api/groups.md | 4 + lib/api/api.rb | 1 + lib/api/group_import.rb | 77 ++++++ lib/api/helpers/file_upload_helpers.rb | 15 ++ lib/api/project_import.rb | 9 +- .../self_monitoring/project/create_service.rb | 9 + locale/gitlab.pot | 10 +- .../projects/pipelines_controller_spec.rb | 70 ++++++ spec/features/dashboard/snippets_spec.rb | 7 +- spec/features/signed_commits_spec.rb | 28 +-- .../self_monitoring/project/create_service_spec.rb | 5 + spec/models/ci/pipeline_spec.rb | 34 +++ spec/requests/api/group_import_spec.rb | 268 +++++++++++++++++++++ 25 files changed, 638 insertions(+), 42 deletions(-) create mode 100644 changelogs/unreleased/32714-snippets-ux-improvement-empty-state.yml create mode 100644 changelogs/unreleased/fe-update-empty-state-btn-x-margin.yml create mode 100644 changelogs/unreleased/fix-signature-badge-popover.yml create mode 100644 changelogs/unreleased/georgekoltsov-group-import-api.yml create mode 100644 doc/api/group_import_export.md create mode 100644 lib/api/group_import.rb create mode 100644 lib/api/helpers/file_upload_helpers.rb create mode 100644 spec/requests/api/group_import_spec.rb 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: " 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: " --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: " --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 -- cgit v1.2.3