Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-15 21:09:35 +00:00
parent f784f7d3b1
commit 7dc8bd3c16
69 changed files with 2194 additions and 1420 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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),
},
});
},

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -0,0 +1,5 @@
---
title: Allow Conan packages to be scoped to project-level
merge_request: 39541
author:
type: added

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fail API Fuzzing CI/CD job when scanner errors
merge_request: 41616
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Updated gitlab:usage_data:dump_sql_in_yaml rake task with redis usage
merge_request: 42189
author:
type: changed

View File

@ -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

View File

@ -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`.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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/). |

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 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 build_recipe_file_upload_url(file_name)
recipe_file_url(url_options(file_name))
end
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

View File

@ -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"
#

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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,
};

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }
let(:project) { public_project }
before do
project.add_guest(guest)
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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 }

View File

@ -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"