Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-16 12:09:06 +00:00
parent 613868af23
commit 0045970352
53 changed files with 900 additions and 1464 deletions

View File

@ -31,9 +31,7 @@ Rails/InverseOf:
- 'app/models/jira_connect_subscription.rb'
- 'app/models/members/group_member.rb'
- 'app/models/members/project_member.rb'
- 'app/models/merge_request.rb'
- 'app/models/merge_request/metrics.rb'
- 'app/models/merge_request_diff.rb'
- 'app/models/namespace.rb'
- 'app/models/notification_setting.rb'
- 'app/models/packages/composer/cache_file.rb'
@ -60,7 +58,6 @@ Rails/InverseOf:
- 'ee/app/models/ee/clusters/agent.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/group.rb'
- 'ee/app/models/ee/merge_request.rb'
- 'ee/app/models/ee/plan.rb'
- 'ee/app/models/ee/project.rb'
- 'ee/app/models/ee/service_desk_setting.rb'

View File

@ -1 +1 @@
11825b3ad89525b194cc4095e581eef843377cdb
0b0e46fe69e9b94e3def2a9318188fcc0f3f00c6

View File

@ -4,17 +4,6 @@
box-shadow: inset 0 0 0 $gl-border-size-1 $red-500 if-important($important);
}
.timezone-dropdown {
.gl-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
.btn-block {
margin-bottom: 0;
}
}
.modal-footer {
@include gl-bg-gray-10;
}
@ -52,65 +41,17 @@ $scroll-top-gradient: linear-gradient(to bottom, $gradient-dark-gray 0%, $gradie
$scroll-bottom-gradient: linear-gradient(to bottom, $gradient-gray 0%, $gradient-dark-gray 100%);
$column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradient-gray 100%);
.schedule-shell {
@include gl-relative;
@include gl-h-full;
@include gl-w-full;
@include gl-overflow-x-auto;
}
.timeline-section {
@include gl-sticky;
@include gl-top-0;
z-index: 20;
.timeline-header-label,
.timeline-header-item {
@include gl-float-left;
}
.timeline-header-label {
@include gl-sticky;
@include gl-top-0;
@include gl-left-0;
width: $details-cell-width;
z-index: 2;
}
.timeline-header-item {
.item-sublabel .sublabel-value {
color: var(--gray-700, $gray-700);
@include gl-font-weight-normal;
&.label-dark {
color: var(--gray-900, $gray-900);
}
&.label-bold {
@include gl-font-weight-bold;
}
}
.item-sublabel {
@include gl-relative;
@include gl-display-flex;
.sublabel-value {
@include gl-flex-grow-1;
@include gl-flex-basis-0;
text-align: center;
@include gl-font-base;
}
}
.current-day-indicator-header {
@include gl-absolute;
@include gl-bottom-0;
height: $grid-size;
width: $grid-size;
background-color: var(--red-500, $red-500);
@include gl-rounded-full;
transform: translate(-50%, 50%);
}
@ -137,35 +78,19 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
.details-cell,
.timeline-cell {
@include gl-float-left;
height: $item-height;
}
.details-cell {
@include gl-sticky;
@include gl-left-0;
width: $details-cell-width;
@include gl-font-base;
z-index: 10;
}
.timeline-cell {
@include gl-relative;
@include gl-bg-transparent;
border-right: $border-style;
&:last-child {
@include gl-border-r-0;
}
.current-day-indicator {
@include gl-absolute;
top: -1px;
width: $gl-spacing-scale-1;
height: calc(100% + 1px);
background-color: var(--red-500, $red-500);
@include gl-pointer-events-none;
transform: translateX(-50%);
}
}

View File

@ -2,7 +2,9 @@
module AlertManagement
class HttpIntegrationsFinder
def initialize(project, params)
TYPE_IDENTIFIERS = ::AlertManagement::HttpIntegration.type_identifiers
def initialize(project, params = {})
@project = project
@params = params
end
@ -13,6 +15,7 @@ module AlertManagement
filter_by_availability
filter_by_endpoint_identifier
filter_by_active
filter_by_type
collection
end
@ -21,15 +24,13 @@ module AlertManagement
attr_reader :project, :params, :collection
# Overridden in EE
def filter_by_availability
return if multiple_alert_http_integrations?
first_id = project.alert_management_http_integrations
.ordered_by_id
.select(:id)
.limit(1)
@collection = collection.id_in(first_id)
# Re-find by id so subsequent filters don't expose unavailable records
@collection = collection.id_in(collection
.select('DISTINCT ON (type_identifier) id')
.ordered_by_type_and_id
.limit(TYPE_IDENTIFIERS.length))
end
def filter_by_endpoint_identifier
@ -44,9 +45,11 @@ module AlertManagement
@collection = collection.active
end
# Overridden in EE
def multiple_alert_http_integrations?
false
def filter_by_type
return unless params[:type_identifier]
return unless TYPE_IDENTIFIERS.include?(params[:type_identifier])
@collection = collection.for_type(params[:type_identifier])
end
end
end

View File

@ -3,8 +3,8 @@
module AlertManagement
class HttpIntegration < ApplicationRecord
include ::Gitlab::Routing
LEGACY_IDENTIFIER = 'legacy'
DEFAULT_NAME_SLUG = 'http-endpoint'
belongs_to :project, inverse_of: :alert_management_http_integrations
@ -19,6 +19,7 @@ module AlertManagement
validates :active, inclusion: { in: [true, false] }
validates :token, presence: true, format: { with: /\A\h{32}\z/ }
validates :name, presence: true, length: { maximum: 255 }
validates :type_identifier, presence: true
validates :endpoint_identifier, presence: true, length: { maximum: 255 }, format: { with: /\A[A-Za-z0-9]+\z/ }
validates :endpoint_identifier, uniqueness: { scope: [:project_id, :active] }, if: :active?
validates :payload_attribute_mapping, json_schema: { filename: 'http_integration_payload_attribute_mapping' }
@ -29,15 +30,30 @@ module AlertManagement
before_validation :ensure_payload_example_not_nil
scope :for_endpoint_identifier, ->(endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) }
scope :for_type, ->(type) { where(type_identifier: type) }
scope :for_project, ->(project_ids) { where(project: project_ids) }
scope :active, -> { where(active: true) }
scope :ordered_by_id, -> { order(:id) }
scope :legacy, -> { for_endpoint_identifier(LEGACY_IDENTIFIER) }
scope :ordered_by_type_and_id, -> { order(:type_identifier, :id) }
enum type_identifier: {
http: 0,
prometheus: 1
}
def url
return project_alerts_notify_url(project, format: :json) if legacy?
if legacy?
return project_alerts_notify_url(project, format: :json) if http?
return notify_project_prometheus_alerts_url(project, format: :json) if prometheus?
end
project_alert_http_integration_url(project, name_slug, endpoint_identifier, format: :json)
end
def legacy?
endpoint_identifier == LEGACY_IDENTIFIER
end
private
def self.generate_token
@ -45,11 +61,7 @@ module AlertManagement
end
def name_slug
(name && Gitlab::Utils.slugify(name)) || DEFAULT_NAME_SLUG
end
def legacy?
endpoint_identifier == LEGACY_IDENTIFIER
(name && Gitlab::Utils.slugify(name)) || "#{type_identifier}-endpoint"
end
# Blank token assignment triggers token reset

View File

@ -95,9 +95,9 @@ class MergeRequest < ApplicationRecord
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline', inverse_of: :merge_request
has_many :suggestions, through: :notes
has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note'
has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note', inverse_of: :noteable
has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees

View File

@ -32,7 +32,7 @@ class MergeRequestDiff < ApplicationRecord
-> { order(:merge_request_diff_id, :relative_order) },
inverse_of: :merge_request_diff
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }, inverse_of: :merge_request_diff
validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true
validates :merge_request_id, uniqueness: { scope: :diff_type }, if: :merge_head?

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module AlertManagement
module HttpIntegrations
class BaseService < BaseProjectService
# @param project [Project]
# @param current_user [User]
# @param params [Hash]
def initialize(project, current_user, params)
@response = nil
super(project: project, current_user: current_user, params: params.with_indifferent_access)
end
private
def allowed?
current_user&.can?(:admin_operations, project)
end
def too_many_integrations?(integration)
AlertManagement::HttpIntegration
.for_project(integration.project_id)
.for_type(integration.type_identifier)
.id_not_in(integration.id)
.any?
end
def permitted_params
params.slice(*permitted_params_keys)
end
# overriden in EE
def permitted_params_keys
%i[name active type_identifier]
end
def error(message)
ServiceResponse.error(message: message)
end
def success(integration)
ServiceResponse.success(payload: { integration: integration.reset })
end
def error_multiple_integrations
error(_('Multiple integrations of a single type are not supported for this project'))
end
def error_on_save(integration)
error(integration.errors.full_messages.to_sentence)
end
end
end
end
::AlertManagement::HttpIntegrations::BaseService.prepend_mod

