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:
-rw-r--r--.gitlab/issue_templates/Feature proposal.md2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue61
-rw-r--r--app/assets/javascripts/pipeline_new/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/controllers/projects/pipelines_controller.rb5
-rw-r--r--app/controllers/projects/static_site_editor_controller.rb24
-rw-r--r--app/models/packages/conan/file_metadatum.rb3
-rw-r--r--app/presenters/packages/conan/package_presenter.rb50
-rw-r--r--app/services/static_site_editor/config_service.rb49
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--changelogs/unreleased/11679-conan-project-level.yml5
-rw-r--r--changelogs/unreleased/20426-allow-mr-to-merge-when-pipeline-blocked-and-pipeline-must-succeed-i.yml6
-rw-r--r--changelogs/unreleased/235874-propogate-errors-api-fuzzing.yml5
-rw-r--r--changelogs/unreleased/245277-handle-redis-for-usage-ping-sql-dump-rake-task.yml5
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/features_inside_dot_gitlab.md16
-rw-r--r--doc/development/packages.md4
-rw-r--r--doc/development/testing_guide/best_practices.md5
-rw-r--r--doc/development/testing_guide/end_to_end/beginners_guide.md2
-rw-r--r--doc/development/testing_guide/end_to_end/best_practices.md121
-rw-r--r--doc/development/testing_guide/end_to_end/rspec_metadata_tests.md7
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md2
-rw-r--r--doc/user/analytics/merge_request_analytics.md17
-rw-r--r--doc/user/gitlab_com/index.md3
-rw-r--r--doc/user/group/index.md3
-rw-r--r--doc/user/packages/conan_repository/index.md94
-rw-r--r--doc/user/packages/pypi_repository/index.md2
-rw-r--r--doc/user/permissions.md3
-rw-r--r--doc/user/project/repository/index.md2
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md4
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/conan_instance_packages.rb10
-rw-r--r--lib/api/conan_package_endpoints.rb (renamed from lib/api/conan_packages.rb)30
-rw-r--r--lib/api/conan_project_packages.rb16
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb94
-rw-r--r--lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml3
-rw-r--r--lib/gitlab/git/repository.rb18
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb2
-rw-r--r--lib/gitlab/static_site_editor/config/combined_config.rb35
-rw-r--r--lib/gitlab/static_site_editor/config/file_config.rb3
-rw-r--r--lib/gitlab/static_site_editor/config/generated_config.rb4
-rw-r--r--lib/gitlab/usage_data_queries.rb8
-rw-r--r--locale/gitlab.pot11
-rw-r--r--package.json2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb11
-rw-r--r--spec/controllers/projects/static_site_editor_controller_spec.rb24
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb4
-rw-r--r--spec/features/static_site_editor_spec.rb8
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js47
-rw-r--r--spec/frontend/pipeline_new/mock_data.js12
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js32
-rw-r--r--spec/lib/gitlab/static_site_editor/config/combined_config_spec.rb37
-rw-r--r--spec/lib/gitlab/static_site_editor/config/file_config_spec.rb4
-rw-r--r--spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb24
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb14
-rw-r--r--spec/policies/project_policy_spec.rb124
-rw-r--r--spec/presenters/packages/conan/package_presenter_spec.rb91
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb152
-rw-r--r--spec/requests/api/conan_packages_spec.rb986
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb152
-rw-r--r--spec/requests/api/projects_spec.rb83
-rw-r--r--spec/services/static_site_editor/config_service_spec.rb64
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb18
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb75
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb843
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb10
-rw-r--r--yarn.lock8
69 files changed, 2195 insertions, 1421 deletions
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index e79b7033eda..0f19b7c75f5 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -1,4 +1,4 @@
-<!-- The first section "Release notes" is required if you want to have your release post blog MR auto generated. Currently piloting, details can be found in this issue: https://gitlab.com/gitlab-com/Product/-/issues/1392 and this video: https://www.youtube.com/watch?v=rfn9ebgTwKg The next four sections: "Problem to solve", "Intended users", "User experience goal", and "Proposal", are strongly recommended in your first draft, while the rest of the sections can be filled out during the problem validation or breakdown phase. However, keep in mind that providing complete and relevant information early helps our product team validate the problem and start working on a solution. -->
+<!-- The first section "Release notes" is required if you want to have your release post blog MR auto generated. Currently in BETA, details on the **release post item generator** can be found in the handbook: https://about.gitlab.com/handbook/marketing/blog/release-posts/#release-post-item-generator and this video: https://www.youtube.com/watch?v=rfn9ebgTwKg. The next four sections: "Problem to solve", "Intended users", "User experience goal", and "Proposal", are strongly recommended in your first draft, while the rest of the sections can be filled out during the problem validation or breakdown phase. However, keep in mind that providing complete and relevant information early helps our product team validate the problem and start working on a solution. -->
### Release notes
diff --git a/Gemfile.lock b/Gemfile.lock
index 75e5cfd5508..d728925c507 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -822,7 +822,7 @@ GEM
parslet (1.8.2)
peek (1.1.0)
railties (>= 4.0.0)
- pg (1.2.2)
+ pg (1.2.3)
png_quantizator (0.2.1)
po_to_json (1.0.1)
json (>= 1.6.0)
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 2b53ac9130b..be8ce832d20 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -14,7 +14,7 @@ import {
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import { s__, __, n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
@@ -29,6 +29,8 @@ export default {
),
formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
errorTitle: __('The form contains the following error:'),
+ warningTitle: __('The form contains the following warning:'),
+ maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
components: {
GlAlert,
GlButton,
@@ -74,13 +76,20 @@ export default {
required: false,
default: () => ({}),
},
+ maxWarnings: {
+ type: Number,
+ required: true,
+ },
},
data() {
return {
searchTerm: '',
refValue: this.refParam,
variables: {},
- error: false,
+ error: null,
+ warnings: [],
+ totalWarnings: 0,
+ isWarningDismissed: false,
};
},
computed: {
@@ -91,6 +100,18 @@ export default {
variablesLength() {
return Object.keys(this.variables).length;
},
+ overMaxWarningsLimit() {
+ return this.totalWarnings > this.maxWarnings;
+ },
+ warningsSummary() {
+ return n__('%d warning found:', '%d warnings found:', this.warnings.length);
+ },
+ summaryMessage() {
+ return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary;
+ },
+ shouldShowWarning() {
+ return this.warnings.length > 0 && !this.isWarningDismissed;
+ },
},
created() {
if (this.variableParams) {
@@ -154,8 +175,11 @@ export default {
redirectTo(`${this.pipelinesPath}/${data.id}`);
})
.catch(err => {
- const [message] = err.response.data.base;
- this.error = message;
+ const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
+ const [error] = errors;
+ this.error = error;
+ this.warnings = warnings;
+ this.totalWarnings = totalWarnings;
});
},
},
@@ -170,8 +194,37 @@ export default {
:dismissible="false"
variant="danger"
class="gl-mb-4"
+ data-testid="run-pipeline-error-alert"
>{{ error }}</gl-alert
>
+ <gl-alert
+ v-if="shouldShowWarning"
+ :title="$options.warningTitle"
+ variant="warning"
+ class="gl-mb-4"
+ data-testid="run-pipeline-warning-alert"
+ @dismiss="isWarningDismissed = true"
+ >
+ <details>
+ <summary>
+ <gl-sprintf :message="summaryMessage">
+ <template #total>
+ {{ totalWarnings }}
+ </template>
+ <template #warningsDisplayed>
+ {{ maxWarnings }}
+ </template>
+ </gl-sprintf>
+ </summary>
+ <p
+ v-for="(warning, index) in warnings"
+ :key="`warning-${index}`"
+ data-testid="run-pipeline-warning"
+ >
+ {{ warning }}
+ </p>
+ </details>
+ </gl-alert>
<gl-form-group :label="s__('Pipeline|Run for')">
<gl-dropdown :text="refValue" block>
<gl-search-box-by-type
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 1c4812c2e0e..f1ea86f8c5f 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -11,6 +11,7 @@ export default () => {
fileParam,
refNames,
settingsLink,
+ maxWarnings,
} = el?.dataset;
const variableParams = JSON.parse(varParam);
@@ -29,6 +30,7 @@ export default () => {
fileParams,
refs,
settingsLink,
+ maxWarnings: Number(maxWarnings),
},
});
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 0d46bac91a5..846b1c453a1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -102,7 +102,8 @@ export default class MergeRequestStore {
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
- this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
+ this.isPipelineBlocked =
+ data.only_allow_merge_if_pipeline_succeeds && pipelineStatus?.group === 'manual';
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.terraformReportsPath = data.terraform_reports_path;
this.testResultsPath = data.test_reports_path;
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 4a105c80154..c1734d2cd8a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -78,7 +78,10 @@ class Projects::PipelinesController < Projects::ApplicationController
.represent(@pipeline),
status: :created
else
- render json: @pipeline.errors, status: :bad_request
+ render json: { errors: @pipeline.error_messages.map(&:content),
+ warnings: @pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
+ total_warnings: @pipeline.warning_messages.length },
+ status: :bad_request
end
end
end
diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb
index 3251814bac8..e97a8db0b79 100644
--- a/app/controllers/projects/static_site_editor_controller.rb
+++ b/app/controllers/projects/static_site_editor_controller.rb
@@ -14,19 +14,27 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
end
def show
- config = Gitlab::StaticSiteEditor::Config::CombinedConfig.new(
- @repository,
- @ref,
- @path,
- params[:return_url]
- )
- @data = config.data
+ service_response = ::StaticSiteEditor::ConfigService.new(
+ container: project,
+ current_user: current_user,
+ params: {
+ ref: @ref,
+ path: @path,
+ return_url: params[:return_url]
+ }
+ ).execute
+
+ if service_response.success?
+ @data = service_response.payload
+ else
+ respond_422
+ end
end
private
def assign_ref_and_path
- @ref, @path = extract_ref(params[:id])
+ @ref, @path = extract_ref(params.fetch(:id))
render_404 if @ref.blank? || @path.blank?
end
diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb
index e1ef62b3959..de54580e948 100644
--- a/app/models/packages/conan/file_metadatum.rb
+++ b/app/models/packages/conan/file_metadatum.rb
@@ -3,6 +3,9 @@
class Packages::Conan::FileMetadatum < ApplicationRecord
belongs_to :package_file, inverse_of: :conan_file_metadatum
+ DEFAULT_PACKAGE_REVISION = '0'.freeze
+ DEFAULT_RECIPE_REVISION = '0'.freeze
+
validates :package_file, presence: true
validates :recipe_revision,
diff --git a/app/presenters/packages/conan/package_presenter.rb b/app/presenters/packages/conan/package_presenter.rb
index a14c6b28017..df770777ad1 100644
--- a/app/presenters/packages/conan/package_presenter.rb
+++ b/app/presenters/packages/conan/package_presenter.rb
@@ -3,6 +3,7 @@
module Packages
module Conan
class PackagePresenter
+ include API::Helpers::Packages::Conan::ApiHelpers
include API::Helpers::RelatedResourcesHelpers
include Gitlab::Utils::StrongMemoize
@@ -17,7 +18,10 @@ module Packages
def recipe_urls
map_package_files do |package_file|
- build_recipe_file_url(package_file) if package_file.conan_file_metadatum.recipe_file?
+ next unless package_file.conan_file_metadatum.recipe_file?
+
+ options = url_options(package_file)
+ recipe_file_url(options)
end
end
@@ -31,7 +35,12 @@ module Packages
map_package_files do |package_file|
next unless package_file.conan_file_metadatum.package_file? && matching_reference?(package_file)
- build_package_file_url(package_file)
+ options = url_options(package_file).merge(
+ conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
+ package_revision: package_file.conan_file_metadatum.package_revision
+ )
+
+ package_file_url(options)
end
end
@@ -45,36 +54,21 @@ module Packages
private
- def build_recipe_file_url(package_file)
- expose_url(
- api_v4_packages_conan_v1_files_export_path(
- package_name: @package.name,
- package_version: @package.version,
- package_username: @package.conan_metadatum.package_username,
- package_channel: @package.conan_metadatum.package_channel,
- recipe_revision: package_file.conan_file_metadatum.recipe_revision,
- file_name: package_file.file_name
- )
- )
- end
-
- def build_package_file_url(package_file)
- expose_url(
- api_v4_packages_conan_v1_files_package_path(
- package_name: @package.name,
- package_version: @package.version,
- package_username: @package.conan_metadatum.package_username,
- package_channel: @package.conan_metadatum.package_channel,
- recipe_revision: package_file.conan_file_metadatum.recipe_revision,
- conan_package_reference: package_file.conan_file_metadatum.conan_package_reference,
- package_revision: package_file.conan_file_metadatum.package_revision,
- file_name: package_file.file_name
- )
- )
+ def url_options(package_file)
+ {
+ package_name: @package.name,
+ package_version: @package.version,
+ package_username: @package.conan_metadatum.package_username,
+ package_channel: @package.conan_metadatum.package_channel,
+ file_name: package_file.file_name,
+ recipe_revision: package_file.conan_file_metadatum.recipe_revision.presence || ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION
+ }
end
def map_package_files
package_files.to_a.map do |package_file|
+ next unless package_file.conan_file_metadatum
+
key = package_file.file_name
value = yield(package_file)
next unless key && value
diff --git a/app/services/static_site_editor/config_service.rb b/app/services/static_site_editor/config_service.rb
new file mode 100644
index 00000000000..987ee071976
--- /dev/null
+++ b/app/services/static_site_editor/config_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module StaticSiteEditor
+ class ConfigService < ::BaseContainerService
+ ValidationError = Class.new(StandardError)
+
+ def execute
+ @project = container
+ check_access!
+
+ ServiceResponse.success(payload: data)
+ rescue ValidationError => e
+ ServiceResponse.error(message: e.message)
+ end
+
+ private
+
+ attr_reader :project
+
+ def check_access!
+ unless can?(current_user, :download_code, project)
+ raise ValidationError, 'Insufficient permissions to read configuration'
+ end
+ end
+
+ def data
+ check_for_duplicate_keys!
+ generated_data.merge(file_data)
+ end
+
+ def generated_data
+ @generated_data ||= Gitlab::StaticSiteEditor::Config::GeneratedConfig.new(
+ project.repository,
+ params.fetch(:ref),
+ params.fetch(:path),
+ params[:return_url]
+ ).data
+ end
+
+ def file_data
+ @file_data ||= Gitlab::StaticSiteEditor::Config::FileConfig.new.data
+ end
+
+ def check_for_duplicate_keys!
+ duplicate_keys = generated_data.keys & file_data.keys
+ raise ValidationError.new("Duplicate key(s) '#{duplicate_keys}' found.") if duplicate_keys.present?
+ end
+ end
+end
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 726bf9af223..8369e147b4a 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -7,7 +7,7 @@
%hr
- if Feature.enabled?(:new_pipeline_form)
- #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project) } }
+ #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project), max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
- else
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
diff --git a/changelogs/unreleased/11679-conan-project-level.yml b/changelogs/unreleased/11679-conan-project-level.yml
new file mode 100644
index 00000000000..c57abdb5461
--- /dev/null
+++ b/changelogs/unreleased/11679-conan-project-level.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Conan packages to be scoped to project-level
+merge_request: 39541
+author:
+type: added
diff --git a/changelogs/unreleased/20426-allow-mr-to-merge-when-pipeline-blocked-and-pipeline-must-succeed-i.yml b/changelogs/unreleased/20426-allow-mr-to-merge-when-pipeline-blocked-and-pipeline-must-succeed-i.yml
new file mode 100644
index 00000000000..898cee8d5a0
--- /dev/null
+++ b/changelogs/unreleased/20426-allow-mr-to-merge-when-pipeline-blocked-and-pipeline-must-succeed-i.yml
@@ -0,0 +1,6 @@
+---
+title: Merge Requests are not blocked when their pipelines are waiting for manual
+ actions unless 'Pipeline must succeed' is checked in the settings.
+merge_request: 42207
+author:
+type: fixed
diff --git a/changelogs/unreleased/235874-propogate-errors-api-fuzzing.yml b/changelogs/unreleased/235874-propogate-errors-api-fuzzing.yml
new file mode 100644
index 00000000000..456b8e1e9fa
--- /dev/null
+++ b/changelogs/unreleased/235874-propogate-errors-api-fuzzing.yml
@@ -0,0 +1,5 @@
+---
+title: Fail API Fuzzing CI/CD job when scanner errors
+merge_request: 41616
+author:
+type: fixed
diff --git a/changelogs/unreleased/245277-handle-redis-for-usage-ping-sql-dump-rake-task.yml b/changelogs/unreleased/245277-handle-redis-for-usage-ping-sql-dump-rake-task.yml
new file mode 100644
index 00000000000..5219de672d3
--- /dev/null
+++ b/changelogs/unreleased/245277-handle-redis-for-usage-ping-sql-dump-rake-task.yml
@@ -0,0 +1,5 @@
+---
+title: Updated gitlab:usage_data:dump_sql_in_yaml rake task with redis usage
+merge_request: 42189
+author:
+type: changed
diff --git a/doc/development/README.md b/doc/development/README.md
index 6a2bf278ff2..398aea55e72 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -197,6 +197,7 @@ See [database guidelines](database/index.md).
- [Defining relations between files using projections](projections.md)
- [Reference processing](./reference_processing.md)
- [Compatibility with multiple versions of the application running at the same time](multi_version_compatibility.md)
+- [Features inside `.gitlab/`](./features_inside_dot_gitlab.md)
## Other GitLab Development Kit (GDK) guides
diff --git a/doc/development/features_inside_dot_gitlab.md b/doc/development/features_inside_dot_gitlab.md
new file mode 100644
index 00000000000..cb883471adf
--- /dev/null
+++ b/doc/development/features_inside_dot_gitlab.md
@@ -0,0 +1,16 @@
+# Features inside the `.gitlab/` directory
+
+We have implemented standard features that depend on configuration files in the `.gitlab/` directory. You can find `.gitlab/` in various GitLab repositories.
+When implementing new features, please refer to these existing features to avoid conflicts:
+
+- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
+- [Issue Templates](../user/project/description_templates.md#creating-issue-templates): `.gitlab/issue_templates/`.
+- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
+- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
+- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
+- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
+- [Customize Auto DevOps Helm Values](../topics/autodevops/customize.md#customize-values-for-helm-chart): `.gitlab/auto-deploy-values.yaml`.
+- [GitLab managed apps CI/CD](../user/clusters/applications.md#usage): `.gitlab/managed-apps/config.yaml`.
+- [Insights](../user/project/insights/index.md#configure-your-insights): `.gitlab/insights.yml`.
+- [Service Desk Templates](../user/project/service_desk.md#using-customized-email-templates): `.gitlab/service_desk_templates/`.
+- [Web IDE](../user/project/web_ide/#web-ide-configuration-file): `.gitlab/.gitlab-webide.yml`.
diff --git a/doc/development/packages.md b/doc/development/packages.md
index e21eff543b4..55e22d4bb5f 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -67,7 +67,7 @@ The current state of existing package registries availability is:
| Repository Type | Project Level | Group Level | Instance Level |
|-----------------|---------------|-------------|----------------|
| Maven | Yes | Yes | Yes |
-| Conan | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) | Yes |
+| Conan | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) | Yes |
| NPM | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) |
| NuGet | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36423) | No |
| PyPI | Yes | No | No |
@@ -87,7 +87,7 @@ Composer package naming scope is Instance Level.
To avoid name conflict for instance-level endpoints you will need to define a package naming convention
that gives a way to identify the project that the package belongs to. This generally involves using the project
ID or full project path in the package name. See
-[Conan's naming convention](../user/packages/conan_repository/index.md#package-recipe-naming-convention) as an example.
+[Conan's naming convention](../user/packages/conan_repository/index.md#package-recipe-naming-convention-for-instance-level-remote) as an example.
For group and project-level endpoints, naming can be less constrained and it will be up to the group and project
members to be certain that there is no conflict between two package names. However, the system should prevent
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 31035d6f6bf..c7853be4932 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -427,9 +427,9 @@ spec itself, but the former is preferred.
It takes around one second to load tests that are using `fast_spec_helper`
instead of 30+ seconds in case of a regular `spec_helper`.
-### `let` variables
+### `subject` and `let` variables
-GitLab's RSpec suite has made extensive use of `let`(along with it strict, non-lazy
+GitLab's RSpec suite has made extensive use of `let`(along with its strict, non-lazy
version `let!`) variables to reduce duplication. However, this sometimes [comes at the cost of clarity](https://thoughtbot.com/blog/lets-not),
so we need to set some guidelines for their use going forward:
@@ -448,6 +448,7 @@ so we need to set some guidelines for their use going forward:
- `let!` variables should be used only in case if strict evaluation with defined
order is required, otherwise `let` will suffice. Remember that `let` is lazy and won't
be evaluated until it is referenced.
+- Use named `subject(:name)` over un-named `subject` in examples, as this gives the subject a contextual name.
### Common test setup
diff --git a/doc/development/testing_guide/end_to_end/beginners_guide.md b/doc/development/testing_guide/end_to_end/beginners_guide.md
index bafcbbd32a1..c552c44c864 100644
--- a/doc/development/testing_guide/end_to_end/beginners_guide.md
+++ b/doc/development/testing_guide/end_to_end/beginners_guide.md
@@ -287,7 +287,7 @@ Note the following important points:
- Our test fabricates only what it needs, when it needs it.
- The issue is fabricated through the API to save time.
- GitLab prefers `let()` over instance variables. See
- [best practices](../best_practices.md#let-variables).
+ [best practices](../best_practices.md#subject-and-let-variables).
- `be_closed` is not implemented in `page/project/issue/show.rb` yet, but will be
implemented in the next step.
diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index 3fed4b46e4a..36cb49256a6 100644
--- a/doc/development/testing_guide/end_to_end/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -3,6 +3,127 @@
NOTE: **Note:**
This is a tailored extension of the Best Practices [found in the testing guide](../best_practices.md).
+## Link a test to its test-case issue
+
+Every test should have a corresponding issue in the [Quality Testcases project](https://gitlab.com/gitlab-org/quality/testcases/).
+It's recommended that you reuse the issue created to plan the test. If one does not already exist you
+can create the issue yourself. Alternatively, you can run the test in a pipeline that has reporting
+enabled and the test-case issue reporter will automatically create a new issue.
+
+Whether you create a new test-case issue or one is created automatically, you will need to manually add
+a `testcase` RSpec metadata tag. In most cases, a single test will be associated with a single test-case
+issue ([see below for exceptions](#exceptions)).
+
+For example:
+
+```ruby
+RSpec.describe 'Stage' do
+ describe 'General description of the feature under test' do
+ it 'test name', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/:issue_id' do
+ ...
+ end
+
+ it 'another test', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/:another_issue_id' do
+ ...
+ end
+ end
+end
+```
+
+### Exceptions
+
+Most tests are defined by a single line of a `spec` file, which is why those tests can be linked to a
+single test-case issue via the `testcase` tag.
+
+However, some tests don't have a one-to-one relationship between a line of a `spec` file and a test-case
+issue. This is because some tests are defined in a way that means a single line is associated with
+multiple tests, including:
+
+- Parallelized tests.
+- Templated tests.
+- Tests in shared examples that include more than one example.
+
+In those and similar cases we can't assign a single `testcase` tag and so we rely on the test-case
+reporter to programmatically determine the correct test-case issue based on the name and description of
+the test. In such cases, the test-case reporter will automatically create a test-case issue the first time
+the test runs, if no issue exists already.
+
+In such a case, if you create the issue yourself or want to reuse an existing issue,
+you must use this [end-to-end test issue template](https://gitlab.com/gitlab-org/quality/testcases/-/blob/master/.gitlab/issue_templates/End-to-end%20Test.md)
+to format the issue description.
+
+To illustrate, there are two tests in the shared examples in [`qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/47b17db82c38ab704a23b5ba5d296ea0c6a732c8/qa/qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb):
+
+```ruby
+shared_examples 'only user with access pushes and merges' do
+ it 'unselected maintainer user fails to push' do
+ ...
+ end
+
+ it 'selected developer user pushes and merges' do
+ ...
+ end
+end
+```
+
+Consider the following test that includes the shared examples:
+
+```ruby
+RSpec.describe 'Create' do
+ describe 'Restricted protected branch push and merge' do
+ context 'when only one user is allowed to merge and push to a protected branch' do
+ ...
+ it_behaves_like 'only user with access pushes and merges'
+ end
+ end
+end
+```
+
+There would be two associated test-case issues, one for each shared example, with the following content:
+
+[Test 1](https://gitlab.com/gitlab-org/quality/testcases/-/issues/600):
+
+````markdown
+```markdown
+Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
+protected branch push and merge when only one user is allowed to merge and push to a protected
+branch behaves like only user with access pushes and merges selecte...
+
+Description:
+### Full description
+
+Create Restricted protected branch push and merge when only one user is allowed to merge and push
+to a protected branch behaves like only user with access pushes and merges selected developer user
+pushes and merges
+
+### File path
+
+./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
+
+```
+````
+
+[Test 2](https://gitlab.com/gitlab-org/quality/testcases/-/issues/602):
+
+````markdown
+```markdown
+Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
+protected branch push and merge when only one user is allowed to merge and push to a protected
+branch behaves like only user with access pushes and merges unselec...
+
+Description:
+### Full description
+
+Create Restricted protected branch push and merge when only one user is allowed to merge and push
+to a protected branch behaves like only user with access pushes and merges unselected maintainer
+user fails to push
+
+### File path
+
+./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
+```
+````
+
## Prefer API over UI
The end-to-end testing framework has the ability to fabricate its resources on a case-by-case basis.
diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
index 4059c1960e2..a9f54b53e5a 100644
--- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
+++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
@@ -8,13 +8,14 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec
| Tag | Description |
|-----|-------------|
| `:elasticsearch` | The test requires an Elasticsearch service. It is used by the [instance-level scenario](https://gitlab.com/gitlab-org/gitlab-qa#definitions) [`Test::Integration::Elasticsearch`](https://gitlab.com/gitlab-org/gitlab/-/blob/72b62b51bdf513e2936301cb6c7c91ec27c35b4d/qa/qa/ee/scenario/test/integration/elasticsearch.rb) to include only tests that require Elasticsearch. |
+| `:gitaly_cluster` | The test will run against a GitLab instance where repositories are stored on redundant Gitaly nodes behind a Praefect node. All nodes are [separate containers](../../../administration/gitaly/praefect.md#requirements-for-configuring-a-gitaly-cluster). Tests that use this tag have a longer setup time since there are three additional containers that need to be started. |
+| `:jira` | The test requires a Jira Server. [GitLab-QA](https://gitlab.com/gitlab-org/gitlab-qa) will provision the Jira Server in a Docker container when the `Test::Integration::Jira` test scenario is run.
| `:kubernetes` | The test includes a GitLab instance that is configured to be run behind an SSH tunnel, allowing a TLS-accessible GitLab. This test will also include provisioning of at least one Kubernetes cluster to test against. *This tag is often be paired with `:orchestrated`.* |
+| `:only` | The test is only to be run against specific environments. See [Environment selection](environment_selection.md) for more information. |
| `:orchestrated` | The GitLab instance under test may be [configured by `gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#orchestrated-tests) to be different to the default GitLab configuration, or `gitlab-qa` may launch additional services in separate Docker containers, or both. Tests tagged with `:orchestrated` are excluded when testing environments where we can't dynamically modify GitLab's configuration (for example, Staging). |
| `:quarantine` | The test has been [quarantined](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#quarantining-tests), will run in a separate job that only includes quarantined tests, and is allowed to fail. The test will be skipped in its regular job so that if it fails it will not hold up the pipeline. Note that you can also [quarantine a test only when it runs against specific environment](environment_selection.md#quarantining-a-test-for-a-specific-environment). |
| `:reliable` | The test has been [promoted to a reliable test](https://about.gitlab.com/handbook/engineering/quality/guidelines/reliable-tests/#promoting-an-existing-test-to-reliable) meaning it passes consistently in all pipelines, including merge requests. |
| `:requires_admin` | The test requires an admin account. Tests with the tag are excluded when run against Canary and Production environments. |
| `:runner` | The test depends on and will set up a GitLab Runner instance, typically to run a pipeline. |
-| `:gitaly_ha` | The test will run against a GitLab instance where repositories are stored on redundant Gitaly nodes behind a Praefect node. All nodes are [separate containers](../../../administration/gitaly/praefect.md#requirements-for-configuring-a-gitaly-cluster). Tests that use this tag have a longer setup time since there are three additional containers that need to be started. |
| `:skip_live_env` | The test will be excluded when run against live deployed environments such as Staging, Canary, and Production. |
-| `:jira` | The test requires a Jira Server. [GitLab-QA](https://gitlab.com/gitlab-org/gitlab-qa) will provision the Jira Server in a Docker container when the `Test::Integration::Jira` test scenario is run.
-| `:only` | The test is only to be run against specific environments. See [Environment selection](environment_selection.md) for more information. |
+| `:testcase` | The link to the test case issue in the [Quality Testcases project](https://gitlab.com/gitlab-org/quality/testcases/). |
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 5cdbc8ebd4f..957f84206e9 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -81,7 +81,7 @@ The repository size limit includes repository files and LFS, and does not includ
For details on manually purging files, see [reducing the repository size using Git](../../project/repository/reducing_the_repo_size_using_git.md).
NOTE: **Note:**
-GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
+GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#account-and-limit-settings).
## Troubleshooting
diff --git a/doc/user/analytics/merge_request_analytics.md b/doc/user/analytics/merge_request_analytics.md
index 2c2199240a8..6a18d46fd1a 100644
--- a/doc/user/analytics/merge_request_analytics.md
+++ b/doc/user/analytics/merge_request_analytics.md
@@ -50,6 +50,23 @@ Data table displaying a maximum of the 100 most recent merge requests merged for
![Throughput table](img/mr_throughput_table_v13_3.png "Merge Request Analytics - Throughput table listing the 100 merge requests most recently merged")
+## Filter the data
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229266) in GitLab 13.4
+
+You can filter the data that is presented on the page based on the following parameters:
+
+- Author
+- Assignees
+- Labels
+- Milestones
+
+To filter results:
+
+1. Click on the filter bar.
+1. Select a parameter to filter by.
+1. Select a value from the autocompleted results, or enter search text to refine the results.
+
## Permissions
The **Merge Request Analytics** feature can be accessed only:
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 9066d8a5ee4..67dd31efe2c 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -89,7 +89,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| [Max number of instance level variables](../../administration/instance_limits.md#number-of-instance-level-variables) | `25` | `25` |
| [Scheduled Job Archival](../../user/admin_area/settings/continuous_integration.md#archive-jobs) | 3 months | Never |
-## Repository size limit
+## Account and limit settings
GitLab.com has the following [account limits](../admin_area/settings/account_and_limit_settings.md) enabled. If a setting is not listed, it is set to the default value.
@@ -99,6 +99,7 @@ or over the repository size limit, you can [reduce your repository size with Git
| Setting | GitLab.com | Default |
| ----------- | ----------- | ------------- |
| Repository size including LFS | 10 GB | Unlimited |
+| Maximum import size | 5 GB | 50 MB |
NOTE: **Note:**
`git push` and GitLab project imports are limited to 5 GB per request through Cloudflare. Git LFS and imports other than a file upload are not affected by this limit.
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index d3aebac3ae6..32b76cf9280 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -668,6 +668,9 @@ To enable delayed deletion of projects:
1. Expand the **Permissions, LFS, 2FA** section, and check **Enable delayed project removal**.
1. Click **Save changes**.
+NOTE: **Note:**
+The group setting for delayed deletion is not inherited by sub-groups and has to be individually defined for each group.
+
#### Prevent project forking outside group **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216987) in GitLab 13.3.
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index 47bdb8d412a..7c3082e0f83 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -83,15 +83,13 @@ conan new Hello/0.1 -t
Next, create a package for that recipe by running `conan create` providing the Conan user and channel:
```shell
-conan create . my-org+my-group+my-project/beta
+conan create . mycompany/beta
```
NOTE: **Note:**
-Current [naming restrictions](#package-recipe-naming-convention) require you to name the `user` value as the `+` separated path of your project on GitLab.
+If you are using the [instance level remote](#instance-level-remote), a specific [naming convention](#package-recipe-naming-convention-for-instance-level-remote) must be followed.
-The example above would create a package belonging to this project: `https://gitlab.com/my-org/my-group/my-project` with a channel of `beta`.
-
-These two example commands generate a final package with the recipe `Hello/0.1@my-org+my-group+my-project/beta`.
+These two example commands generate a final package with the recipe `Hello/0.1@mycompany/beta`.
For more advanced details on creating and managing your packages, refer to the [Conan docs](https://docs.conan.io/en/latest/creating_packages.html).
@@ -99,6 +97,38 @@ You are now ready to upload your package to the GitLab registry. To get started,
## Adding the GitLab Package Registry as a Conan remote
+You can add the GitLab Package Registry as a Conan remote at the project or instance level.
+
+### Project level remote
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) in GitLab 13.4.
+
+The project level remote allows you to work with packages within a given project.
+The advantage of using the project level remote is there are no restrictions to your
+package name, however all GitLab Conan packages require a full recipe
+with the user and channel (`package_name/version@user/channel`).
+
+To add the remote:
+
+```shell
+conan remote add gitlab https://gitlab.example.com/api/v4/projects/<project_id>/packages/conan
+```
+
+Once the remote is set, you can use the remote when running Conan commands by adding `--remote=gitlab` to the end of your commands.
+
+For example:
+
+```shell
+conan search Hello* --all --remote=gitlab
+```
+
+### Instance level remote
+
+The instance level remote allows you to use a single remote to access packages accross your entire
+GitLab instance. However, when using this remote, there are certain
+[package naming restrictions](#package-recipe-naming-convention-for-instance-level-remote)
+that must be followed.
+
Add a new remote to your Conan configuration:
```shell
@@ -113,6 +143,25 @@ For example:
conan search 'Hello*' --remote=gitlab
```
+#### Package recipe naming convention for instance level remote
+
+The standard Conan recipe convention looks like `package_name/version@user/channel`,
+but if you're using the [instance level remote](#instance-level-remote), the recipe
+`user` must be the plus sign (`+`) separated project path.
+
+The following table shows some example recipes you can give your package based on
+the project name and path.
+
+| Project | Package | Supported |
+| ---------------------------------- | ----------------------------------------------- | --------- |
+| `foo/bar` | `my-package/1.0.0@foo+bar/stable` | Yes |
+| `foo/bar-baz/buz` | `my-package/1.0.0@foo+bar-baz+buz/stable` | Yes |
+| `gitlab-org/gitlab-ce` | `my-package/1.0.0@gitlab-org+gitlab-ce/stable` | Yes |
+| `gitlab-org/gitlab-ce` | `my-package/1.0.0@foo/stable` | No |
+
+NOTE: **Note:**
+[Project level remotes](#project-level-remote) allow for more flexible package names.
+
## Authenticating to the GitLab Conan Repository
You need a personal access token or deploy token.
@@ -142,7 +191,7 @@ Alternatively, you could explicitly include your credentials in any given comman
For example:
```shell
-CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
+CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> conan upload Hello/0.1@mycompany/beta --all --remote=gitlab
```
### Setting a default remote to your project (optional)
@@ -150,7 +199,7 @@ CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<
If you'd like Conan to always use GitLab as the registry for your package, you can tell Conan to always reference the GitLab remote for a given package recipe:
```shell
-conan remote add_ref Hello/0.1@my-group+my-project/beta gitlab
+conan remote add_ref Hello/0.1@mycompany/beta gitlab
```
NOTE: **Note:**
@@ -165,34 +214,19 @@ The rest of the example commands in this documentation assume that you've added
## Uploading a package
-First you need to [create your Conan package locally](https://docs.conan.io/en/latest/creating_packages/getting_started.html). In order to work with the GitLab Package Registry, a specific [naming convention](#package-recipe-naming-convention) must be followed.
+First you need to [create your Conan package locally](https://docs.conan.io/en/latest/creating_packages/getting_started.html).
+
+NOTE: **Note:**
+If you are using the [instance level remote](#instance-level-remote), a specific [naming convention](#package-recipe-naming-convention-for-instance-level-remote) must be followed.
Ensure you have a project created on GitLab and that the personal access token you're using has the correct permissions for write access to the container registry by selecting the `api` [scope](../../../user/profile/personal_access_tokens.md#limiting-scopes-of-a-personal-access-token).
You can upload your package to the GitLab Package Registry using the `conan upload` command:
```shell
-conan upload Hello/0.1@my-group+my-project/beta --all
+conan upload Hello/0.1@mycompany/beta --all
```
-### Package recipe naming convention
-
-Standard Conan recipe convention looks like `package_name/version@user/channel`.
-
-**The recipe user must be the `+` separated project path**. The package
-name may be anything, but it is preferred that the project name be used unless
-it's not possible due to a naming collision. For example:
-
-| Project | Package | Supported |
-| ---------------------------------- | ----------------------------------------------- | --------- |
-| `foo/bar` | `my-package/1.0.0@foo+bar/stable` | Yes |
-| `foo/bar-baz/buz` | `my-package/1.0.0@foo+bar-baz+buz/stable` | Yes |
-| `gitlab-org/gitlab-ce` | `my-package/1.0.0@gitlab-org+gitlab-ce/stable` | Yes |
-| `gitlab-org/gitlab-ce` | `my-package/1.0.0@foo/stable` | No |
-
-NOTE: **Note:**
-A future iteration will extend support to [project and group level](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) remotes which allows for more flexible naming conventions.
-
## Installing a package
Conan packages are commonly installed as dependencies using the `conanfile.txt` file.
@@ -204,7 +238,7 @@ Add the Conan recipe to the `[requires]` section of the file:
```ini
[requires]
- Hello/0.1@my-group+my-project/beta
+ Hello/0.1@mycompany/beta
[generators]
cmake
@@ -253,7 +287,7 @@ To search using a partial name, use the wildcard symbol `*`, which should be pla
```shell
conan search Hello --all --remote=gitlab
conan search He* --all --remote=gitlab
-conan search Hello/0.1@my-group+my-project/beta --all --remote=gitlab
+conan search Hello/0.1@mycompany/beta --all --remote=gitlab
```
The scope of your search includes all projects you have permission to access, this includes your private projects as well as all public projects.
@@ -263,7 +297,7 @@ The scope of your search includes all projects you have permission to access, th
The `conan info` command returns information about a given package:
```shell
-conan info Hello/0.1@my-group+my-project/beta
+conan info Hello/0.1@mycompany/beta
```
## List of supported CLI commands
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 2b30fd9209a..97f3f69d676 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -302,7 +302,7 @@ Successfully installed mypypipackage-0.0.1
## Using GitLab CI with PyPI packages
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11678) in GitLab 13.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 72111928a79..e2baac1a962 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -248,7 +248,7 @@ group.
| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
-| Create project in group | | | ✓ (3) | ✓ (3) | ✓ (3) |
+| Create project in group | | | ✓ (3)(5) | ✓ (3) | ✓ (3) |
| Share (invite) groups with groups | | | | | ✓ |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
| Create/edit/delete iterations | | | ✓ | ✓ | ✓ |
@@ -283,6 +283,7 @@ group.
- The [instance level](admin_area/settings/visibility_and_access_controls.md#default-project-creation-protection).
- The [group level](group/index.md#default-project-creation-level).
1. Does not apply to subgroups.
+1. Developers can push commits to the default branch of a new project only if the [default branch protection](group/index.md#changing-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
### Subgroup permissions
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index a57806cf3ff..536cae263b8 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -190,7 +190,7 @@ updated every 15 minutes at most, so may not reflect recent activity. The displa
The project size may differ slightly from one instance to another due to compression, housekeeping, and other factors.
[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins.
-GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
+GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#account-and-limit-settings).
## Contributors
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index 7e7716569b8..2a641121931 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -252,9 +252,9 @@ When using repository cleanup, note:
Repository size limits:
-- Can [be set by an administrator](../../admin_area/settings/account_and_limit_settings.md#repository-size-limit)
+- Can [be set by an administrator](../../admin_area/settings/account_and_limit_settings.md#account-and-limit-settings)
on self-managed instances. **(STARTER ONLY)**
-- Are [set for GitLab.com](../../gitlab_com/index.md#repository-size-limit).
+- Are [set for GitLab.com](../../gitlab_com/index.md#account-and-limit-settings).
When a project has reached its size limit, you cannot:
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ba77203154e..b37751e1b47 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -194,7 +194,8 @@ module API
mount ::API::NugetPackages
mount ::API::PypiPackages
mount ::API::ComposerPackages
- mount ::API::ConanPackages
+ mount ::API::ConanProjectPackages
+ mount ::API::ConanInstancePackages
mount ::API::MavenPackages
mount ::API::NpmPackages
mount ::API::GenericPackages
diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb
new file mode 100644
index 00000000000..209748d79fa
--- /dev/null
+++ b/lib/api/conan_instance_packages.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Conan Instance-Level Package Manager Client API
+module API
+ class ConanInstancePackages < Grape::API::Instance
+ namespace 'packages/conan/v1' do
+ include ConanPackageEndpoints
+ end
+ end
+end
diff --git a/lib/api/conan_packages.rb b/lib/api/conan_package_endpoints.rb
index 899683de184..445447cfcd2 100644
--- a/lib/api/conan_packages.rb
+++ b/lib/api/conan_package_endpoints.rb
@@ -9,8 +9,8 @@
#
# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
module API
- class ConanPackages < Grape::API::Instance
- helpers ::API::Helpers::PackagesManagerClientsHelpers
+ module ConanPackageEndpoints
+ extend ActiveSupport::Concern
PACKAGE_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX,
@@ -28,15 +28,19 @@ module API
CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze
- before do
- require_packages_enabled!
+ included do
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::Packages::Conan::ApiHelpers
+ helpers ::API::Helpers::RelatedResourcesHelpers
- # Personal access token will be extracted from Bearer or Basic authorization
- # in the overridden find_personal_access_token or find_user_from_job_token helpers
- authenticate!
- end
+ before do
+ require_packages_enabled!
+
+ # Personal access token will be extracted from Bearer or Basic authorization
+ # in the overridden find_personal_access_token or find_user_from_job_token helpers
+ authenticate!
+ end
- namespace 'packages/conan/v1' do
desc 'Ping the Conan API' do
detail 'This feature was introduced in GitLab 12.2'
end
@@ -242,7 +246,7 @@ module API
delete do
authorize!(:destroy_package, project)
- package_event('delete_package')
+ package_event('delete_package', category: 'API::ConanPackages')
package.destroy
end
@@ -341,11 +345,5 @@ module API
end
end
end
-
- helpers do
- include Gitlab::Utils::StrongMemoize
- include ::API::Helpers::RelatedResourcesHelpers
- include ::API::Helpers::Packages::Conan::ApiHelpers
- end
end
end
diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb
new file mode 100644
index 00000000000..c51992231a7
--- /dev/null
+++ b/lib/api/conan_project_packages.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Conan Project-Level Package Manager Client API
+module API
+ class ConanProjectPackages < Grape::API::Instance
+ params do
+ requires :id, type: Integer, desc: 'The ID of a project', regexp: %r{\A[1-9]\d*\z}
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ namespace ':id/packages/conan/v1' do
+ include ConanPackageEndpoints
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index 5b513ce1aab..dcbf933a4e1 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -5,6 +5,8 @@ module API
module Packages
module Conan
module ApiHelpers
+ include Gitlab::Utils::StrongMemoize
+
def present_download_urls(entity)
authorize!(:read_package, project)
@@ -31,7 +33,7 @@ module API
def recipe_upload_urls
{ upload_urls: Hash[
file_names.select(&method(:recipe_file?)).map do |file_name|
- [file_name, recipe_file_upload_url(file_name)]
+ [file_name, build_recipe_file_upload_url(file_name)]
end
] }
end
@@ -39,7 +41,7 @@ module API
def package_upload_urls
{ upload_urls: Hash[
file_names.select(&method(:package_file?)).map do |file_name|
- [file_name, package_file_upload_url(file_name)]
+ [file_name, build_package_file_upload_url(file_name)]
end
] }
end
@@ -52,32 +54,58 @@ module API
file_name.in?(::Packages::Conan::FileMetadatum::PACKAGE_FILES)
end
- def package_file_upload_url(file_name)
- expose_url(
- api_v4_packages_conan_v1_files_package_path(
- package_name: params[:package_name],
- package_version: params[:package_version],
- package_username: params[:package_username],
- package_channel: params[:package_channel],
- recipe_revision: '0',
- conan_package_reference: params[:conan_package_reference],
- package_revision: '0',
- file_name: file_name
- )
+ def build_package_file_upload_url(file_name)
+ options = url_options(file_name).merge(
+ conan_package_reference: params[:conan_package_reference],
+ package_revision: ::Packages::Conan::FileMetadatum::DEFAULT_PACKAGE_REVISION
)
+
+ package_file_url(options)
+ end
+
+ def build_recipe_file_upload_url(file_name)
+ recipe_file_url(url_options(file_name))
end
- def recipe_file_upload_url(file_name)
- expose_url(
- api_v4_packages_conan_v1_files_export_path(
- package_name: params[:package_name],
- package_version: params[:package_version],
- package_username: params[:package_username],
- package_channel: params[:package_channel],
- recipe_revision: '0',
- file_name: file_name
+ def url_options(file_name)
+ {
+ package_name: params[:package_name],
+ package_version: params[:package_version],
+ package_username: params[:package_username],
+ package_channel: params[:package_channel],
+ file_name: file_name,
+ recipe_revision: ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION
+ }
+ end
+
+ def package_file_url(options)
+ case package_scope
+ when :project
+ expose_url(
+ api_v4_projects_packages_conan_v1_files_package_path(
+ options.merge(id: project.id)
+ )
)
- )
+ when :instance
+ expose_url(
+ api_v4_packages_conan_v1_files_package_path(options)
+ )
+ end
+ end
+
+ def recipe_file_url(options)
+ case package_scope
+ when :project
+ expose_url(
+ api_v4_projects_packages_conan_v1_files_export_path(
+ options.merge(id: project.id)
+ )
+ )
+ when :instance
+ expose_url(
+ api_v4_packages_conan_v1_files_export_path(options)
+ )
+ end
end
def recipe
@@ -86,8 +114,13 @@ module API
def project
strong_memoize(:project) do
- full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
- Project.find_by_full_path(full_path)
+ case package_scope
+ when :project
+ find_project!(params[:id])
+ when :instance
+ full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
+ find_project!(full_path)
+ end
end
end
@@ -97,6 +130,7 @@ module API
.conan
.with_name(params[:package_name])
.with_version(params[:package_version])
+ .with_conan_username(params[:package_username])
.with_conan_channel(params[:package_channel])
.order_created
.last
@@ -124,7 +158,7 @@ module API
conan_package_reference: params[:conan_package_reference]
).execute!
- package_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
+ package_event('pull_package', category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
present_carrierwave_file!(package_file.file)
end
@@ -135,7 +169,7 @@ module API
def track_push_package_event
if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate
- package_event('push_package')
+ package_event('push_package', category: 'API::ConanPackages')
end
end
@@ -236,6 +270,10 @@ module API
token
end
+
+ def package_scope
+ params[:id].present? ? :project : :instance
+ end
end
end
end
diff --git a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
index a078f203cf2..c3a92b67a8b 100644
--- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
@@ -118,6 +118,9 @@ apifuzzer_fuzz:
# Wait for testing to complete if api fuzzer is scanning
- if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI" != "" ]; then echo "Waiting for API Fuzzer to exit"; docker wait apifuzzer; fi
#
+ # Propagate exit code from api fuzzer (if any)
+ - if [[ $(docker inspect apifuzzer --format='{{.State.ExitCode}}') != "0" ]]; then echo "API Fuzzing exited with an error. Logs are available as job artifacts."; docker logs apifuzzer; exit 1; fi
+ #
# Run user provided pre-script
- sh -c "$FUZZAPI_POST_SCRIPT"
#
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 3783a340257..8ace4157ad7 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -19,15 +19,15 @@ module Gitlab
GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
- NoRepository = Class.new(StandardError)
- InvalidRepository = Class.new(StandardError)
- InvalidBlobName = Class.new(StandardError)
- InvalidRef = Class.new(StandardError)
- GitError = Class.new(StandardError)
- DeleteBranchError = Class.new(StandardError)
- TagExistsError = Class.new(StandardError)
- ChecksumError = Class.new(StandardError)
- class CreateTreeError < StandardError
+ NoRepository = Class.new(::Gitlab::Git::BaseError)
+ InvalidRepository = Class.new(::Gitlab::Git::BaseError)
+ InvalidBlobName = Class.new(::Gitlab::Git::BaseError)
+ InvalidRef = Class.new(::Gitlab::Git::BaseError)
+ GitError = Class.new(::Gitlab::Git::BaseError)
+ DeleteBranchError = Class.new(::Gitlab::Git::BaseError)
+ TagExistsError = Class.new(::Gitlab::Git::BaseError)
+ ChecksumError = Class.new(::Gitlab::Git::BaseError)
+ class CreateTreeError < ::Gitlab::Git::BaseError
attr_reader :error_code
def initialize(error_code)
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
index bb0c18735bb..6417ec20960 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
@@ -5,7 +5,7 @@ module Gitlab
module DuplicateJobs
class Client
def call(worker_class, job, queue, _redis_pool, &block)
- DuplicateJob.new(job, queue).schedule(&block)
+ ::Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(job, queue).schedule(&block)
end
end
end
diff --git a/lib/gitlab/static_site_editor/config/combined_config.rb b/lib/gitlab/static_site_editor/config/combined_config.rb
deleted file mode 100644
index 387ce08e552..00000000000
--- a/lib/gitlab/static_site_editor/config/combined_config.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module StaticSiteEditor
- module Config
- class CombinedConfig
- def initialize(repository, ref, path, return_url)
- @repository = repository
- @ref = ref
- @path = path
- @return_url = return_url
- end
-
- def data
- generated_data = Gitlab::StaticSiteEditor::Config::GeneratedConfig.new(
- @repository,
- @ref,
- @path,
- @return_url
- ).data
- file_data = Gitlab::StaticSiteEditor::Config::FileConfig.new.data
- check_for_duplicate_keys(generated_data, file_data)
- generated_data.merge(file_data)
- end
-
- private
-
- def check_for_duplicate_keys(generated_data, file_data)
- duplicate_keys = generated_data.keys & file_data.keys
- raise StandardError.new("Duplicate key(s) '#{duplicate_keys}' found.") if duplicate_keys.present?
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb
index b43a5440e6b..f647c85e1c8 100644
--- a/lib/gitlab/static_site_editor/config/file_config.rb
+++ b/lib/gitlab/static_site_editor/config/file_config.rb
@@ -5,9 +5,8 @@ module Gitlab
module Config
class FileConfig
def data
- merge_requests_illustration_path = ActionController::Base.helpers.image_path('illustrations/merge_requests.svg')
{
- merge_requests_illustration_path: merge_requests_illustration_path
+ static_site_generator: 'middleman'
}
end
end
diff --git a/lib/gitlab/static_site_editor/config/generated_config.rb b/lib/gitlab/static_site_editor/config/generated_config.rb
index 4c875b1d211..f3dce74a32f 100644
--- a/lib/gitlab/static_site_editor/config/generated_config.rb
+++ b/lib/gitlab/static_site_editor/config/generated_config.rb
@@ -14,6 +14,7 @@ module Gitlab
end
def data
+ merge_requests_illustration_path = ActionController::Base.helpers.image_path('illustrations/merge_requests.svg')
{
branch: ref,
path: path,
@@ -23,7 +24,8 @@ module Gitlab
namespace: project.namespace.full_path,
return_url: sanitize_url(return_url),
is_supported_content: supported_content?.to_s,
- base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path)
+ base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path),
+ merge_requests_illustration_path: merge_requests_illustration_path
}
end
diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb
index f38050ceee6..55940f51f99 100644
--- a/lib/gitlab/usage_data_queries.rb
+++ b/lib/gitlab/usage_data_queries.rb
@@ -11,6 +11,14 @@ module Gitlab
raw_sql(relation, column, :distinct)
end
+ def redis_usage_data(counter = nil, &block)
+ if block_given?
+ { redis_usage_data_block: block.to_s }
+ elsif counter.present?
+ { redis_usage_data_counter: counter }
+ end
+ end
+
private
def raw_sql(relation, column, distinct = nil)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d89f004ba59..55d92bffe2b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -305,6 +305,11 @@ msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
msgstr[1] ""
+msgid "%d warning found:"
+msgid_plural "%d warnings found:"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
@@ -819,6 +824,9 @@ msgstr ""
msgid "%{total} open issue weight"
msgstr ""
+msgid "%{total} warnings found: showing first %{warningsDisplayed}"
+msgstr ""
+
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
@@ -25064,6 +25072,9 @@ msgstr ""
msgid "The form contains the following error:"
msgstr ""
+msgid "The form contains the following warning:"
+msgstr ""
+
msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr ""
diff --git a/package.json b/package.json
index d7b8514d9f2..9569a0d997e 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.164.0",
- "@gitlab/ui": "21.2.1",
+ "@gitlab/ui": "21.3.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.22.3",
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1196a702115..c3be7de25a8 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -801,6 +801,11 @@ RSpec.describe Projects::PipelinesController do
context 'with an invalid .gitlab-ci.yml file' do
before do
stub_ci_pipeline_yaml_file(YAML.dump({
+ build: {
+ stage: 'build',
+ script: 'echo',
+ rules: [{ when: 'always' }]
+ },
test: {
stage: 'invalid',
script: 'echo'
@@ -812,9 +817,13 @@ RSpec.describe Projects::PipelinesController do
expect { subject }.not_to change { project.ci_pipelines.count }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['base']).to include(
+ expect(json_response['errors']).to eq([
'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
+ ])
+ expect(json_response['warnings'][0]).to include(
+ 'jobs:build may allow multiple pipelines to run for a single action due to `rules:when`'
)
+ expect(json_response['total_warnings']).to eq(1)
end
end
end
diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb
index 1156887b2b6..7883c7e6f81 100644
--- a/spec/controllers/projects/static_site_editor_controller_spec.rb
+++ b/spec/controllers/projects/static_site_editor_controller_spec.rb
@@ -7,12 +7,6 @@ RSpec.describe Projects::StaticSiteEditorController do
let_it_be(:user) { create(:user) }
let(:data) { instance_double(Hash) }
- before do
- allow_next_instance_of(Gitlab::StaticSiteEditor::Config::CombinedConfig) do |config|
- allow(config).to receive(:data) { data }
- end
- end
-
describe 'GET show' do
let(:default_params) do
{
@@ -23,6 +17,16 @@ RSpec.describe Projects::StaticSiteEditorController do
}
end
+ let(:service_response) do
+ ServiceResponse.success(payload: data)
+ end
+
+ before do
+ allow_next_instance_of(::StaticSiteEditor::ConfigService) do |instance|
+ allow(instance).to receive(:execute).and_return(service_response)
+ end
+ end
+
context 'User roles' do
context 'anonymous' do
before do
@@ -74,6 +78,14 @@ RSpec.describe Projects::StaticSiteEditorController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when invalid config file' do
+ let(:service_response) { ServiceResponse.error(message: 'invalid') }
+
+ it 'returns 422' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index c7d26dfc814..93fea44707c 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -127,8 +127,10 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
end
- context 'when merge request is in the blocked pipeline state' do
+ context 'when merge request is in the blocked pipeline state and pipeline must succeed' do
before do
+ project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
+
create(
:ci_pipeline,
project: project,
diff --git a/spec/features/static_site_editor_spec.rb b/spec/features/static_site_editor_spec.rb
index 9ae23b4bdec..b67e47b6ac4 100644
--- a/spec/features/static_site_editor_spec.rb
+++ b/spec/features/static_site_editor_spec.rb
@@ -13,7 +13,11 @@ RSpec.describe 'Static Site Editor' do
visit project_show_sse_path(project, 'master/README.md')
end
- it 'renders Static Site Editor page' do
- expect(page).to have_selector('#static-site-editor')
+ it 'renders Static Site Editor page with generated and file attributes' do
+ # assert generated config value is present
+ expect(page).to have_css('#static-site-editor[data-branch="master"]')
+
+ # assert file config value is present
+ expect(page).to have_css('#static-site-editor[data-static-site-generator="middleman"]')
end
end
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 8a3173a4862..97a92778f1a 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,10 +1,10 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem, GlForm } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlForm, GlSprintf } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
-import { mockRefs, mockParams, mockPostParams, mockProjectId } from '../mock_data';
+import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -28,6 +28,10 @@ describe('Pipeline New Form', () => {
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
+ const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
+ const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
+ const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
+ const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data);
const createComponent = (term = '', props = {}, method = shallowMount) => {
@@ -38,6 +42,7 @@ describe('Pipeline New Form', () => {
refs: mockRefs,
defaultBranch: 'master',
settingsLink: '',
+ maxWarnings: 25,
...props,
},
data() {
@@ -50,8 +55,6 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
-
- mock.onPost(pipelinesPath).reply(200, postResponse);
});
afterEach(() => {
@@ -62,6 +65,10 @@ describe('Pipeline New Form', () => {
});
describe('Dropdown with branches and tags', () => {
+ beforeEach(() => {
+ mock.onPost(pipelinesPath).reply(200, postResponse);
+ });
+
it('displays dropdown with all branches and tags', () => {
createComponent();
expect(findDropdownItems()).toHaveLength(mockRefs.length);
@@ -82,6 +89,8 @@ describe('Pipeline New Form', () => {
describe('Form', () => {
beforeEach(() => {
createComponent('', mockParams, mount);
+
+ mock.onPost(pipelinesPath).reply(200, postResponse);
});
it('displays the correct values for the provided query params', async () => {
expect(findDropdown().props('text')).toBe('tag-1');
@@ -124,4 +133,34 @@ describe('Pipeline New Form', () => {
expect(findVariableRows()).toHaveLength(4);
});
});
+
+ describe('Form errors and warnings', () => {
+ beforeEach(() => {
+ createComponent();
+
+ mock.onPost(pipelinesPath).reply(400, mockError);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ return waitForPromises();
+ });
+
+ it('shows both error and warning', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(true);
+ });
+
+ it('shows the correct error', () => {
+ expect(findErrorAlert().text()).toBe(mockError.errors[0]);
+ });
+
+ it('shows the correct warning title', () => {
+ const { length } = mockError.warnings;
+ expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ });
+
+ it('shows the correct amount of warnings', () => {
+ expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index 55ec1fb5afc..55286e0ec7e 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -19,3 +19,15 @@ export const mockPostParams = {
{ key: 'test_file', value: 'test_file_val', variable_type: 'file' },
],
};
+
+export const mockError = {
+ errors: [
+ 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post',
+ ],
+ warnings: [
+ 'jobs:build1 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ 'jobs:build2 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ 'jobs:build3 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ ],
+ total_warnings: 7,
+};
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index 48326eda404..b691a366a0f 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -69,6 +69,38 @@ describe('MergeRequestStore', () => {
});
});
+ describe('isPipelineBlocked', () => {
+ const pipelineWaitingForManualAction = {
+ details: {
+ status: {
+ group: 'manual',
+ },
+ },
+ };
+
+ it('should be `false` when the pipeline status is missing', () => {
+ store.setData({ ...mockData, pipeline: undefined });
+
+ expect(store.isPipelineBlocked).toBe(false);
+ });
+
+ it('should be `false` when the pipeline is waiting for manual action', () => {
+ store.setData({ ...mockData, pipeline: pipelineWaitingForManualAction });
+
+ expect(store.isPipelineBlocked).toBe(false);
+ });
+
+ it('should be `true` when the pipeline is waiting for manual action and the pipeline must succeed', () => {
+ store.setData({
+ ...mockData,
+ pipeline: pipelineWaitingForManualAction,
+ only_allow_merge_if_pipeline_succeeds: true,
+ });
+
+ expect(store.isPipelineBlocked).toBe(true);
+ });
+ });
+
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
diff --git a/spec/lib/gitlab/static_site_editor/config/combined_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/combined_config_spec.rb
deleted file mode 100644
index 9a31728648b..00000000000
--- a/spec/lib/gitlab/static_site_editor/config/combined_config_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::StaticSiteEditor::Config::CombinedConfig do
- subject(:config) { described_class.new(repository, ref, path, return_url) }
-
- let(:repository) { double(:repository) }
- let(:ref) { double(:ref) }
- let(:path) { double(:path) }
- let(:return_url) { double(:return_url) }
- let(:generated_data) { { generated: true } }
- let(:file_data) { { file: true } }
-
- describe '#data' do
- subject { config.data }
-
- before do
- allow_next_instance_of(Gitlab::StaticSiteEditor::Config::GeneratedConfig) do |config|
- allow(config).to receive(:data) { generated_data }
- end
- allow_next_instance_of(Gitlab::StaticSiteEditor::Config::FileConfig) do |config|
- allow(config).to receive(:data) { file_data }
- end
- end
-
- it 'returns merged generated data and config file data' do
- is_expected.to eq({ generated: true, file: true })
- end
-
- it 'raises an exception if any keys would be overwritten by the merge' do
- generated_data[:duplicate_key] = true
- file_data[:duplicate_key] = true
- expect { subject }.to raise_error(StandardError, /duplicate key.*duplicate_key.*found/i)
- end
- end
-end
diff --git a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
index 665e2651d65..594425c2dab 100644
--- a/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config/file_config_spec.rb
@@ -9,9 +9,7 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::FileConfig do
subject { config.data }
it 'returns hardcoded data for now' do
- is_expected.to match(
- merge_requests_illustration_path: %r{illustrations/merge_requests}
- )
+ is_expected.to match(static_site_generator: 'middleman')
end
end
end
diff --git a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
index 44e0c9249f5..3433a54be9c 100644
--- a/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config/generated_config_spec.rb
@@ -20,17 +20,19 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
subject { config.data }
it 'returns data for the frontend component' do
- is_expected.to eq(
- branch: 'master',
- commit_id: repository.commit.id,
- namespace: 'namespace',
- path: 'README.md',
- project: 'project',
- project_id: project.id,
- return_url: 'http://example.com',
- is_supported_content: 'true',
- base_url: '/namespace/project/-/sse/master%2FREADME.md'
- )
+ is_expected
+ .to match({
+ branch: 'master',
+ commit_id: repository.commit.id,
+ namespace: 'namespace',
+ path: 'README.md',
+ project: 'project',
+ project_id: project.id,
+ return_url: 'http://example.com',
+ is_supported_content: 'true',
+ base_url: '/namespace/project/-/sse/master%2FREADME.md',
+ merge_requests_illustration_path: %r{illustrations/merge_requests}
+ })
end
context 'when namespace is a subgroup' do
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 06f3174dd34..617c047a868 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -18,4 +18,18 @@ RSpec.describe Gitlab::UsageDataQueries do
expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
end
end
+
+ describe '.redis_usage_data' do
+ subject(:redis_usage_data) { described_class.redis_usage_data { 42 } }
+
+ it 'returns a class for redis_usage_data with a counter call' do
+ expect(described_class.redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter))
+ .to eq(redis_usage_data_counter: Gitlab::UsageDataCounters::WikiPageCounter)
+ end
+
+ it 'returns a stringified block for redis_usage_data with a block' do
+ is_expected.to include(:redis_usage_data_block)
+ expect(redis_usage_data[:redis_usage_data_block]).to start_with('#<Proc:')
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index af17434b4f2..6f8e21bde85 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -6,21 +6,9 @@ RSpec.describe ProjectPolicy do
include ExternalAuthorizationServiceHelpers
include_context 'ProjectPolicy context'
- let_it_be(:other_user) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:developer) { create(:user) }
- let_it_be(:maintainer) { create(:user) }
- let_it_be(:owner) { create(:user) }
- let_it_be(:admin) { create(:admin) }
- let(:project) { create(:project, :public, namespace: owner.namespace) }
-
- before do
- project.add_guest(guest)
- project.add_maintainer(maintainer)
- project.add_developer(developer)
- project.add_reporter(reporter)
- end
+ let(:project) { public_project }
+
+ subject { described_class.new(current_user, project) }
def expect_allowed(*permissions)
permissions.each { |p| is_expected.to be_allowed(p) }
@@ -31,7 +19,7 @@ RSpec.describe ProjectPolicy do
end
context 'with no project feature' do
- subject { described_class.new(owner, project) }
+ let(:current_user) { owner }
before do
project.project_feature.destroy!
@@ -63,7 +51,7 @@ RSpec.describe ProjectPolicy do
end
context 'issues feature' do
- subject { described_class.new(owner, project) }
+ let(:current_user) { owner }
context 'when the feature is disabled' do
before do
@@ -91,7 +79,7 @@ RSpec.describe ProjectPolicy do
end
context 'merge requests feature' do
- subject { described_class.new(owner, project) }
+ let(:current_user) { owner }
it 'disallows all permissions when the feature is disabled' do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
@@ -105,9 +93,8 @@ RSpec.describe ProjectPolicy do
end
context 'for a guest in a private project' do
- let(:project) { create(:project, :private) }
-
- subject { described_class.new(guest, project) }
+ let(:current_user) { guest }
+ let(:project) { private_project }
it 'disallows the guest from reading the merge request and merge request iid' do
expect_disallowed(:read_merge_request)
@@ -116,12 +103,10 @@ RSpec.describe ProjectPolicy do
end
context 'pipeline feature' do
- let(:project) { create(:project) }
+ let(:project) { private_project }
describe 'for unconfirmed user' do
- let(:unconfirmed_user) { create(:user, confirmed_at: nil) }
-
- subject { described_class.new(unconfirmed_user, project) }
+ let(:current_user) { create(:user, confirmed_at: nil) }
it 'disallows to modify pipelines' do
expect_disallowed(:create_pipeline)
@@ -131,7 +116,7 @@ RSpec.describe ProjectPolicy do
end
describe 'for confirmed user' do
- subject { described_class.new(developer, project) }
+ let(:current_user) { developer }
it 'allows modify pipelines' do
expect_allowed(:create_pipeline)
@@ -143,7 +128,7 @@ RSpec.describe ProjectPolicy do
context 'builds feature' do
context 'when builds are disabled' do
- subject { described_class.new(owner, project) }
+ let(:current_user) { owner }
before do
project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED)
@@ -163,7 +148,7 @@ RSpec.describe ProjectPolicy do
end
context 'when builds are disabled only for some users' do
- subject { described_class.new(guest, project) }
+ let(:current_user) { guest }
before do
project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
@@ -194,7 +179,7 @@ RSpec.describe ProjectPolicy do
end
context 'when user is a project member' do
- subject { described_class.new(owner, project) }
+ let(:current_user) { owner }
context 'when it is disabled' do
before do
@@ -212,8 +197,8 @@ RSpec.describe ProjectPolicy do
end
end
- context 'when user is some other user' do
- subject { described_class.new(other_user, project) }
+ context 'when user is non-member' do
+ let(:current_user) { non_member }
context 'when access level is private' do
before do
@@ -243,7 +228,7 @@ RSpec.describe ProjectPolicy do
context 'when a public project has merge requests allowing access' do
include ProjectForksHelper
- let(:user) { create(:user) }
+ let(:current_user) { create(:user) }
let(:target_project) { create(:project, :public) }
let(:project) { fork_project(target_project) }
let!(:merge_request) do
@@ -259,20 +244,18 @@ RSpec.describe ProjectPolicy do
%w(create_build create_pipeline)
end
- subject { described_class.new(user, project) }
-
it 'does not allow pushing code' do
expect_disallowed(*maintainer_abilities)
end
it 'allows pushing if the user is a member with push access to the target project' do
- target_project.add_developer(user)
+ target_project.add_developer(current_user)
expect_allowed(*maintainer_abilities)
end
it 'disallows abilities to a maintainer if the merge request was closed' do
- target_project.add_developer(user)
+ target_project.add_developer(current_user)
merge_request.close!
expect_disallowed(*maintainer_abilities)
@@ -280,12 +263,9 @@ RSpec.describe ProjectPolicy do
end
it_behaves_like 'clusterable policies' do
- let(:clusterable) { create(:project, :repository) }
- let(:cluster) do
- create(:cluster,
- :provided_by_gcp,
- :project,
- projects: [clusterable])
+ let_it_be(:clusterable) { create(:project, :repository) }
+ let_it_be(:cluster) do
+ create(:cluster, :provided_by_gcp, :project, projects: [clusterable])
end
end
@@ -356,16 +336,14 @@ RSpec.describe ProjectPolicy do
end
context 'forking a project' do
- subject { described_class.new(current_user, project) }
-
context 'anonymous user' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:fork_project) }
end
context 'project member' do
- let_it_be(:project) { create(:project, :private) }
+ let(:project) { private_project }
context 'guest' do
let(:current_user) { guest }
@@ -384,10 +362,8 @@ RSpec.describe ProjectPolicy do
end
describe 'update_max_artifacts_size' do
- subject { described_class.new(current_user, project) }
-
context 'when no user' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { expect_disallowed(:update_max_artifacts_size) }
end
@@ -416,12 +392,10 @@ RSpec.describe ProjectPolicy do
context 'alert bot' do
let(:current_user) { User.alert_bot }
- subject { described_class.new(current_user, project) }
-
it { is_expected.to be_allowed(:reporter_access) }
context 'within a private project' do
- let(:project) { create(:project, :private) }
+ let(:project) { private_project }
it { is_expected.to be_allowed(:admin_issue) }
end
@@ -430,8 +404,6 @@ RSpec.describe ProjectPolicy do
context 'support bot' do
let(:current_user) { User.support_bot }
- subject { described_class.new(current_user, project) }
-
context 'with service desk disabled' do
it { expect_allowed(:guest_access) }
it { expect_disallowed(:create_note, :read_project) }
@@ -455,8 +427,6 @@ RSpec.describe ProjectPolicy do
end
describe 'read_prometheus_alerts' do
- subject { described_class.new(current_user, project) }
-
context 'with admin' do
let(:current_user) { admin }
@@ -500,17 +470,15 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
end
describe 'metrics_dashboard feature' do
- subject { described_class.new(current_user, project) }
-
context 'public project' do
- let(:project) { create(:project, :public) }
+ let(:project) { public_project }
context 'feature private' do
context 'with reporter' do
@@ -530,7 +498,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
end
@@ -562,7 +530,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
@@ -574,7 +542,7 @@ RSpec.describe ProjectPolicy do
end
context 'internal project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { internal_project }
context 'feature private' do
context 'with reporter' do
@@ -594,7 +562,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard)}
end
@@ -626,7 +594,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
end
@@ -634,7 +602,7 @@ RSpec.describe ProjectPolicy do
end
context 'private project' do
- let(:project) { create(:project, :private) }
+ let(:project) { private_project }
context 'feature private' do
context 'with reporter' do
@@ -654,7 +622,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
end
@@ -678,7 +646,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
end
@@ -703,7 +671,7 @@ RSpec.describe ProjectPolicy do
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:metrics_dashboard) }
end
@@ -735,8 +703,6 @@ RSpec.describe ProjectPolicy do
end
describe 'create_web_ide_terminal' do
- subject { described_class.new(current_user, project) }
-
context 'with admin' do
let(:current_user) { admin }
@@ -780,20 +746,20 @@ RSpec.describe ProjectPolicy do
end
context 'with non member' do
- let(:current_user) { create(:user) }
+ let(:current_user) { non_member }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
end
describe 'read_repository_graphs' do
- subject { described_class.new(guest, project) }
+ let(:current_user) { guest }
before do
allow(subject).to receive(:allowed?).with(:read_repository_graphs).and_call_original
@@ -814,7 +780,7 @@ RSpec.describe ProjectPolicy do
end
describe 'design permissions' do
- subject { described_class.new(guest, project) }
+ let(:current_user) { guest }
let(:design_permissions) do
%i[read_design_activity read_design]
@@ -836,7 +802,7 @@ RSpec.describe ProjectPolicy do
end
describe 'read_build_report_results' do
- subject { described_class.new(guest, project) }
+ let(:current_user) { guest }
before do
allow(subject).to receive(:allowed?).with(:read_build_report_results).and_call_original
@@ -874,8 +840,6 @@ RSpec.describe ProjectPolicy do
end
describe 'read_package' do
- subject { described_class.new(current_user, project) }
-
context 'with admin' do
let(:current_user) { admin }
@@ -926,13 +890,13 @@ RSpec.describe ProjectPolicy do
end
context 'with non member' do
- let(:current_user) { create(:user) }
+ let(:current_user) { non_member }
it { is_expected.to be_allowed(:read_package) }
end
context 'with anonymous' do
- let(:current_user) { nil }
+ let(:current_user) { anonymous }
it { is_expected.to be_allowed(:read_package) }
end
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index 61787f5870e..4e8af752f3e 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -7,15 +7,34 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
let_it_be(:package) { create(:conan_package) }
let_it_be(:project) { package.project }
let_it_be(:conan_package_reference) { '123456789'}
+ let(:params) { { package_scope: :instance } }
- describe '#recipe_urls' do
- subject { described_class.new(package, user, project).recipe_urls }
-
- context 'no existing package' do
+ shared_examples 'no existing package' do
+ context 'when package does not exist' do
let(:package) { nil }
it { is_expected.to be_empty }
end
+ end
+
+ shared_examples 'conan_file_metadatum is not found' do
+ context 'when no conan_file_metadatum exists' do
+ before do
+ package.package_files.each do |file|
+ file.conan_file_metadatum.delete
+ file.reload
+ end
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#recipe_urls' do
+ subject { described_class.new(package, user, project, params).recipe_urls }
+
+ it_behaves_like 'no existing package'
+ it_behaves_like 'conan_file_metadatum is not found'
context 'existing package' do
let(:expected_result) do
@@ -33,17 +52,28 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
it { is_expected.to eq(expected_result) }
end
+
+ context 'with package_scope of project' do
+ # #recipe_file_url checks for params[:id]
+ let(:params) { { id: project.id } }
+
+ let(:expected_result) do
+ {
+ "conanfile.py" => "#{Settings.build_base_gitlab_url}/api/v4/projects/#{project.id}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ "conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/projects/#{project.id}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
end
end
describe '#recipe_snapshot' do
subject { described_class.new(package, user, project).recipe_snapshot }
- context 'no existing package' do
- let(:package) { nil }
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'no existing package'
+ it_behaves_like 'conan_file_metadatum is not found'
context 'existing package' do
let(:expected_result) do
@@ -60,17 +90,21 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
describe '#package_urls' do
let(:reference) { conan_package_reference }
+ let(:params) do
+ {
+ conan_package_reference: reference,
+ package_scope: :instance
+ }
+ end
+
subject do
described_class.new(
- package, user, project, conan_package_reference: reference
+ package, user, project, params
).package_urls
end
- context 'no existing package' do
- let(:package) { nil }
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'no existing package'
+ it_behaves_like 'conan_file_metadatum is not found'
context 'existing package' do
let(:expected_result) do
@@ -83,6 +117,26 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
it { is_expected.to eq(expected_result) }
+ context 'with package_scope of project' do
+ # #package_file_url checks for params[:id]
+ let(:params) do
+ {
+ conan_package_reference: reference,
+ id: project.id
+ }
+ end
+
+ let(:expected_result) do
+ {
+ "conaninfo.txt" => "#{Settings.build_base_gitlab_url}/api/v4/projects/#{project.id}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conaninfo.txt",
+ "conanmanifest.txt" => "#{Settings.build_base_gitlab_url}/api/v4/projects/#{project.id}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conanmanifest.txt",
+ "conan_package.tgz" => "#{Settings.build_base_gitlab_url}/api/v4/projects/#{project.id}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conan_package.tgz"
+ }
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+
context 'multiple packages with different references' do
let(:info_file) { create(:conan_package_file, :conan_package_info, package: package) }
let(:manifest_file) { create(:conan_package_file, :conan_package_manifest, package: package) }
@@ -132,11 +186,8 @@ RSpec.describe ::Packages::Conan::PackagePresenter do
).package_snapshot
end
- context 'no existing package' do
- let(:package) { nil }
-
- it { is_expected.to be_empty }
- end
+ it_behaves_like 'no existing package'
+ it_behaves_like 'conan_file_metadatum is not found'
context 'existing package' do
let(:expected_result) do
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
new file mode 100644
index 00000000000..817530f0bad
--- /dev/null
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::ConanInstancePackages do
+ include_context 'conan api setup'
+
+ describe 'GET /api/v4/packages/conan/v1/ping' do
+ let_it_be(:url) { '/packages/conan/v1/ping' }
+
+ it_behaves_like 'conan ping endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/search' do
+ let_it_be(:url) { '/packages/conan/v1/conans/search' }
+
+ it_behaves_like 'conan search endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
+ let_it_be(:url) { '/packages/conan/v1/users/authenticate' }
+
+ it_behaves_like 'conan authenticate endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
+ let_it_be(:url) { "/packages/conan/v1/users/check_credentials" }
+
+ it_behaves_like 'conan check_credentials endpoint'
+ end
+
+ context 'recipe endpoints' do
+ include_context 'conan recipe endpoints'
+
+ let(:project_id) { 9999 }
+ let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4" }
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/packages/conan/v1/conans/#{recipe_path}" }
+
+ it_behaves_like 'recipe snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" }
+
+ it_behaves_like 'package snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
+ subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do
+ subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do
+ subject { get api("/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do
+ subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
+ subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'recipe upload_urls endpoint'
+ end
+
+ describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do
+ subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'package upload_urls endpoint'
+ end
+
+ describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers}
+
+ it_behaves_like 'delete package endpoint'
+ end
+ end
+
+ context 'file download endpoints' do
+ include_context 'conan file download endpoints'
+
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/export/:file_name' do
+ subject do
+ get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'recipe file download endpoint'
+ it_behaves_like 'project not found by recipe'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ subject do
+ get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'package file download endpoint'
+ it_behaves_like 'project not found by recipe'
+ end
+ end
+
+ context 'file upload endpoints' do
+ include_context 'conan file upload endpoints'
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do
+ let(:file_name) { 'conanfile.py' }
+
+ subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do
+ let(:file_name) { 'conaninfo.txt' }
+
+ subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
+ let(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" }
+
+ it_behaves_like 'workhorse recipe file upload endpoint'
+ end
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do
+ let(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" }
+
+ it_behaves_like 'workhorse package file upload endpoint'
+ end
+ end
+end
diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb
deleted file mode 100644
index ab37c361618..00000000000
--- a/spec/requests/api/conan_packages_spec.rb
+++ /dev/null
@@ -1,986 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe API::ConanPackages do
- include WorkhorseHelpers
- include HttpBasicAuthHelpers
- include PackagesManagerApiSpecHelpers
-
- let(:package) { create(:conan_package) }
- let_it_be(:personal_access_token) { create(:personal_access_token) }
- let_it_be(:user) { personal_access_token.user }
- let(:project) { package.project }
-
- let(:base_secret) { SecureRandom.base64(64) }
- let(:auth_token) { personal_access_token.token }
- let(:job) { create(:ci_build, user: user, status: :running) }
- let(:job_token) { job.token }
- let(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
- let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
-
- let(:headers) do
- { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
- end
-
- let(:jwt_secret) do
- OpenSSL::HMAC.hexdigest(
- OpenSSL::Digest::SHA256.new,
- base_secret,
- Gitlab::ConanToken::HMAC_KEY
- )
- end
-
- before do
- project.add_developer(user)
- allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
- end
-
- describe 'GET /api/v4/packages/conan/v1/ping' do
- it 'responds with 401 Unauthorized when no token provided' do
- get api('/packages/conan/v1/ping')
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 200 OK when valid token is provided' do
- jwt = build_jwt(personal_access_token)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
- end
-
- it 'responds with 200 OK when valid job token is provided' do
- jwt = build_jwt_from_job(job)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
- end
-
- it 'responds with 200 OK when valid deploy token is provided' do
- jwt = build_jwt_from_deploy_token(deploy_token)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
- end
-
- it 'responds with 401 Unauthorized when invalid access token ID is provided' do
- jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when invalid user is provided' do
- jwt = build_jwt(personal_access_token, user_id: 12345)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
- jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when invalid JWT is provided' do
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header('invalid-jwt')
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when the job is not running' do
- job.update!(status: :failed)
- jwt = build_jwt_from_job(job)
- get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- context 'packages feature disabled' do
- it 'responds with 404 Not Found' do
- stub_packages_setting(enabled: false)
- get api('/packages/conan/v1/ping')
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/search' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
-
- get api('/packages/conan/v1/conans/search'), headers: headers, params: params
- end
-
- subject { json_response['results'] }
-
- context 'returns packages with a matching name' do
- let(:params) { { q: package.conan_recipe } }
-
- it { is_expected.to contain_exactly(package.conan_recipe) }
- end
-
- context 'returns packages using a * wildcard' do
- let(:params) { { q: "#{package.name[0, 3]}*" } }
-
- it { is_expected.to contain_exactly(package.conan_recipe) }
- end
-
- context 'does not return non-matching packages' do
- let(:params) { { q: "foo" } }
-
- it { is_expected.to be_blank }
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
- subject { get api('/packages/conan/v1/users/authenticate'), headers: headers }
-
- context 'when using invalid token' do
- let(:auth_token) { 'invalid_token' }
-
- it 'responds with 401' do
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when valid JWT access token is provided' do
- it 'responds with 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'token has valid validity time' do
- freeze_time do
- subject
-
- payload = JSONWebToken::HMACToken.decode(
- response.body, jwt_secret).first
- expect(payload['access_token']).to eq(personal_access_token.id)
- expect(payload['user_id']).to eq(personal_access_token.user_id)
-
- duration = payload['exp'] - payload['iat']
- expect(duration).to eq(1.hour)
- end
- end
- end
-
- context 'with valid job token' do
- let(:auth_token) { job_token }
-
- it 'responds with 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with valid deploy token' do
- let(:auth_token) { deploy_token.token }
-
- it 'responds with 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
- it 'responds with a 200 OK with PAT' do
- get api('/packages/conan/v1/users/check_credentials'), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'with job token' do
- let(:auth_token) { job_token }
-
- it 'responds with a 200 OK with job token' do
- get api('/packages/conan/v1/users/check_credentials'), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with deploy token' do
- let(:auth_token) { deploy_token.token }
-
- it 'responds with a 200 OK with job token' do
- get api('/packages/conan/v1/users/check_credentials'), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- it 'responds with a 401 Unauthorized when an invalid token is used' do
- get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token')
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- shared_examples 'rejects invalid recipe' do
- context 'with invalid recipe path' do
- let(:recipe_path) { '../../foo++../..' }
-
- it 'returns 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
-
- shared_examples 'rejects invalid file_name' do |invalid_file_name|
- let(:file_name) { invalid_file_name }
-
- context 'with invalid file_name' do
- it 'returns 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
-
- shared_examples 'rejects recipe for invalid project' do
- context 'with invalid recipe path' do
- let(:recipe_path) { 'aa/bb/not-existing-project/ccc' }
-
- it 'returns forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- shared_examples 'rejects recipe for not found package' do
- context 'with invalid recipe path' do
- let(:recipe_path) do
- 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
- end
-
- it 'returns not found' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- shared_examples 'empty recipe for not found package' do
- context 'with invalid recipe url' do
- let(:recipe_path) do
- 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
- end
-
- it 'returns not found' do
- allow(::Packages::Conan::PackagePresenter).to receive(:new)
- .with(
- nil,
- user,
- project,
- any_args
- ).and_return(presenter)
- allow(presenter).to receive(:recipe_snapshot) { {} }
- allow(presenter).to receive(:package_snapshot) { {} }
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq("{}")
- end
- end
- end
-
- shared_examples 'not selecting a package with the wrong type' do
- context 'with a nuget package with same name and version' do
- let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
- let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) }
- let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" }
-
- it 'calls the presenter with a nil package' do
- expect(::Packages::Conan::PackagePresenter).to receive(:new)
- .with(nil, user, project, any_args)
-
- subject
- end
- end
- end
-
- shared_examples 'recipe download_urls' do
- let(:recipe_path) { package.conan_recipe_path }
-
- it 'returns the download_urls for the recipe files' do
- expected_response = {
- 'conanfile.py' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
- 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
- }
-
- allow(presenter).to receive(:recipe_urls) { expected_response }
-
- subject
-
- expect(json_response).to eq(expected_response)
- end
-
- it_behaves_like 'not selecting a package with the wrong type'
- end
-
- shared_examples 'package download_urls' do
- let(:recipe_path) { package.conan_recipe_path }
-
- it 'returns the download_urls for the package files' do
- expected_response = {
- 'conaninfo.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
- 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
- 'conan_package.tgz' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
- }
-
- allow(presenter).to receive(:package_urls) { expected_response }
-
- subject
-
- expect(json_response).to eq(expected_response)
- end
-
- it_behaves_like 'not selecting a package with the wrong type'
- end
-
- context 'recipe endpoints' do
- let(:jwt) { build_jwt(personal_access_token) }
- let(:headers) { build_token_auth_header(jwt.encoded) }
- let(:conan_package_reference) { '123456789' }
- let(:presenter) { double('::Packages::Conan::PackagePresenter') }
-
- before do
- allow(::Packages::Conan::PackagePresenter).to receive(:new)
- .with(package, user, package.project, any_args)
- .and_return(presenter)
- end
-
- shared_examples 'rejects invalid upload_url params' do
- context 'with unaccepted json format' do
- let(:params) { %w[foo bar] }
-
- it 'returns 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
-
- shared_examples 'successful response when using Unicorn' do
- context 'on Unicorn', :unicorn do
- it 'returns successfully' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
- let(:recipe_path) { package.conan_recipe_path }
-
- subject { get api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'empty recipe for not found package'
-
- context 'with existing package' do
- it 'returns a hash of files with their md5 hashes' do
- expected_response = {
- 'conanfile.py' => 'md5hash1',
- 'conanmanifest.txt' => 'md5hash2'
- }
-
- allow(presenter).to receive(:recipe_snapshot) { expected_response }
-
- subject
-
- expect(json_response).to eq(expected_response)
- end
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do
- let(:recipe_path) { package.conan_recipe_path }
-
- subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'empty recipe for not found package'
-
- context 'with existing package' do
- it 'returns a hash of md5 values for the files' do
- expected_response = {
- 'conaninfo.txt' => "md5hash1",
- 'conanmanifest.txt' => "md5hash2",
- 'conan_package.tgz' => "md5hash3"
- }
-
- allow(presenter).to receive(:package_snapshot) { expected_response }
-
- subject
-
- expect(json_response).to eq(expected_response)
- end
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
- subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'recipe download_urls'
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do
- subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'package download_urls'
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do
- subject { get api("/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'recipe download_urls'
- end
-
- describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do
- subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects recipe for invalid project'
- it_behaves_like 'package download_urls'
- end
-
- describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
- let(:recipe_path) { package.conan_recipe_path }
-
- let(:params) do
- { 'conanfile.py': 24,
- 'conanmanifest.txt': 123 }
- end
-
- subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
-
- it 'returns a set of upload urls for the files requested' do
- subject
-
- expected_response = {
- 'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
- 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
- }
-
- expect(response.body).to eq(expected_response.to_json)
- end
-
- context 'with conan_sources and conan_export files' do
- let(:params) do
- { 'conan_sources.tgz': 345,
- 'conan_export.tgz': 234,
- 'conanmanifest.txt': 123 }
- end
-
- it 'returns upload urls for the additional files' do
- subject
-
- expected_response = {
- 'conan_sources.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
- 'conan_export.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
- 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
- }
-
- expect(response.body).to eq(expected_response.to_json)
- end
- end
-
- context 'with an invalid file' do
- let(:params) do
- { 'invalid_file.txt': 10,
- 'conanmanifest.txt': 123 }
- end
-
- it 'does not return the invalid file as an upload_url' do
- subject
-
- expected_response = {
- 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
- }
-
- expect(response.body).to eq(expected_response.to_json)
- end
- end
- end
-
- describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do
- let(:recipe_path) { package.conan_recipe_path }
-
- let(:params) do
- { 'conaninfo.txt': 24,
- 'conanmanifest.txt': 123,
- 'conan_package.tgz': 523 }
- end
-
- subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
-
- it 'returns a set of upload urls for the files requested' do
- expected_response = {
- 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
- 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
- 'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
- }
-
- subject
-
- expect(response.body).to eq(expected_response.to_json)
- end
-
- context 'with invalid files' do
- let(:params) do
- { 'conaninfo.txt': 24,
- 'invalid_file.txt': 10 }
- end
-
- it 'returns upload urls only for the valid requested files' do
- expected_response = {
- 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt"
- }
-
- subject
-
- expect(response.body).to eq(expected_response.to_json)
- end
- end
- end
-
- describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
- let(:recipe_path) { package.conan_recipe_path }
-
- subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers}
-
- it_behaves_like 'rejects invalid recipe'
-
- it 'returns unauthorized for users without valid permission' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- context 'with delete permissions' do
- before do
- project.add_maintainer(user)
- end
-
- it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_package'
-
- it 'deletes a package' do
- expect { subject }.to change { Packages::Package.count }.from(2).to(1)
- end
- end
- end
- end
-
- context 'file endpoints' do
- let(:jwt) { build_jwt(personal_access_token) }
- let(:headers) { build_token_auth_header(jwt.encoded) }
- let(:recipe_path) { package.conan_recipe_path }
-
- shared_examples 'denies download with no token' do
- context 'with no private token' do
- let(:headers) { {} }
-
- it 'returns 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
- end
-
- shared_examples 'a public project with packages' do
- it 'returns the file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
- end
-
- shared_examples 'an internal project with packages' do
- before do
- project.team.truncate
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it_behaves_like 'denies download with no token'
-
- it 'returns the file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
- end
-
- shared_examples 'a private project with packages' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
-
- it_behaves_like 'denies download with no token'
-
- it 'returns the file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
-
- it 'denies download when not enough permissions' do
- project.add_guest(user)
-
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- shared_examples 'a project is not found' do
- let(:recipe_path) { 'not/package/for/project' }
-
- it 'returns forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/export/:file_name' do
- let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') }
- let(:metadata) { recipe_file.conan_file_metadatum }
-
- subject do
- get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
- headers: headers
- end
-
- it_behaves_like 'a public project with packages'
- it_behaves_like 'an internal project with packages'
- it_behaves_like 'a private project with packages'
- it_behaves_like 'a project is not found'
- end
-
- describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
-:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
- let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') }
- let(:metadata) { package_file.conan_file_metadatum }
-
- subject do
- get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
- headers: headers
- end
-
- it_behaves_like 'a public project with packages'
- it_behaves_like 'an internal project with packages'
- it_behaves_like 'a private project with packages'
- it_behaves_like 'a project is not found'
-
- context 'tracking the conan_package.tgz download' do
- let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) }
-
- it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package'
- end
- end
- end
-
- context 'file uploads' do
- let(:jwt) { build_jwt(personal_access_token) }
- 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 } }
- let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
- let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
-
- shared_examples 'uploads a package file' do
- context 'file size above maximum limit' do
- before do
- params['file.size'] = project.actual_limits.conan_max_file_size + 1
- end
-
- it 'handles as a local file' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
-
- context 'with object storage disabled' do
- context 'without a file from workhorse' do
- let(:params) { { file: nil } }
-
- it 'rejects the request' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
-
- context 'with a file' do
- it_behaves_like 'package workhorse uploads'
- end
-
- context 'without a token' do
- it 'rejects request without a token' do
- headers_with_token.delete('HTTP_AUTHORIZATION')
-
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when params from workhorse are correct' do
- it 'creates package and stores package file' do
- expect { subject }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
-
- package_file = project.packages.last.package_files.reload.last
- expect(package_file.file_name).to eq(params[:file].original_filename)
- end
-
- it "doesn't attempt to migrate file to object storage" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
- end
- end
-
- context 'with object storage enabled' do
- context 'and direct upload enabled' do
- let!(:fog_connection) do
- stub_package_file_object_storage(direct_upload: true)
- end
-
- let(:tmp_object) do
- fog_connection.directories.new(key: 'packages').files.create(
- key: "tmp/uploads/#{file_name}",
- body: 'content'
- )
- end
-
- let(:fog_file) { fog_to_uploaded_file(tmp_object) }
-
- ['123123', '../../123123'].each do |remote_id|
- context "with invalid remote_id: #{remote_id}" do
- let(:params) do
- {
- file: fog_file,
- 'file.remote_id' => remote_id
- }
- end
-
- it 'responds with status 403' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- context 'with valid remote_id' do
- let(:params) do
- {
- file: fog_file,
- 'file.remote_id' => file_name
- }
- end
-
- it 'creates package and stores package file' do
- expect { subject }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
-
- package_file = project.packages.last.package_files.reload.last
- expect(package_file.file_name).to eq(params[:file].original_filename)
- expect(package_file.file.read).to eq('content')
- end
- end
- end
-
- it_behaves_like 'background upload schedules a file migration'
- end
- end
-
- shared_examples 'workhorse authorization' do
- it 'authorizes posting package with a valid token' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- end
-
- it 'rejects request without a valid token' do
- headers_with_token['HTTP_AUTHORIZATION'] = 'foo'
-
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'rejects request without a valid permission' do
- project.add_guest(user)
-
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- it 'rejects requests that bypassed gitlab-workhorse' do
- headers_with_token.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_package_file_object_storage(enabled: true, direct_upload: true)
- end
-
- it 'responds with status 200, location of package remote store and object details' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).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']).not_to have_key('MultipartUpload')
- end
- end
-
- context 'when direct upload is disabled' do
- before do
- stub_package_file_object_storage(enabled: true, direct_upload: false)
- end
-
- it 'handles as a local file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path)
- expect(json_response['RemoteObject']).to be_nil
- end
- end
- end
- end
-
- describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do
- let(:file_name) { 'conanfile.py' }
-
- subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
- it_behaves_like 'workhorse authorization'
- end
-
- describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do
- let(:file_name) { 'conaninfo.txt' }
-
- subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
- it_behaves_like 'workhorse authorization'
- end
-
- describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
- let(:file_name) { 'conanfile.py' }
- let(:params) { { file: temp_file(file_name) } }
-
- subject do
- workhorse_finalize(
- "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}",
- method: :put,
- file_key: :file,
- params: params,
- send_rewritten_field: true,
- headers: headers_with_token
- )
- end
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
- it_behaves_like 'uploads a package file'
- end
-
- describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do
- let(:file_name) { 'conaninfo.txt' }
- let(:params) { { file: temp_file(file_name) } }
-
- subject do
- workhorse_finalize(
- "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}",
- method: :put,
- file_key: :file,
- params: params,
- headers: headers_with_token,
- send_rewritten_field: true
- )
- end
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
- it_behaves_like 'uploads a package file'
-
- context 'tracking the conan_package.tgz upload' do
- let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
-
- it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
- end
- end
- end
-end
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
new file mode 100644
index 00000000000..fefaf9790b1
--- /dev/null
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::ConanProjectPackages do
+ include_context 'conan api setup'
+
+ let(:project_id) { project.id }
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/ping' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/ping" }
+
+ it_behaves_like 'conan ping endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/search' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/conans/search" }
+
+ it_behaves_like 'conan search endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/users/authenticate" }
+
+ it_behaves_like 'conan authenticate endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/users/check_credentials' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/users/check_credentials" }
+
+ it_behaves_like 'conan check_credentials endpoint'
+ end
+
+ context 'recipe endpoints' do
+ include_context 'conan recipe endpoints'
+
+ let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4/projects/#{project_id}" }
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}" }
+
+ it_behaves_like 'recipe snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" }
+
+ it_behaves_like 'package snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
+ subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'recipe upload_urls endpoint'
+ end
+
+ describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do
+ subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'package upload_urls endpoint'
+ end
+
+ describe 'DELETE /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ subject { delete api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}"), headers: headers}
+
+ it_behaves_like 'delete package endpoint'
+ end
+ end
+
+ context 'file download endpoints' do
+ include_context 'conan file download endpoints'
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/export/:file_name' do
+ subject do
+ get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'recipe file download endpoint'
+ it_behaves_like 'project not found by project id'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ subject do
+ get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'package file download endpoint'
+ it_behaves_like 'project not found by project id'
+ end
+ end
+
+ context 'file upload endpoints' do
+ include_context 'conan file upload endpoints'
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do
+ let(:file_name) { 'conanfile.py' }
+
+ subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do
+ let(:file_name) { 'conaninfo.txt' }
+
+ subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
+ let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" }
+
+ it_behaves_like 'workhorse recipe file upload endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do
+ let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" }
+
+ it_behaves_like 'workhorse package file upload endpoint'
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index df95835ae94..b4705c4cc55 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -49,15 +49,15 @@ end
RSpec.describe API::Projects do
include ProjectForksHelper
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace) }
- let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
- let(:user4) { create(:user, username: 'user.with.dot') }
- let(:project3) do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project, reload: true) { create(:project, :repository, namespace: user.namespace) }
+ let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
+ let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
+ let_it_be(:user4) { create(:user, username: 'user.with.dot') }
+ let_it_be(:project3, reload: true) do
create(:project,
:private,
:repository,
@@ -71,14 +71,14 @@ RSpec.describe API::Projects do
snippets_enabled: false)
end
- let(:project_member2) do
+ let_it_be(:project_member2) do
create(:project_member,
user: user4,
project: project3,
access_level: ProjectMember::MAINTAINER)
end
- let(:project4) do
+ let_it_be(:project4, reload: true) do
create(:project,
name: 'third_project',
path: 'third_project',
@@ -86,6 +86,8 @@ RSpec.describe API::Projects do
namespace: user4.namespace)
end
+ let(:user_projects) { [public_project, project, project2, project3] }
+
shared_context 'with language detection' do
let(:ruby) { create(:programming_language, name: 'Ruby') }
let(:javascript) { create(:programming_language, name: 'JavaScript') }
@@ -146,14 +148,7 @@ RSpec.describe API::Projects do
end
end
- let!(:public_project) { create(:project, :public, name: 'public_project') }
-
- before do
- project
- project2
- project3
- project4
- end
+ let_it_be(:public_project) { create(:project, :public, name: 'public_project') }
context 'when unauthenticated' do
it_behaves_like 'projects response' do
@@ -171,7 +166,7 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { {} }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
it_behaves_like 'projects response without N + 1 queries' do
@@ -257,7 +252,7 @@ RSpec.describe API::Projects do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- statistics = json_response.first['statistics']
+ statistics = json_response.find { |p| p['id'] == project.id }['statistics']
expect(statistics).to be_present
expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size')
end
@@ -386,14 +381,14 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } }
+ let(:projects) { user_projects.select { |p| p.id > project2.id } }
end
context 'regression: empty string is ignored' do
it_behaves_like 'projects response' do
let(:filter) { { id_after: '' } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
end
end
@@ -402,14 +397,14 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } }
+ let(:projects) { user_projects.select { |p| p.id < project2.id } }
end
context 'regression: empty string is ignored' do
it_behaves_like 'projects response' do
let(:filter) { { id_before: '' } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
end
end
@@ -418,7 +413,7 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id, id_after: public_project.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id && p.id > public_project.id } }
+ let(:projects) { user_projects.select { |p| p.id < project2.id && p.id > public_project.id } }
end
end
@@ -481,7 +476,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(project3.id)
+ expect(json_response.map { |p| p['id'] }).to eq(user_projects.map(&:id).sort.reverse)
end
end
@@ -501,7 +496,6 @@ RSpec.describe API::Projects do
let(:public_project) { create(:project, :public) }
before do
- project_member
user3.update!(starred_projects: [project, project2, project3, public_project])
end
@@ -642,7 +636,6 @@ RSpec.describe API::Projects do
context 'non-admin user' do
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
it 'returns projects ordered normally' do
get api('/projects', current_user), params: { order_by: order_by }
@@ -650,7 +643,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).to eq(projects.map(&:id).reverse)
+ expect(json_response.map { |project| project['id'] }).to eq(user_projects.map(&:id).sort.reverse)
end
end
end
@@ -686,7 +679,8 @@ RSpec.describe API::Projects do
context 'with keyset pagination' do
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:first_project_id) { user_projects.map(&:id).min }
+ let(:last_project_id) { user_projects.map(&:id).max }
context 'headers and records' do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } }
@@ -696,11 +690,11 @@ RSpec.describe API::Projects do
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_after=#{public_project.id}")
+ expect(response.header['Links']).to include("id_after=#{first_project_id}")
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
- expect(response.header['Link']).to include("id_after=#{public_project.id}")
+ expect(response.header['Link']).to include("id_after=#{first_project_id}")
end
it 'contains only the first project with per_page = 1' do
@@ -708,7 +702,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
- expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(first_project_id)
end
it 'still includes a link if the end has reached and there is no more data after this page' do
@@ -752,11 +746,11 @@ RSpec.describe API::Projects do
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_before=#{project3.id}")
+ expect(response.header['Links']).to include("id_before=#{last_project_id}")
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
- expect(response.header['Link']).to include("id_before=#{project3.id}")
+ expect(response.header['Link']).to include("id_before=#{last_project_id}")
end
it 'contains only the last project with per_page = 1' do
@@ -764,7 +758,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
- expect(json_response.map { |p| p['id'] }).to contain_exactly(project3.id)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(last_project_id)
end
end
@@ -793,7 +787,7 @@ RSpec.describe API::Projects do
ids += Gitlab::Json.parse(response.body).map { |p| p['id'] }
end
- expect(ids).to contain_exactly(*projects.map(&:id))
+ expect(ids).to contain_exactly(*user_projects.map(&:id))
end
end
end
@@ -814,7 +808,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project')
@@ -825,7 +819,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('foo_project')
expect(project.path).to eq('foo_project')
@@ -836,7 +830,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('path-project-Foo')
@@ -1985,7 +1979,8 @@ RSpec.describe API::Projects do
context 'when authenticated' do
context 'valid request' do
it_behaves_like 'project users response' do
- let(:current_user) { user }
+ let(:project) { project4 }
+ let(:current_user) { user4 }
end
end
@@ -2011,8 +2006,8 @@ RSpec.describe API::Projects do
get api("/projects/#{project.id}/users?skip_users=#{user.id}", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(1)
- expect(json_response[0]['id']).to eq(other_user.id)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |m| m['id'] }).not_to include(user.id)
end
end
end
diff --git a/spec/services/static_site_editor/config_service_spec.rb b/spec/services/static_site_editor/config_service_spec.rb
new file mode 100644
index 00000000000..5fff4e0af53
--- /dev/null
+++ b/spec/services/static_site_editor/config_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe StaticSiteEditor::ConfigService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ # params
+ let(:ref) { double(:ref) }
+ let(:path) { double(:path) }
+ let(:return_url) { double(:return_url) }
+
+ # stub data
+ let(:generated_data) { { generated: true } }
+ let(:file_data) { { file: true } }
+
+ describe '#execute' do
+ subject(:execute) do
+ described_class.new(
+ container: project,
+ current_user: user,
+ params: {
+ ref: ref,
+ path: path,
+ return_url: return_url
+ }
+ ).execute
+ end
+
+ context 'when insufficient permission' do
+ it 'returns an error' do
+ expect(execute).to be_error
+ expect(execute.message).to eq('Insufficient permissions to read configuration')
+ end
+ end
+
+ context 'for developer' do
+ before do
+ project.add_developer(user)
+
+ allow_next_instance_of(Gitlab::StaticSiteEditor::Config::GeneratedConfig) do |config|
+ allow(config).to receive(:data) { generated_data }
+ end
+
+ allow_next_instance_of(Gitlab::StaticSiteEditor::Config::FileConfig) do |config|
+ allow(config).to receive(:data) { file_data }
+ end
+ end
+
+ it 'returns merged generated data and config file data' do
+ expect(execute).to be_success
+ expect(execute.payload).to eq(generated: true, file: true)
+ end
+
+ it 'returns an error if any keys would be overwritten by the merge' do
+ generated_data[:duplicate_key] = true
+ file_data[:duplicate_key] = true
+ expect(execute).to be_error
+ expect(execute.message).to match(/duplicate key.*duplicate_key.*found/i)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index dd08d81bc46..3016494ac8d 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -1,13 +1,17 @@
# frozen_string_literal: true
RSpec.shared_context 'ProjectPolicy context' do
+ let_it_be(:anonymous) { nil }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
- let(:project) { create(:project, :public, namespace: owner.namespace) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be_with_refind(:private_project) { create(:project, :private, namespace: owner.namespace) }
+ let_it_be_with_refind(:internal_project) { create(:project, :internal, namespace: owner.namespace) }
+ let_it_be_with_refind(:public_project) { create(:project, :public, namespace: owner.namespace) }
let(:base_guest_permissions) do
%i[
@@ -86,10 +90,12 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions }
let(:owner_permissions) { base_owner_permissions + additional_owner_permissions }
- before do
- project.add_guest(guest)
- project.add_maintainer(maintainer)
- project.add_developer(developer)
- project.add_reporter(reporter)
+ before_all do
+ [private_project, internal_project, public_project].each do |project|
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
end
end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
new file mode 100644
index 00000000000..580ebf00dcb
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'conan api setup' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:package) { create(:conan_package) }
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:user) { personal_access_token.user }
+ let_it_be(:base_secret) { SecureRandom.base64(64) }
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job_token) { job.token }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+
+ let(:project) { package.project }
+ let(:auth_token) { personal_access_token.token }
+ let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+
+ let(:headers) do
+ { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
+ end
+
+ let(:jwt_secret) do
+ OpenSSL::HMAC.hexdigest(
+ OpenSSL::Digest::SHA256.new,
+ base_secret,
+ Gitlab::ConanToken::HMAC_KEY
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
+ end
+end
+
+RSpec.shared_context 'conan recipe endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ let(:headers) { build_token_auth_header(jwt.encoded) }
+ let(:conan_package_reference) { '123456789' }
+ let(:presenter) { double('::Packages::Conan::PackagePresenter') }
+
+ before do
+ allow(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(package, user, package.project, any_args)
+ .and_return(presenter)
+ end
+end
+
+RSpec.shared_context 'conan file download endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ let(:headers) { build_token_auth_header(jwt.encoded) }
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') }
+ let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') }
+ let(:metadata) { package_file.conan_file_metadatum }
+end
+
+RSpec.shared_context 'conan file upload endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include WorkhorseHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ 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 } }
+ let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
+ let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index d8476f5dcc2..d05e5eb9120 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -59,8 +59,7 @@ RSpec.shared_examples 'project policies as anonymous' do
let(:project) { create(:project, :public, namespace: group) }
let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
let(:anonymous_permissions) { guest_permissions - user_permissions }
-
- subject { described_class.new(nil, project) }
+ let(:current_user) { anonymous }
before do
create(:group_member, :invited, group: group)
@@ -78,9 +77,8 @@ RSpec.shared_examples 'project policies as anonymous' do
end
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(nil, project) }
+ let(:project) { private_project }
+ let(:current_user) { anonymous }
it { is_expected.to be_banned }
end
@@ -109,10 +107,10 @@ RSpec.shared_examples 'deploy token does not get confused with user' do
end
RSpec.shared_examples 'project policies as guest' do
- subject { described_class.new(guest, project) }
-
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { private_project }
+ let(:current_user) { guest }
+
let(:reporter_public_build_permissions) do
reporter_permissions - [:read_build, :read_pipeline]
end
@@ -167,9 +165,8 @@ end
RSpec.shared_examples 'project policies as reporter' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(reporter, project) }
+ let(:project) { private_project }
+ let(:current_user) { reporter }
it do
expect_allowed(*guest_permissions)
@@ -192,9 +189,8 @@ end
RSpec.shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(developer, project) }
+ let(:project) { private_project }
+ let(:current_user) { developer }
it do
expect_allowed(*guest_permissions)
@@ -217,9 +213,8 @@ end
RSpec.shared_examples 'project policies as maintainer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(maintainer, project) }
+ let(:project) { private_project }
+ let(:current_user) { maintainer }
it do
expect_allowed(*guest_permissions)
@@ -242,9 +237,8 @@ end
RSpec.shared_examples 'project policies as owner' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(owner, project) }
+ let(:project) { private_project }
+ let(:current_user) { owner }
it do
expect_allowed(*guest_permissions)
@@ -267,9 +261,8 @@ end
RSpec.shared_examples 'project policies as admin with admin mode' do
context 'abilities for non-public projects', :enable_admin_mode do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(admin, project) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it do
expect_allowed(*guest_permissions)
@@ -316,9 +309,8 @@ end
RSpec.shared_examples 'project policies as admin without admin mode' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(admin, project) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it { is_expected.to be_banned }
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
new file mode 100644
index 00000000000..c56290a0aa9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -0,0 +1,843 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'conan ping endpoint' do
+ it 'responds with 401 Unauthorized when no token provided' do
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 200 OK when valid token is provided' do
+ jwt = build_jwt(personal_access_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid job token is provided' do
+ jwt = build_jwt_from_job(job)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid deploy token is provided' do
+ jwt = build_jwt_from_deploy_token(deploy_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 401 Unauthorized when invalid access token ID is provided' do
+ jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid user is provided' do
+ jwt = build_jwt(personal_access_token, user_id: 12345)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
+ jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid JWT is provided' do
+ get api(url), headers: build_token_auth_header('invalid-jwt')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'packages feature disabled' do
+ it 'responds with 404 Not Found' do
+ stub_packages_setting(enabled: false)
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan search endpoint' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+
+ get api(url), headers: headers, params: params
+ end
+
+ subject { json_response['results'] }
+
+ context 'returns packages with a matching name' do
+ let(:params) { { q: package.conan_recipe } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'returns packages using a * wildcard' do
+ let(:params) { { q: "#{package.name[0, 3]}*" } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'does not return non-matching packages' do
+ let(:params) { { q: "foo" } }
+
+ it { is_expected.to be_blank }
+ end
+end
+
+RSpec.shared_examples 'conan authenticate endpoint' do
+ subject { get api(url), headers: headers }
+
+ context 'when using invalid token' do
+ let(:auth_token) { 'invalid_token' }
+
+ it 'responds with 401' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when valid JWT access token is provided' do
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'token has valid validity time' do
+ freeze_time do
+ subject
+
+ payload = JSONWebToken::HMACToken.decode(
+ response.body, jwt_secret).first
+ expect(payload['access_token']).to eq(personal_access_token.id)
+ expect(payload['user_id']).to eq(personal_access_token.user_id)
+
+ duration = payload['exp'] - payload['iat']
+ expect(duration).to eq(1.hour)
+ end
+ end
+ end
+
+ context 'with valid job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with valid deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan check_credentials endpoint' do
+ it 'responds with a 200 OK with PAT' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'with job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'responds with a 401 Unauthorized when an invalid token is used' do
+ get api(url), headers: build_token_auth_header('invalid-token')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+end
+
+RSpec.shared_examples 'rejects invalid recipe' do
+ context 'with invalid recipe path' do
+ let(:recipe_path) { '../../foo++../..' }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name|
+ let(:file_name) { invalid_file_name }
+
+ context 'with invalid file_name' do
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects recipe for invalid project' do
+ context 'with invalid project' do
+ let(:recipe_path) { 'aa/bb/cc/dd' }
+ let(:project_id) { 9999 }
+
+ it_behaves_like 'not found request'
+ end
+end
+
+RSpec.shared_examples 'empty recipe for not found package' do
+ context 'with invalid recipe url' do
+ let(:recipe_path) do
+ 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ end
+
+ it 'returns not found' do
+ allow(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(
+ nil,
+ user,
+ project,
+ any_args
+ ).and_return(presenter)
+ allow(presenter).to receive(:recipe_snapshot) { {} }
+ allow(presenter).to receive(:package_snapshot) { {} }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq("{}")
+ end
+ end
+end
+
+RSpec.shared_examples 'not selecting a package with the wrong type' do
+ context 'with a nuget package with same name and version' do
+ let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) }
+ let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" }
+
+ it 'calls the presenter with a nil package' do
+ expect(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(nil, user, project, any_args)
+
+ subject
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the recipe files' do
+ expected_response = {
+ 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ allow(presenter).to receive(:recipe_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'package download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the package files' do
+ expected_response = {
+ 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ allow(presenter).to receive(:package_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'rejects invalid upload_url params' do
+ context 'with unaccepted json format' do
+ let(:params) { %w[foo bar] }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'successful response when using Unicorn' do
+ context 'on Unicorn', :unicorn do
+ it 'returns successfully' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of files with their md5 hashes' do
+ expected_response = {
+ 'conanfile.py' => 'md5hash1',
+ 'conanmanifest.txt' => 'md5hash2'
+ }
+
+ allow(presenter).to receive(:recipe_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'package snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of md5 values for the files' do
+ expected_response = {
+ 'conaninfo.txt' => "md5hash1",
+ 'conanmanifest.txt' => "md5hash2",
+ 'conan_package.tgz' => "md5hash3"
+ }
+
+ allow(presenter).to receive(:package_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'recipe download_urls'
+end
+
+RSpec.shared_examples 'package download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'package download_urls'
+end
+
+RSpec.shared_examples 'recipe upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conanfile.py': 24,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ subject
+
+ expected_response = {
+ 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with conan_sources and conan_export files' do
+ let(:params) do
+ { 'conan_sources.tgz': 345,
+ 'conan_export.tgz': 234,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'returns upload urls for the additional files' do
+ subject
+
+ expected_response = {
+ 'conan_sources.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
+ 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+
+ context 'with an invalid file' do
+ let(:params) do
+ { 'invalid_file.txt': 10,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'does not return the invalid file as an upload_url' do
+ subject
+
+ expected_response = {
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'package upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'conanmanifest.txt': 123,
+ 'conan_package.tgz': 523 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with invalid files' do
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'invalid_file.txt': 10 }
+ end
+
+ it 'returns upload urls only for the valid requested files' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'delete package endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it_behaves_like 'rejects invalid recipe'
+
+ it 'returns unauthorized for users without valid permission' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'with delete permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package'
+
+ it 'deletes a package' do
+ expect { subject }.to change { Packages::Package.count }.from(2).to(1)
+ end
+ end
+end
+
+RSpec.shared_examples 'denies download with no token' do
+ context 'with no private token' do
+ let(:headers) { {} }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+end
+
+RSpec.shared_examples 'a public project with packages' do
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'an internal project with packages' do
+ before do
+ project.team.truncate
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'a private project with packages' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it 'denies download when not enough permissions' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+end
+
+RSpec.shared_examples 'not found request' do
+ it 'returns not found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+end
+
+RSpec.shared_examples 'recipe file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+end
+
+RSpec.shared_examples 'package file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+
+ context 'tracking the conan_package.tgz download' do
+ let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package'
+ end
+end
+
+RSpec.shared_examples 'project not found by recipe' do
+ let(:recipe_path) { 'not/package/for/project' }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'project not found by project id' do
+ let(:project_id) { 99999 }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'workhorse authorize endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'workhorse authorization'
+end
+
+RSpec.shared_examples 'workhorse recipe file upload endpoint' do
+ let(:file_name) { 'conanfile.py' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'uploads a package file'
+end
+
+RSpec.shared_examples 'workhorse package file upload endpoint' do
+ let(:file_name) { 'conaninfo.txt' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
+ it_behaves_like 'uploads a package file'
+
+ context 'tracking the conan_package.tgz upload' do
+ let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package'
+ end
+end
+
+RSpec.shared_examples 'uploads a package file' do
+ context 'file size above maximum limit' do
+ before do
+ params['file.size'] = project.actual_limits.conan_max_file_size + 1
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with object storage disabled' do
+ context 'without a file from workhorse' do
+ let(:params) { { file: nil } }
+
+ it 'rejects the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with a file' do
+ it_behaves_like 'package workhorse uploads'
+ end
+
+ context 'without a token' do
+ it 'rejects request without a token' do
+ headers_with_token.delete('HTTP_AUTHORIZATION')
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when params from workhorse are correct' do
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ end
+
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => remote_id
+ }
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => file_name
+ }
+ end
+
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ expect(package_file.file.read).to eq('content')
+ end
+ end
+ end
+
+ it_behaves_like 'background upload schedules a file migration'
+ end
+end
+
+RSpec.shared_examples 'workhorse authorization' do
+ it 'authorizes posting package with a valid token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'rejects request without a valid token' do
+ headers_with_token['HTTP_AUTHORIZATION'] = 'foo'
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'rejects request without a valid permission' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'rejects requests that bypassed gitlab-workhorse' do
+ headers_with_token.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_package_file_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it 'responds with status 200, location of package remote store and object details' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).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']).not_to have_key('MultipartUpload')
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index 0dfc49f2d41..f31cbcfdec1 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -14,8 +14,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
- expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id
+ expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id)
end
end
@@ -40,7 +39,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
end
end
@@ -50,16 +49,15 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
- expect(json_response.second).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
get api("/#{attributable_name}", admin), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
attributable_response = json_response.find { |r| r['id'] == attributable.id }
other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id }
diff --git a/yarn.lock b/yarn.lock
index 4782fed4382..1b86922873e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.164.0.tgz#6cefad871c45f945ef92b99015d0f510b1d2de4a"
integrity sha512-a9e/cYUc1QQk7azjH4x/m6/p3icavwGEi5F9ipNlDqiJtUor5tqojxvMxPOhuVbN/mTwnC6lGsSZg4tqTsdJAQ==
-"@gitlab/ui@21.2.1":
- version "21.2.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.2.1.tgz#260e308e82374224095aea59a891f8a458eace3e"
- integrity sha512-9ne5kPMGCVHSSeWmMzcnXgB4JPKhbVQ520FvM+z3MkvTaQrxhh4H/wKD4durBusTokbrRXsOhqtuV/NI1WJjDQ==
+"@gitlab/ui@21.3.0":
+ version "21.3.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.3.0.tgz#1cdcfed11e9aa730af1b1a9102390cab81c36928"
+ integrity sha512-6k02S4y1wNnxmPpHOe5WA0FIeAfzs2M06Xz1CCYQ+7M7P0PBaG0FFW+uvWyVnnp5CUaL2frMBn9TXD9pXdOeKg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"