View File

@ -2,68 +2,34 @@
module AlertManagement
module HttpIntegrations
class CreateService
# @param project [Project]
# @param current_user [User]
# @param params [Hash]
def initialize(project, current_user, params)
@project = project
@current_user = current_user
@params = params.with_indifferent_access
end
class CreateService < BaseService
def execute
return error_no_permissions unless allowed?
return error_multiple_integrations unless creation_allowed?
integration = project.alert_management_http_integrations.create(permitted_params)
return error_in_create(integration) unless integration.valid?
::AlertManagement::HttpIntegration.transaction do
integration = project.alert_management_http_integrations.build(permitted_params)
success(integration)
if integration.save
@response = success(integration)
if too_many_integrations?(integration)
@response = error_multiple_integrations
raise ActiveRecord::Rollback
end
else
@response = error_on_save(integration)
end
end
@response
end
private
attr_reader :project, :current_user, :params
def allowed?
current_user&.can?(:admin_operations, project)
end
def creation_allowed?
project.alert_management_http_integrations.empty?
end
def permitted_params
params.slice(*permitted_params_keys)
end
# overriden in EE
def permitted_params_keys
%i[name active]
end
def error(message)
ServiceResponse.error(message: message)
end
def success(integration)
ServiceResponse.success(payload: { integration: integration })
end
def error_no_permissions
error(_('You have insufficient permissions to create an HTTP integration for this project'))
end
def error_multiple_integrations
error(_('Multiple HTTP integrations are not supported for this project'))
end
def error_in_create(integration)
error(integration.errors.full_messages.to_sentence)
end
end
end
end
::AlertManagement::HttpIntegrations::CreateService.prepend_mod_with('AlertManagement::HttpIntegrations::CreateService')

View File

@ -12,6 +12,7 @@ module AlertManagement
def execute
return error_no_permissions unless allowed?
return error_legacy_prometheus unless destroy_allowed?
if integration.destroy
success
@ -28,6 +29,12 @@ module AlertManagement
current_user&.can?(:admin_operations, integration)
end
# Prevents downtime while migrating from Integrations::Prometheus.
# Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/409734
def destroy_allowed?
!(integration.legacy? && integration.prometheus?)
end
def error(message)
ServiceResponse.error(message: message)
end
@ -39,6 +46,10 @@ module AlertManagement
def error_no_permissions
error(_('You have insufficient permissions to remove this HTTP integration'))
end
def error_legacy_prometheus
error(_('Legacy Prometheus integrations cannot currently be removed'))
end
end
end
end

View File

@ -2,51 +2,48 @@
module AlertManagement
module HttpIntegrations
class UpdateService
class UpdateService < BaseService
# @param integration [AlertManagement::HttpIntegration]
# @param current_user [User]
# @param params [Hash]
def initialize(integration, current_user, params)
@integration = integration
@current_user = current_user
@params = params.with_indifferent_access
super(integration.project, current_user, params)
end
def execute
return error_no_permissions unless allowed?
params[:token] = nil if params.delete(:regenerate_token)
integration.transaction do
if integration.update(permitted_params.merge(token_params))
@response = success(integration)
if integration.update(permitted_params)
success
else
error(integration.errors.full_messages.to_sentence)
if type_update? && too_many_integrations?(integration)
@response = error_multiple_integrations
raise ActiveRecord::Rollback
end
else
@response = error_on_save(integration)
end
end
@response
end
private
attr_reader :integration, :current_user, :params
attr_reader :integration
def allowed?
current_user&.can?(:admin_operations, integration)
def token_params
return {} unless params[:regenerate_token]
{ token: nil }
end
def permitted_params
params.slice(*permitted_params_keys)
end
# overriden in EE
def permitted_params_keys
%i[name active token]
end
def error(message)
ServiceResponse.error(message: message)
end
def success
ServiceResponse.success(payload: { integration: integration.reset })
def type_update?
params[:type_identifier].present?
end
def error_no_permissions
@ -55,5 +52,3 @@ module AlertManagement
end
end
end
::AlertManagement::HttpIntegrations::UpdateService.prepend_mod_with('AlertManagement::HttpIntegrations::UpdateService')

View File

@ -79,12 +79,18 @@ module Projects
end
def valid_alert_manager_token?(token, integration)
valid_for_manual?(token) ||
valid_for_alerts_endpoint?(token, integration) ||
valid_for_alerts_endpoint?(token, integration) ||
valid_for_manual?(token) ||
valid_for_cluster?(token)
end
def valid_for_manual?(token)
# If migration from Integrations::Prometheus to
# AlertManagement::HttpIntegrations is complete,
# we should use use the HttpIntegration as SSOT.
# Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/409734
return false if project.alert_management_http_integrations.legacy.prometheus.any?
prometheus = project.find_or_initialize_integration('prometheus')
return false unless prometheus.manual_configuration?

View File

@ -0,0 +1,8 @@
---
name: npm_group_level_endpoints
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119073
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409476
milestone: '16.0'
type: development
group: group::package registry
default_enabled: false

View File

@ -3,7 +3,7 @@ key_path: redis_hll_counters.user_container_registry.i_container_registry_writes
description: A monthly count of unique users that have executed write operations to the registry
product_section: ops
product_stage: package
product_group: package
product_group: container_registry
value_type: number
status: active
milestone: "16.0"
@ -22,7 +22,8 @@ options:
- i_container_registry_delete_repository_user
- i_container_registry_create_repository_user
- i_container_registry_push_repository_user
performance_indicator_type: []
performance_indicator_type:
- gmau
distribution:
- ce
- ee

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddTypeToHttpIntegrations < Gitlab::Database::Migration[2.1]
def change
add_column :alert_management_http_integrations, :type_identifier, :integer, default: 0, null: false, limit: 2
end
end

View File

@ -0,0 +1 @@
ad16293967c9751d138690328308944dd0930cd88e1afa16d825fbaf2cc8299c

View File

@ -11042,6 +11042,7 @@ CREATE TABLE alert_management_http_integrations (
name text NOT NULL,
payload_example jsonb DEFAULT '{}'::jsonb NOT NULL,
payload_attribute_mapping jsonb DEFAULT '{}'::jsonb NOT NULL,
type_identifier smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_286943b636 CHECK ((char_length(encrypted_token_iv) <= 255)),
CONSTRAINT check_392143ccf4 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_e270820180 CHECK ((char_length(endpoint_identifier) <= 255)),

View File

@ -124,6 +124,7 @@ different scopes:
- Use the instance-level prefix to make requests in the scope of the entire instance.
- Use the project-level prefix to make requests in a single project's scope.
- Use the group-level prefix to make requests in a groups scope.
The examples in this document all use the project-level prefix.
@ -147,6 +148,22 @@ The examples in this document all use the project-level prefix.
| --------- | ------ | -------- | ----------- |
| `id` | string | yes | The project ID or full project path. |
### Group-level
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299834) in GitLab 16.0 [with a flag](../../administration/feature_flags.md) named `npm_group_level_endpoints`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `npm_group_level_endpoints`.
The feature is not ready for production use.
```plaintext
/groups/:id/-/packages/npm`
```
| Attribute | Type | Required | Description |
| --------- | ------ | -------- | ----------- |
| `id` | string | yes | The group ID or full group path. |
## Metadata
Returns the metadata for a given package.

View File

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Users API **(FREE)**
This documentation has information on API calls, parameters and responses for the Users API.
This documentation has information on API calls, parameters and responses for the Users API.
For information on user activities that update the user event timestamps, see [get user activities](#get-user-activities).
@ -875,7 +875,7 @@ Parameters:
| :------------------------------- | :------- | :--------------------------------------------------------------------------- |
| `view_diffs_file_by_file` | Yes | Flag indicating the user sees only one file diff per page. |
| `show_whitespace_in_diffs` | Yes | Flag indicating the user sees whitespace changes in diffs. |
| `pass_user_identities_to_ci_jwt` | Yes | Flag indicating the user passes their external identities as CI information. This attribute does not contain enough information to identify or authorize the user in an external system. The attribute is internal to GitLab, and must not be passed to third-party services. |
| `pass_user_identities_to_ci_jwt` | Yes | Flag indicating the user passes their external identities as CI information. This attribute does not contain enough information to identify or authorize the user in an external system. The attribute is internal to GitLab, and must not be passed to third-party services. For more information and examples, see [Token Payload](../ci/secrets/id_token_authentication.md#token-payload). |
## User follow

View File

@ -60,6 +60,7 @@ The token also includes custom claims provided by GitLab:
| `user_id` | Always | ID of the user executing the job. |
| `user_login` | Always | Username of the user executing the job. |
| `user_email` | Always | Email of the user executing the job. |
| `user_identities` | User Preference setting | List of the user's external identities ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/387537) in GitLab 16.0). |
| `pipeline_id` | Always | ID of the pipeline. |
| `pipeline_source` | Always | [Pipeline source](../jobs/job_control.md#common-if-clauses-for-rules). |
| `job_id` | Always | ID of the job. |
@ -83,6 +84,10 @@ The token also includes custom claims provided by GitLab:
"user_id": "1",
"user_login": "sample-user",
"user_email": "sample-user@example.com",
"user_identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john.smith"},
],
"pipeline_id": "574",
"pipeline_source": "push",
"job_id": "302",

View File

@ -35,6 +35,8 @@ When you select a row in the compliance report, a drawer appears that provides:
### View the compliance violations report for a group
> Target branch search [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/358414) in GitLab 16.0.
Prerequisites:
- You must be an administrator or have the Owner role for the group.
@ -50,6 +52,12 @@ You can sort the compliance report on:
- Type of violation.
- Merge request title.
You can filter the compliance violations report on:
- Project.
- Date range of merge.
- Target branch.
Select a row to see details of the compliance violation.
#### Severity levels

View File

@ -190,8 +190,7 @@ During provisioning:
- Both primary and secondary emails are considered when checking whether a GitLab user account exists.
- Duplicate usernames are handled by adding suffix `1` when creating the user. For example, if `test_user` already
exists, `test_user1` is used. If `test_user1` already exists, GitLab increments the suffix until an unused username
is found.
exists, `test_user1` is used. If `test_user1` already exists, GitLab increments the suffix to find an unused username. If no unused username is found after 4 tries, a random string is attached to the username.
On subsequent visits, new and existing users can access groups either:

View File

@ -119,15 +119,16 @@ Your package should now publish to the Package Registry when the pipeline runs.
If multiple packages have the same name and version, when you install a package, the most recently-published package is retrieved.
You can install a package from a GitLab project or instance:
You can install a package from a GitLab project, group, or instance:
- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
- **Group-level**: Use when you have many npm packages in different projects in the same GitLab group.
- **Project-level**: Use when you have few npm packages and they are not in the same GitLab group.
### Authenticate to the Package Registry
You must authenticate to the Package Registry to install a package from a private project.
No authentication is needed if the project is public.
You must authenticate to the Package Registry to install a package from a private project or a private group.
No authentication is needed if the project or the group is public.
To authenticate with `npm`:
@ -145,7 +146,13 @@ If you're installing:
npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token
```
From the project level:
- From the group level:
```shell
npm config set -- //your_domain_name/api/v4/groups/your_group_id/-/packages/npm/:_authToken=your_token
```
- From the project level:
```shell
npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token
@ -154,6 +161,7 @@ If you're installing:
In these examples:
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_group_id` with your group ID, found on the group's home page.
- Replace `your_project_id` is your project ID, found on the project's home page.
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
@ -185,6 +193,32 @@ To install a package from the instance level, the package must have been publish
npm install @scope/my-package
```
### Install from the group level
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299834) in GitLab 16.0 [with a flag](../../../administration/feature_flags.md) named `npm_group_level_endpoints`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `npm_group_level_endpoints`.
The feature is not ready for production use.
1. [Authenticate to the Package Registry](#authenticate-to-the-package-registry).
1. Set the registry
```shell
npm config set @scope:registry=https://your_domain_name/api/v4/groups/your_group_id/-/packages/npm/
```
- Replace `@scope` with the [root level group](#naming-convention) of the group you're installing to the package from.
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
- Replace `your_group_id` is your group ID, found on the group's home page.
1. Install the package
```shell
npm install @scope/my-package
```
### Install from the project level
1. [Authenticate to the Package Registry](#authenticate-to-the-package-registry).

View File

@ -182,6 +182,13 @@ NOTE:
This feature is experimental, and choosing absolute times might break certain layouts.
Open an issue if you notice that using absolute times breaks a layout.
## User identities in CI job JSON web tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/387537) in GitLab 16.0. False by default.
You can select to include the list of your external identities in the JSON Web Token information that is generated for a CI job.
For more information and examples, see [Token Payload](../../ci/secrets/id_token_authentication.md#token-payload).
## Integrations
Configure your preferences with third-party services which provide enhancements to your GitLab experience.

View File

@ -256,6 +256,7 @@ module API
mount ::API::Metrics::Dashboard::Annotations
mount ::API::Metrics::UserStarredDashboards
mount ::API::Namespaces
mount ::API::NpmGroupPackages
mount ::API::NpmInstancePackages
mount ::API::NpmProjectPackages
mount ::API::NugetGroupPackages

View File

@ -11,16 +11,12 @@ module API
package_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
def endpoint_scope
params[:id].present? ? :project : :instance
end
def project
strong_memoize(:project) do
case endpoint_scope
when :project
user_project(action: :read_package)
when :instance
when :instance, :group
# Simulate the same behavior as #user_project by re-using #find_project!
# but take care if the project_id is nil as #find_project! is not designed
# to handle it.
@ -39,6 +35,8 @@ module API
::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil)
when :instance
::Packages::Npm::PackageFinder.new(package_name, namespace: top_namespace_from(package_name))
when :group
::Packages::Npm::PackageFinder.new(package_name, namespace: group)
end
end
@ -57,6 +55,14 @@ module API
case endpoint_scope
when :project
params[:id]
when :group
finder = ::Packages::Npm::PackageFinder.new(
params[:package_name],
namespace: group,
last_of_each_version: false
)
finder.last&.project_id
when :instance
package_name = params[:package_name]
@ -91,6 +97,13 @@ module API
Namespace.top_most.by_path(namespace_path)
end
def group
group = find_group(params[:id])
not_found!('Group') unless can?(current_user, :read_group, group)
group
end
strong_memoize_attr :group
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module API
class NpmGroupPackages < ::API::Base
helpers ::API::Helpers::Packages::Npm
feature_category :package_registry
urgency :low
helpers do
def endpoint_scope
:group
end
end
after_validation do
not_found! unless Feature.enabled?(:npm_group_level_endpoints, group)
end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/-/packages/npm' do
params do
requires :package_name, type: String, desc: 'Package name'
end
namespace '-/package/*package_name' do
get 'dist-tags', format: false do
not_found!
end
namespace 'dist-tags/:tag' do
put format: false do
not_found!
end
delete format: false do
not_found!
end
end
end
post '-/npm/v1/security/audits/quick' do
not_found!
end
post '-/npm/v1/security/advisories/bulk' do
not_found!
end
include ::API::Concerns::Packages::NpmEndpoints
end
end
end
end

View File

@ -10,6 +10,12 @@ module API
render_api_error!(e.message, 400)
end
helpers do
def endpoint_scope
:instance
end
end
namespace 'packages/npm' do
include ::API::Concerns::Packages::NpmEndpoints
end

View File

@ -10,6 +10,12 @@ module API
render_api_error!(e.message, 400)
end
helpers do
def endpoint_scope
:project
end
end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
end

View File

@ -1239,7 +1239,7 @@ module API
params do
optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page'
optional :show_whitespace_in_diffs, type: Boolean, desc: 'Flag indicating the user sees whitespace changes in diffs'
optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities as CI information'
optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities to a CI job as part of a JSON web token.'
at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt
end
put "preferences", feature_category: :user_profile, urgency: :high do

View File

@ -26475,6 +26475,9 @@ msgstr ""
msgid "Leave zen mode"
msgstr ""
msgid "Legacy Prometheus integrations cannot currently be removed"
msgstr ""
msgid "Legacy Web IDE"
msgstr ""
@ -29161,15 +29164,15 @@ msgstr ""
msgid "Multi-project"
msgstr ""
msgid "Multiple HTTP integrations are not supported for this project"
msgstr ""
msgid "Multiple IP address ranges are supported. Does not affect access to the group's settings."
msgstr ""
msgid "Multiple Prometheus integrations are not supported"
msgstr ""
msgid "Multiple integrations of a single type are not supported for this project"
msgstr ""
msgid "Multiple signatures"
msgstr ""

View File

@ -39,19 +39,20 @@ module QA
end
def network
shell "docker network inspect #{@network}"
rescue CommandError
'bridge'
else
@network
network_exists?(@network) ? @network : 'bridge'
end
def runner_network
shell "docker network inspect #{@runner_network}"
rescue CommandError
network
else
@runner_network
network_exists?(@runner_network) ? @runner_network : network
end
def inspect_network(name)
shell("docker network inspect #{name}", fail_on_exception: false, return_exit_status: true)
end
def network_exists?(name)
_, status = inspect_network(name)
status == 0
end
def pull

View File

@ -11,9 +11,10 @@ module QA
module_function
def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true, mask_secrets: []) # rubocop:disable Metrics/CyclomaticComplexity
def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true, mask_secrets: [], return_exit_status: false) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
cmd_string = Array(command).join(' ')
cmd_output = ''
exit_status = 0
QA::Runtime::Logger.info("Executing: `#{mask_secrets_on_string(cmd_string, mask_secrets).cyan}`")
@ -36,7 +37,9 @@ module QA
# add newline after progress dots
puts if print_progress_dots && !cmd_output.empty?
if wait.value.exited? && wait.value.exitstatus.nonzero? && fail_on_exception
exit_status = wait.value.exitstatus if wait.value.exited?
if exit_status.nonzero? && fail_on_exception
Runtime::Logger.error("Command output:\n#{cmd_output.strip}") unless cmd_output.empty?
raise CommandError, "Command: `#{mask_secrets_on_string(cmd_string, mask_secrets)}` failed! ✘"
end
@ -44,7 +47,7 @@ module QA
Runtime::Logger.debug("Command output:\n#{cmd_output.strip}") unless cmd_output.empty?
end
cmd_output.strip
return_exit_status ? [cmd_output.strip, exit_status] : cmd_output.strip
end
def sql_to_docker_exec_cmd(sql, username, password, database, host, container)

View File

@ -19,6 +19,12 @@ FactoryBot.define do
endpoint_identifier { 'legacy' }
end
trait :prometheus do
type_identifier { :prometheus }
end
initialize_with { new(**attributes) }
factory :alert_management_prometheus_integration, traits: [:prometheus]
end
end

View File

@ -2,10 +2,12 @@
require 'spec_helper'
RSpec.describe AlertManagement::HttpIntegrationsFinder do
RSpec.describe AlertManagement::HttpIntegrationsFinder, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project ) }
let_it_be(:extra_integration) { create(:alert_management_http_integration, project: project ) }
let_it_be(:prometheus_integration) { create(:alert_management_prometheus_integration, :inactive, project: project ) }
let_it_be(:extra_prometheus_integration) { create(:alert_management_prometheus_integration, project: project ) }
let_it_be(:alt_project_integration) { create(:alert_management_http_integration) }
let(:params) { {} }
@ -14,7 +16,7 @@ RSpec.describe AlertManagement::HttpIntegrationsFinder do
subject(:execute) { described_class.new(project, params).execute }
context 'empty params' do
it { is_expected.to contain_exactly(integration) }
it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
context 'endpoint_identifier param given' do
@ -37,7 +39,7 @@ RSpec.describe AlertManagement::HttpIntegrationsFinder do
context 'but blank' do
let(:params) { { endpoint_identifier: nil } }
it { is_expected.to contain_exactly(integration) }
it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
end
@ -46,18 +48,34 @@ RSpec.describe AlertManagement::HttpIntegrationsFinder do
it { is_expected.to contain_exactly(integration) }
context 'when integration is disabled' do
before do
integration.update!(active: false)
end
it { is_expected.to be_empty }
end
context 'but blank' do
let(:params) { { active: nil } }
it { is_expected.to contain_exactly(integration) }
it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
end
context 'type_identifier param given' do
let(:params) { { type_identifier: extra_integration.type_identifier } }
it { is_expected.to contain_exactly(integration) }
context 'matches an unavailable integration' do
let(:params) { { type_identifier: extra_prometheus_integration.type_identifier } }
it { is_expected.to contain_exactly(prometheus_integration) }
end
context 'but unknown' do
let(:params) { { type_identifier: :unknown } }
it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
context 'but blank' do
let(:params) { { type_identifier: nil } }
it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "GraphQL Pipeline details", '(JavaScript fixtures)', type: :request, feature_category: :pipeline_composition do
include ApiHelpers
include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:admin) { project.first_owner }
let_it_be(:commit) { create(:commit, project: project) }
let_it_be(:pipeline) do
create(:ci_pipeline, project: project, sha: commit.id, ref: 'master', user: admin, status: :success)
end
let_it_be(:build_success) do
create(:ci_build, :dependent, name: 'build_my_app', pipeline: pipeline, stage: 'build', status: :success)
end
let_it_be(:build_test) { create(:ci_build, :dependent, name: 'test_my_app', pipeline: pipeline, stage: 'test') }
let_it_be(:build_deploy_failed) do
create(:ci_build, :dependent, name: 'deploy_my_app', status: :failed, pipeline: pipeline, stage: 'deploy')
end
let_it_be(:bridge) { create(:ci_bridge, pipeline: pipeline) }
let(:pipeline_details_query_path) { 'app/graphql/queries/pipelines/get_pipeline_details.query.graphql' }
it "pipelines/pipeline_details.json" do
query = get_graphql_query_as_string(pipeline_details_query_path, with_base_path: false)
post_graphql(query, current_user: admin, variables: { projectPath: project.full_path, iid: pipeline.iid })
expect_graphql_errors_to_be_empty
end
end

View File

@ -397,6 +397,44 @@ describe('URL utility', () => {
});
});
describe('visitUrl', () => {
let originalLocation;
const mockUrl = 'http://example.com/page';
beforeAll(() => {
originalLocation = window.location;
Object.defineProperty(window, 'location', {
writable: true,
value: new URL(TEST_HOST),
});
});
afterAll(() => {
window.location = originalLocation;
});
it('navigates to a page', () => {
urlUtils.visitUrl(mockUrl);
expect(window.location.href).toBe(mockUrl);
});
it('navigates to a new page', () => {
const otherWindow = {};
Object.defineProperty(window, 'open', {
writable: true,
value: jest.fn().mockReturnValue(otherWindow),
});
urlUtils.visitUrl(mockUrl, true);
expect(otherWindow.opener).toBe(null);
expect(otherWindow.location).toBe(mockUrl);
});
});
describe('updateHistory', () => {
const state = { key: 'prop' };
const title = 'TITLE';

View File

@ -1,471 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DAG visualization parsing utilities generateColumnsFromLayersList matches the snapshot 1`] = `
Array [
Object {
"groups": Array [
Object {
"__typename": "CiGroup",
"id": "4",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "6",
"kind": "BUILD",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "8",
"path": "/root/abcd-dag/-/jobs/1482/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1482",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "7",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"size": 1,
"stageName": "build",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "5",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "9",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "11",
"kind": "BUILD",
"name": "build_b",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "13",
"path": "/root/abcd-dag/-/jobs/1515/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1515",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "12",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "build_b",
"size": 1,
"stageName": "build",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "10",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "14",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "16",
"kind": "BUILD",
"name": "build_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "18",
"path": "/root/abcd-dag/-/jobs/1484/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1484",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "17",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "build_c",
"size": 1,
"stageName": "build",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "15",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "19",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "21",
"kind": "BUILD",
"name": "build_d 1/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "23",
"path": "/root/abcd-dag/-/jobs/1485/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1485",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "22",
"label": "passed",
"tooltip": "passed",
},
},
Object {
"__typename": "CiJob",
"id": "24",
"kind": "BUILD",
"name": "build_d 2/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "26",
"path": "/root/abcd-dag/-/jobs/1486/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1486",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "25",
"label": "passed",
"tooltip": "passed",
},
},
Object {
"__typename": "CiJob",
"id": "27",
"kind": "BUILD",
"name": "build_d 3/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "29",
"path": "/root/abcd-dag/-/jobs/1487/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1487",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "28",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "build_d",
"size": 3,
"stageName": "build",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "20",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "57",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "59",
"kind": "BUILD",
"name": "test_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": null,
"detailsPath": "/root/kinder-pipe/-/pipelines/154",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "60",
"label": null,
"tooltip": null,
},
},
],
"name": "test_c",
"size": 1,
"stageName": "test",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "58",
"label": null,
},
},
],
"id": "layer-0",
"name": "",
"status": Object {
"action": null,
},
},
Object {
"groups": Array [
Object {
"__typename": "CiGroup",
"id": "32",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "34",
"kind": "BUILD",
"name": "test_a",
"needs": Array [
"build_c",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"previousStageJobsOrNeeds": Array [
"build_c",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "36",
"path": "/root/abcd-dag/-/jobs/1514/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1514",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "35",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "test_a",
"size": 1,
"stageName": "test",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "33",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "40",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "42",
"kind": "BUILD",
"name": "test_b 1/2",
"needs": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"previousStageJobsOrNeeds": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "44",
"path": "/root/abcd-dag/-/jobs/1489/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1489",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "43",
"label": "passed",
"tooltip": "passed",
},
},
Object {
"__typename": "CiJob",
"id": "67",
"kind": "BUILD",
"name": "test_b 2/2",
"needs": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"previousStageJobsOrNeeds": Array [
"build_d 3/3",
"build_d 2/3",
"build_d 1/3",
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": Object {
"__typename": "StatusAction",
"buttonTitle": "Retry this job",
"icon": "retry",
"id": "51",
"path": "/root/abcd-dag/-/jobs/1490/retry",
"title": "Retry",
},
"detailsPath": "/root/abcd-dag/-/jobs/1490",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "50",
"label": "passed",
"tooltip": "passed",
},
},
],
"name": "test_b",
"size": 2,
"stageName": "test",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "41",
"label": "passed",
},
},
Object {
"__typename": "CiGroup",
"id": "61",
"jobs": Array [
Object {
"__typename": "CiJob",
"id": "53",
"kind": "BUILD",
"name": "test_d",
"needs": Array [
"build_b",
],
"previousStageJobsOrNeeds": Array [
"build_b",
],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
"action": null,
"detailsPath": "/root/abcd-dag/-/pipelines/153",
"group": "success",
"hasDetails": true,
"icon": "status_success",
"id": "64",
"label": null,
"tooltip": null,
},
},
],
"name": "test_d",
"size": 1,
"stageName": "test",
"status": Object {
"__typename": "DetailedStatus",
"group": "success",
"icon": "status_success",
"id": "62",
"label": null,
},
},
],
"id": "layer-1",
"name": "",
"status": Object {
"action": null,
},
},
]
`;

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
@ -7,11 +8,8 @@ import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import {
generateResponse,
mockPipelineResponse,
pipelineWithUpstreamDownstream,
} from './mock_data';
import { generateResponse, pipelineWithUpstreamDownstream } from './mock_data';
describe('graph component', () => {
let wrapper;

View File

@ -2,6 +2,7 @@ import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitl
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -26,7 +27,6 @@ import {
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
@ -34,7 +34,7 @@ import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_head
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
import { mapCallouts, mockCalloutsResponse } from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@ -55,8 +55,6 @@ describe('Pipeline graph wrapper', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findGraph = () => wrapper.findComponent(PipelineGraph);
const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
const findAllStageColumnGroupsInColumn = () =>
wrapper.findComponent(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const findViewSelector = () => wrapper.findComponent(GraphViewSelector);
const findViewSelectorToggle = () => findViewSelector().findComponent(GlToggle);
const findViewSelectorTrip = () => findViewSelector().findComponent(GlAlert);
@ -316,12 +314,10 @@ describe('Pipeline graph wrapper', () => {
});
it('switches between views', async () => {
const groupsInFirstColumn =
mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length;
expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
expect(findStageColumnTitle().text()).toBe('build');
expect(findStageColumnTitle().text()).toBe('deploy');
await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
expect(findStageColumnTitle().text()).toBe('');
});
@ -507,9 +503,9 @@ describe('Pipeline graph wrapper', () => {
});
describe('with metrics path', () => {
const duration = 875;
const numLinks = 7;
const totalGroups = 8;
const duration = 500;
const numLinks = 3;
const totalGroups = 7;
const metricsData = {
histograms: [
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
@ -559,9 +555,6 @@ describe('Pipeline graph wrapper', () => {
createComponentWithApollo({
provide: {
metricsPath,
glFeatures: {
pipelineGraphLayersView: true,
},
},
data: {
currentViewType: LAYER_VIEW,

View File

@ -1,6 +1,7 @@
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
@ -15,11 +16,8 @@ import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import { LOAD_FAILURE } from '~/pipelines/constants';
import {
mockPipelineResponse,
pipelineWithUpstreamDownstream,
wrappedPipelineReturn,
} from './mock_data';
import { pipelineWithUpstreamDownstream, wrappedPipelineReturn } from './mock_data';
const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);

View File

@ -5,710 +5,6 @@ import {
RETRY_ACTION_TITLE,
} from '~/pipelines/components/graph/constants';
export const mockPipelineResponse = {
data: {
project: {
__typename: 'Project',
id: '1',
pipeline: {
__typename: 'Pipeline',
id: 163,
iid: '22',
complete: true,
usesNeeds: true,
downstream: null,
upstream: null,
userPermissions: {
__typename: 'PipelinePermissions',
updatePipeline: true,
},
stages: {
__typename: 'CiStageConnection',
nodes: [
{
__typename: 'CiStage',
id: '2',
name: 'build',
status: {
__typename: 'DetailedStatus',
id: '3',
action: null,
},
groups: {
__typename: 'CiGroupConnection',
nodes: [
{
__typename: 'CiGroup',
id: '4',
name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
size: 1,
status: {
__typename: 'DetailedStatus',
id: '5',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '6',
kind: BUILD_KIND,
name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '7',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1482',
group: 'success',
action: {
__typename: 'StatusAction',
id: '8',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1482/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
],
},
},
{
__typename: 'CiGroup',
name: 'build_b',
id: '9',
size: 1,
status: {
__typename: 'DetailedStatus',
id: '10',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '11',
name: 'build_b',
kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '12',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1515',
group: 'success',
action: {
__typename: 'StatusAction',
id: '13',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1515/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
],
},
},
{
__typename: 'CiGroup',
id: '14',
name: 'build_c',
size: 1,
status: {
__typename: 'DetailedStatus',
id: '15',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '16',
name: 'build_c',
kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '17',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1484',
group: 'success',
action: {
__typename: 'StatusAction',
id: '18',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1484/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
],
},
},
{
__typename: 'CiGroup',
id: '19',
name: 'build_d',
size: 3,
status: {
__typename: 'DetailedStatus',
id: '20',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '21',
kind: BUILD_KIND,
name: 'build_d 1/3',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '22',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1485',
group: 'success',
action: {
__typename: 'StatusAction',
id: '23',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1485/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
{
__typename: 'CiJob',
id: '24',
kind: BUILD_KIND,
name: 'build_d 2/3',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '25',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1486',
group: 'success',
action: {
__typename: 'StatusAction',
id: '26',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1486/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
{
__typename: 'CiJob',
id: '27',
kind: BUILD_KIND,
name: 'build_d 3/3',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '28',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1487',
group: 'success',
action: {
__typename: 'StatusAction',
id: '29',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1487/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
],
},
},
],
},
},
{
__typename: 'CiStage',
id: '30',
name: 'test',
status: {
__typename: 'DetailedStatus',
id: '31',
action: null,
},
groups: {
__typename: 'CiGroupConnection',
nodes: [
{
__typename: 'CiGroup',
id: '32',
name: 'test_a',
size: 1,
status: {
__typename: 'DetailedStatus',
id: '33',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '34',
kind: BUILD_KIND,
name: 'test_a',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '35',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1514',
group: 'success',
action: {
__typename: 'StatusAction',
id: '36',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1514/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '37',
name: 'build_c',
},
{
__typename: 'CiBuildNeed',
id: '38',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '39',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '37',
name: 'build_c',
},
{
__typename: 'CiBuildNeed',
id: '38',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '39',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
},
],
},
},
{
__typename: 'CiGroup',
id: '40',
name: 'test_b',
size: 2,
status: {
__typename: 'DetailedStatus',
id: '41',
label: 'passed',
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '42',
kind: BUILD_KIND,
name: 'test_b 1/2',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '43',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1489',
group: 'success',
action: {
__typename: 'StatusAction',
id: '44',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1489/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '45',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '46',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '47',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '48',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '49',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '45',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '46',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '47',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '48',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '49',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
},
{
__typename: 'CiJob',
id: '67',
kind: BUILD_KIND,
name: 'test_b 2/2',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '50',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1490',
group: 'success',
action: {
__typename: 'StatusAction',
id: '51',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/abcd-dag/-/jobs/1490/retry',
title: 'Retry',
},
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '52',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '53',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '54',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '55',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '56',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '52',
name: 'build_d 3/3',
},
{
__typename: 'CiBuildNeed',
id: '53',
name: 'build_d 2/3',
},
{
__typename: 'CiBuildNeed',
id: '54',
name: 'build_d 1/3',
},
{
__typename: 'CiBuildNeed',
id: '55',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
id: '56',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
},
},
],
},
},
{
__typename: 'CiGroup',
name: 'test_c',
id: '57',
size: 1,
status: {
__typename: 'DetailedStatus',
id: '58',
label: null,
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '59',
kind: BUILD_KIND,
name: 'test_c',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '60',
icon: 'status_success',
tooltip: null,
label: null,
hasDetails: true,
detailsPath: '/root/kinder-pipe/-/pipelines/154',
group: 'success',
action: null,
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [],
},
},
],
},
},
{
__typename: 'CiGroup',
id: '61',
name: 'test_d',
size: 1,
status: {
id: '62',
__typename: 'DetailedStatus',
label: null,
group: 'success',
icon: 'status_success',
},
jobs: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiJob',
id: '53',
kind: BUILD_KIND,
name: 'test_d',
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
id: '64',
icon: 'status_success',
tooltip: null,
label: null,
hasDetails: true,
detailsPath: '/root/abcd-dag/-/pipelines/153',
group: 'success',
action: null,
},
needs: {
__typename: 'CiBuildNeedConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '65',
name: 'build_b',
},
],
},
previousStageJobsOrNeeds: {
__typename: 'CiJobConnection',
nodes: [
{
__typename: 'CiBuildNeed',
id: '65',
name: 'build_b',
},
],
},
},
],
},
},
],
},
},
],
},
},
},
},
};
export const downstream = {
nodes: [
{

View File

@ -1,7 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
import { generateResponse } from '../graph/mock_data';
describe('links layer component', () => {
let wrapper;

View File

@ -1,3 +1,4 @@
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import {
makeLinksFromNodes,
@ -14,7 +15,7 @@ import { createNodeDict } from '~/pipelines/utils';
import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
import { generateResponse } from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@ -152,14 +153,6 @@ describe('DAG visualization parsing utilities', () => {
});
});
});
/*
Just as a fallback in case multiple functions change, so tests pass
but the implementation moves away from case.
*/
it('matches the snapshot', () => {
expect(columns).toMatchSnapshot();
});
});
});

View File

@ -17,20 +17,9 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
let_it_be(:project) { create(:project, :public, namespace: namespace) }
let_it_be(:package) { create(:npm_package, project: project) }
describe '#endpoint_scope' do
subject { object.endpoint_scope }
context 'when params includes an id' do
let(:params) { { id: 42, package_name: 'foo' } }
it { is_expected.to eq(:project) }
end
context 'when params does not include an id' do
let(:params) { { package_name: 'foo' } }
it { is_expected.to eq(:instance) }
end
before do
allow(object).to receive(:endpoint_scope).and_return(endpoint_scope)
allow(object).to receive(:current_user).and_return(user)
end
describe '#finder_for_endpoint_scope' do
@ -40,6 +29,7 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
context 'when called with project scope' do
let(:params) { { id: project.id } }
let(:endpoint_scope) { :project }
it 'returns a PackageFinder for project scope' do
expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, project: project)
@ -50,6 +40,7 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
context 'when called with instance scope' do
let(:params) { { package_name: package_name } }
let(:endpoint_scope) { :instance }
it 'returns a PackageFinder for namespace scope' do
expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, namespace: group)
@ -57,6 +48,17 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
subject
end
end
context 'when called with group scope' do
let(:params) { { id: group.id } }
let(:endpoint_scope) { :group }
it 'returns a PackageFinder for group scope' do
expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, namespace: group)
subject
end
end
end
describe '#project_id_or_nil' do
@ -64,11 +66,21 @@ RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registr
context 'when called with project scope' do
let(:params) { { id: project.id } }
let(:endpoint_scope) { :project }
it { is_expected.to eq(project.id) }
end
context 'when called with namespace scope' do
context 'when called with group scope' do
let(:params) { { id: group.id, package_name: package.name } }
let(:endpoint_scope) { :group }
it { is_expected.to eq(project.id) }
end
context 'when called with instance scope' do
let(:endpoint_scope) { :instance }
context 'when given an unscoped name' do
let(:params) { { package_name: 'foo' } }

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddTypeToHttpIntegrations, feature_category: :incident_management do
let(:integrations) { table(:alert_management_http_integrations) }
it 'correctly migrates up and down' do
reversible_migration do |migration|
migration.before -> {
expect(integrations.column_names).not_to include('type_identifier')
}
migration.after -> {
integrations.reset_column_information
expect(integrations.column_names).to include('type_identifier')
}
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe AlertManagement::HttpIntegration do
RSpec.describe AlertManagement::HttpIntegration, feature_category: :incident_management do
include ::Gitlab::Routing.url_helpers
let_it_be(:project) { create(:project) }
@ -21,6 +21,7 @@ RSpec.describe AlertManagement::HttpIntegration do
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:type_identifier) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
context 'when active' do
@ -86,6 +87,66 @@ RSpec.describe AlertManagement::HttpIntegration do
end
end
describe 'scopes' do
let_it_be(:integration_1) { create(:alert_management_http_integration) }
let_it_be(:integration_2) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:integration_3) { create(:alert_management_http_integration, :prometheus, project: project) }
let_it_be(:integration_4) { create(:alert_management_http_integration, :legacy, :inactive) }
describe '.for_endpoint_identifier' do
let(:identifier) { integration_1.endpoint_identifier }
subject { described_class.for_endpoint_identifier(identifier) }
it { is_expected.to contain_exactly(integration_1) }
end
describe '.for_type' do
let(:type) { :prometheus }
subject { described_class.for_type(type) }
it { is_expected.to contain_exactly(integration_3) }
end
describe '.for_project' do
let(:project) { integration_2.project }
subject { described_class.for_project(project) }
it { is_expected.to contain_exactly(integration_2, integration_3) }
context 'with project_ids array' do
let(:project) { [integration_1.project_id] }
it { is_expected.to contain_exactly(integration_1) }
end
end
describe '.active' do
subject { described_class.active }
it { is_expected.to contain_exactly(integration_1, integration_3) }
end
describe '.legacy' do
subject { described_class.legacy }
it { is_expected.to contain_exactly(integration_4) }
end
describe '.ordered_by_type_and_id' do
before do
# Rearrange cache by saving to avoid false-positives
integration_2.touch
end
subject { described_class.ordered_by_type_and_id }
it { is_expected.to eq([integration_1, integration_2, integration_4, integration_3]) }
end
end
describe 'before validation' do
describe '#ensure_payload_example_not_nil' do
subject(:integration) { build(:alert_management_http_integration, payload_example: payload_example) }
@ -230,5 +291,33 @@ RSpec.describe AlertManagement::HttpIntegration do
)
end
end
context 'for a prometheus integration' do
let(:integration) { build(:alert_management_http_integration, :prometheus) }
it do
is_expected.to eq(
project_alert_http_integration_url(
integration.project,
'datadog',
integration.endpoint_identifier,
format: :json
)
)
end
context 'for a legacy integration' do
let(:integration) { build(:alert_management_http_integration, :prometheus, :legacy) }
it do
is_expected.to eq(
notify_project_prometheus_alerts_url(
integration.project,
format: :json
)
)
end
end
end
end
end

View File

@ -0,0 +1,198 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::NpmGroupPackages, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include_context 'npm api setup'
describe 'GET /api/v4/groups/:id/-/packages/npm/*package_name' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/#{package_name}") }
it_behaves_like 'handling get metadata requests', scope: :group
context 'with a duplicate package name in another project' do
subject { get(url) }
before do
group.add_developer(user)
end
let_it_be(:project2) { create(:project, :public, namespace: namespace) }
let_it_be(:package2) do
create(:npm_package,
project: project2,
name: "@#{group.path}/scoped_package",
version: '1.2.0')
end
it 'includes all matching package versions in the response' do
subject
expect(json_response['versions'].keys).to match_array([package.version, package2.version])
end
context 'with the feature flag disabled' do
before do
stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
end
it 'returns matching package versions from only one project' do
subject
expect(json_response['versions'].keys).to match_array([package2.version])
end
end
end
context 'with mixed group and project visibilities' do
subject { get(url, headers: headers) }
where(:auth, :group_visibility, :project_visibility, :user_role, :expected_status) do
nil | :public | :public | nil | :ok
nil | :public | :internal | nil | :not_found
nil | :public | :private | nil | :not_found
nil | :internal | :internal | nil | :not_found
nil | :internal | :private | nil | :not_found
nil | :private | :private | nil | :not_found
:oauth | :public | :public | :guest | :ok
:oauth | :public | :internal | :guest | :ok
:oauth | :public | :private | :guest | :forbidden
:oauth | :internal | :internal | :guest | :ok
:oauth | :internal | :private | :guest | :forbidden
:oauth | :private | :private | :guest | :forbidden
:oauth | :public | :public | :reporter | :ok
:oauth | :public | :internal | :reporter | :ok
:oauth | :public | :private | :reporter | :ok
:oauth | :internal | :internal | :reporter | :ok
:oauth | :internal | :private | :reporter | :ok
:oauth | :private | :private | :reporter | :ok
:personal_access_token | :public | :public | :guest | :ok
:personal_access_token | :public | :internal | :guest | :ok
:personal_access_token | :public | :private | :guest | :forbidden
:personal_access_token | :internal | :internal | :guest | :ok
:personal_access_token | :internal | :private | :guest | :forbidden
:personal_access_token | :private | :private | :guest | :forbidden
:personal_access_token | :public | :public | :reporter | :ok
:personal_access_token | :public | :internal | :reporter | :ok
:personal_access_token | :public | :private | :reporter | :ok
:personal_access_token | :internal | :internal | :reporter | :ok
:personal_access_token | :internal | :private | :reporter | :ok
:personal_access_token | :private | :private | :reporter | :ok
:job_token | :public | :public | :developer | :ok
:job_token | :public | :internal | :developer | :ok
:job_token | :public | :private | :developer | :ok
:job_token | :internal | :internal | :developer | :ok
:job_token | :internal | :private | :developer | :ok
:job_token | :private | :private | :developer | :ok
:deploy_token | :public | :public | nil | :ok
:deploy_token | :public | :internal | nil | :ok
:deploy_token | :public | :private | nil | :ok
:deploy_token | :internal | :internal | nil | :ok
:deploy_token | :internal | :private | nil | :ok
:deploy_token | :private | :private | nil | :ok
end
with_them do
let(:headers) do
case auth
when :oauth
build_token_auth_header(token.plaintext_token)
when :personal_access_token
build_token_auth_header(personal_access_token.token)
when :job_token
build_token_auth_header(job.token)
when :deploy_token
build_token_auth_header(deploy_token.token)
else
{}
end
end
before do
project.update!(visibility: project_visibility.to_s)
project.send("add_#{user_role}", user) if user_role
group.update!(visibility: group_visibility.to_s)
group.send("add_#{user_role}", user) if user_role
end
it_behaves_like 'returning response status', params[:expected_status]
end
end
context 'when user is a reporter of project but is not a direct member of group' do
subject { get(url, headers: headers) }
where(:group_visibility, :project_visibility, :expected_status) do
:public | :public | :ok
:public | :internal | :ok
:public | :private | :ok
:internal | :internal | :ok
:internal | :private | :ok
:private | :private | :ok
end
with_them do
let(:headers) { build_token_auth_header(personal_access_token.token) }
before do
project.update!(visibility: project_visibility.to_s)
project.add_reporter(user)
group.update!(visibility: group_visibility.to_s)
end
it_behaves_like 'returning response status', params[:expected_status]
end
end
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags") }
subject { get(url) }
it_behaves_like 'returning response status', :not_found
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let(:tag_name) { 'test' }
let(:headers) { build_token_auth_header(personal_access_token.token) }
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
subject { put(url, headers: headers) }
it_behaves_like 'returning response status', :not_found
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let(:tag_name) { 'test' }
let(:headers) { build_token_auth_header(personal_access_token.token) }
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
subject { delete(url, headers: headers) }
it_behaves_like 'returning response status', :not_found
end
describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/advisories/bulk' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/advisories/bulk") }
subject { post(url) }
it_behaves_like 'returning response status', :not_found
end
describe 'POST /api/v4/groups/:id/-/packages/npm/-/npm/v1/security/audits/quick' do
let(:url) { api("/groups/#{group.id}/-/packages/npm/-/npm/v1/security/audits/quick") }
subject { post(url) }
it_behaves_like 'returning response status', :not_found
end
end

View File

@ -38,12 +38,6 @@ RSpec.describe AlertManagement::HttpIntegrations::CreateService, feature_categor
it_behaves_like 'error response', 'You have insufficient permissions to create an HTTP integration for this project'
end
context 'when an integration already exists' do
let_it_be(:existing_integration) { create(:alert_management_http_integration, project: project) }
it_behaves_like 'error response', 'Multiple HTTP integrations are not supported for this project'
end
context 'when an error occurs during update' do
it_behaves_like 'error response', "Name can't be blank"
end
@ -61,6 +55,38 @@ RSpec.describe AlertManagement::HttpIntegrations::CreateService, feature_categor
expect(integration.token).to be_present
expect(integration.endpoint_identifier).to be_present
end
context 'with an existing HTTP integration' do
let_it_be(:http_integration) { create(:alert_management_http_integration, project: project) }
it_behaves_like 'error response', 'Multiple integrations of a single type are not supported for this project'
context 'when creating a different type of integration' do
let(:params) { { type_identifier: :prometheus, name: 'Prometheus' } }
it 'is successful' do
expect(response).to be_success
expect(response.payload[:integration]).to be_a(::AlertManagement::HttpIntegration)
end
end
end
context 'with an existing Prometheus integration' do
let_it_be(:http_integration) { create(:alert_management_prometheus_integration, project: project) }
context 'when creating a different type of integration' do
it 'is successful' do
expect(response).to be_success
expect(response.payload[:integration]).to be_a(::AlertManagement::HttpIntegration)
end
end
context 'when creating the same time of integration' do
let(:params) { { type_identifier: :prometheus, name: 'Prometheus' } }
it_behaves_like 'error response', 'Multiple integrations of a single type are not supported for this project'
end
end
end
end
end

View File

@ -47,6 +47,13 @@ RSpec.describe AlertManagement::HttpIntegrations::DestroyService, feature_catego
it_behaves_like 'error response', 'Name cannot be removed'
end
context 'when destroying a legacy Prometheus integration' do
let_it_be(:existing_integration) { create(:alert_management_prometheus_integration, :legacy, project: project) }
let!(:integration) { existing_integration }
it_behaves_like 'error response', 'Legacy Prometheus integrations cannot currently be removed'
end
it 'successfully returns the integration' do
expect(response).to be_success

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :metrics do
RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :incident_management do
include PrometheusHelpers
using RSpec::Parameterized::TableSyntax
@ -163,6 +163,24 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :m
raise "invalid result: #{result.inspect}"
end
end
context 'with simultaneous manual configuration' do
let_it_be(:integration) { create(:alert_management_prometheus_integration, :legacy, project: project) }
let_it_be(:old_prometheus_integration) { create(:prometheus_integration, project: project) }
let_it_be(:alerting_setting) { create(:project_alerting_setting, project: project, token: integration.token) }
subject { service.execute(integration.token, integration) }
it_behaves_like 'processes one firing and one resolved prometheus alerts'
context 'when HTTP integration is inactive' do
before do
integration.update!(active: false)
end
it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
end
end
end
context 'incident settings' do

View File

@ -46,9 +46,8 @@ module JavaScriptFixturesHelpers
#
# query_path - file path to the GraphQL query, relative to `app/assets/javascripts`.
# ee - boolean, when true `query_path` will be looked up in `/ee`.
def get_graphql_query_as_string(query_path, ee: false)
base = (ee ? 'ee/' : '') + 'app/assets/javascripts'
def get_graphql_query_as_string(query_path, ee: false, with_base_path: true)
base = (ee ? 'ee/' : '') + (with_base_path ? 'app/assets/javascripts' : '')
path = Rails.root / base / query_path
queries = Gitlab::Graphql::Queries.find(path)
if queries.length == 1

View File

@ -259,8 +259,13 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
before do
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
group.send("add_#{user_role}", user) if user_role && scope == :group
group.update!(visibility: visibility.to_s) if scope == :group
package.update!(name: package_name) unless package_name == 'non-existing-package'
if scope == :instance
if %i[instance group].include?(scope)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
else
allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
@ -280,6 +285,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
end
status = :not_found if scope == :group && params[:package_name_type] == :non_existing && !params[:request_forward]
it_behaves_like example_name, status: status
end
end
@ -300,6 +307,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
let(:headers) { build_token_auth_header(personal_access_token.token) }
before do
group.add_developer(user) if scope == :group
project.add_developer(user)
end