summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-16 12:09:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-16 12:09:06 +0000
commit0045970352e8729b2797591beb88a7df884d84f4 (patch)
treeb9cd4c5aaaa26ce4a3c944ec5cfdbd7ad44b796d
parent613868af23d7c0e09210857518895edd6678f5e9 (diff)
downloadgitlab-ce-0045970352e8729b2797591beb88a7df884d84f4.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rails/inverse_of.yml3
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss75
-rw-r--r--app/finders/alert_management/http_integrations_finder.rb27
-rw-r--r--app/models/alert_management/http_integration.rb28
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/services/alert_management/http_integrations/base_service.rb57
-rw-r--r--app/services/alert_management/http_integrations/create_service.rb64
-rw-r--r--app/services/alert_management/http_integrations/destroy_service.rb11
-rw-r--r--app/services/alert_management/http_integrations/update_service.rb49
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb10
-rw-r--r--config/feature_flags/development/npm_group_level_endpoints.yml8
-rw-r--r--config/metrics/counts_28d/20230417064258_i_container_registry_writes_user_monthly.yml5
-rw-r--r--db/migrate/20230428165514_add_type_to_http_integrations.rb7
-rw-r--r--db/schema_migrations/202304281655141
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/packages/npm.md17
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/ci/secrets/id_token_authentication.md5
-rw-r--r--doc/user/compliance/compliance_report/index.md8
-rw-r--r--doc/user/group/saml_sso/scim_setup.md3
-rw-r--r--doc/user/packages/npm_registry/index.md42
-rw-r--r--doc/user/profile/preferences.md7
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/helpers/packages/npm.rb23
-rw-r--r--lib/api/npm_group_packages.rb56
-rw-r--r--lib/api/npm_instance_packages.rb6
-rw-r--r--lib/api/npm_project_packages.rb6
-rw-r--r--lib/api/users.rb2
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/service/docker_run/base.rb21
-rw-r--r--qa/qa/service/shellout.rb9
-rw-r--r--spec/factories/alert_management/http_integrations.rb6
-rw-r--r--spec/finders/alert_management/http_integrations_finder_spec.rb38
-rw-r--r--spec/frontend/fixtures/pipeline_details.rb38
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js38
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap471
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js8
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js23
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js8
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js704
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js4
-rw-r--r--spec/frontend/pipelines/utils_spec.js11
-rw-r--r--spec/lib/api/helpers/packages/npm_spec.rb42
-rw-r--r--spec/migrations/add_type_to_http_integrations_spec.rb21
-rw-r--r--spec/models/alert_management/http_integration_spec.rb91
-rw-r--r--spec/requests/api/npm_group_packages_spec.rb198
-rw-r--r--spec/services/alert_management/http_integrations/create_service_spec.rb38
-rw-r--r--spec/services/alert_management/http_integrations/destroy_service_spec.rb7
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb20
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb5
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb10
53 files changed, 895 insertions, 1459 deletions
diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml
index 59c0e14e180..30f5aaeff61 100644
--- a/.rubocop_todo/rails/inverse_of.yml
+++ b/.rubocop_todo/rails/inverse_of.yml
@@ -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'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a32468aaacf..61d75e125be 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-11825b3ad89525b194cc4095e581eef843377cdb
+0b0e46fe69e9b94e3def2a9318188fcc0f3f00c6
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 51bffd99dd0..10cc6cbd78e 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -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%);
}
}
diff --git a/app/finders/alert_management/http_integrations_finder.rb b/app/finders/alert_management/http_integrations_finder.rb
index e8e85da11b7..77a3824576f 100644
--- a/app/finders/alert_management/http_integrations_finder.rb
+++ b/app/finders/alert_management/http_integrations_finder.rb
@@ -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
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index 906855d6dfc..d5162865a79 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -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
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7b1d4b97d3b..b7e39423e85 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -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
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 0e699d7a81d..d36857fc94a 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -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?
diff --git a/app/services/alert_management/http_integrations/base_service.rb b/app/services/alert_management/http_integrations/base_service.rb
new file mode 100644
index 00000000000..980f18631c0
--- /dev/null
+++ b/app/services/alert_management/http_integrations/base_service.rb
@@ -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
diff --git a/app/services/alert_management/http_integrations/create_service.rb b/app/services/alert_management/http_integrations/create_service.rb
index 1abe0548c45..17e39577c29 100644
--- a/app/services/alert_management/http_integrations/create_service.rb
+++ b/app/services/alert_management/http_integrations/create_service.rb
@@ -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?
-
- success(integration)
- end
- private
+ ::AlertManagement::HttpIntegration.transaction do
+ integration = project.alert_management_http_integrations.build(permitted_params)
- attr_reader :project, :current_user, :params
+ if integration.save
+ @response = success(integration)
- def allowed?
- current_user&.can?(:admin_operations, project)
- end
+ if too_many_integrations?(integration)
+ @response = error_multiple_integrations
- def creation_allowed?
- project.alert_management_http_integrations.empty?
- end
-
- def permitted_params
- params.slice(*permitted_params_keys)
- end
+ raise ActiveRecord::Rollback
+ end
+ else
+ @response = error_on_save(integration)
+ end
+ end
- # overriden in EE
- def permitted_params_keys
- %i[name active]
+ @response
end
- def error(message)
- ServiceResponse.error(message: message)
- end
-
- def success(integration)
- ServiceResponse.success(payload: { integration: integration })
- end
+ private
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')
diff --git a/app/services/alert_management/http_integrations/destroy_service.rb b/app/services/alert_management/http_integrations/destroy_service.rb
index aeb3f6cb807..1bd73ca46e4 100644
--- a/app/services/alert_management/http_integrations/destroy_service.rb
+++ b/app/services/alert_management/http_integrations/destroy_service.rb
@@ -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
diff --git a/app/services/alert_management/http_integrations/update_service.rb b/app/services/alert_management/http_integrations/update_service.rb
index 8662f966a2e..f7a079576e4 100644
--- a/app/services/alert_management/http_integrations/update_service.rb
+++ b/app/services/alert_management/http_integrations/update_service.rb
@@ -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 type_update? && too_many_integrations?(integration)
+ @response = error_multiple_integrations
- if integration.update(permitted_params)
- success
- else
- error(integration.errors.full_messages.to_sentence)
+ 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)
- end
+ def token_params
+ return {} unless params[:regenerate_token]
- def permitted_params
- params.slice(*permitted_params_keys)
+ { token: nil }
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')
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 1e084c0e5eb..1d24a113e05 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -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?
diff --git a/config/feature_flags/development/npm_group_level_endpoints.yml b/config/feature_flags/development/npm_group_level_endpoints.yml
new file mode 100644
index 00000000000..13ebade2cec
--- /dev/null
+++ b/config/feature_flags/development/npm_group_level_endpoints.yml
@@ -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
diff --git a/config/metrics/counts_28d/20230417064258_i_container_registry_writes_user_monthly.yml b/config/metrics/counts_28d/20230417064258_i_container_registry_writes_user_monthly.yml
index a114ce9fbaa..ba8507dcc25 100644
--- a/config/metrics/counts_28d/20230417064258_i_container_registry_writes_user_monthly.yml
+++ b/config/metrics/counts_28d/20230417064258_i_container_registry_writes_user_monthly.yml
@@ -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
diff --git a/db/migrate/20230428165514_add_type_to_http_integrations.rb b/db/migrate/20230428165514_add_type_to_http_integrations.rb
new file mode 100644
index 00000000000..b799b35fbbd
--- /dev/null
+++ b/db/migrate/20230428165514_add_type_to_http_integrations.rb
@@ -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
diff --git a/db/schema_migrations/20230428165514 b/db/schema_migrations/20230428165514
new file mode 100644
index 00000000000..2cad40833c1
--- /dev/null
+++ b/db/schema_migrations/20230428165514
@@ -0,0 +1 @@
+ad16293967c9751d138690328308944dd0930cd88e1afa16d825fbaf2cc8299c \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9e64191eb74..e8fdacd0ee9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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)),
diff --git a/doc/api/packages/npm.md b/doc/api/packages/npm.md
index bf48fbc8f65..664737e317a 100644
--- a/doc/api/packages/npm.md
+++ b/doc/api/packages/npm.md
@@ -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 group’s 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.
diff --git a/doc/api/users.md b/doc/api/users.md
index a2293090209..4d6bdb26020 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -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
diff --git a/doc/ci/secrets/id_token_authentication.md b/doc/ci/secrets/id_token_authentication.md
index 1ff2a6efbcf..12e0402be25 100644
--- a/doc/ci/secrets/id_token_authentication.md
+++ b/doc/ci/secrets/id_token_authentication.md
@@ -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",
diff --git a/doc/user/compliance/compliance_report/index.md b/doc/user/compliance/compliance_report/index.md
index d04aeec066f..90167b0b5c7 100644
--- a/doc/user/compliance/compliance_report/index.md
+++ b/doc/user/compliance/compliance_report/index.md
@@ -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
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 38a1c4125aa..e05c28bd3f7 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -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:
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 52fdda0d523..33ae73dddc2 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -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).
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index da4d2da70fe..e72113dc321 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -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.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index f50c705c3ea..a7acd44e72a 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -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
diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb
index 4eb6c39b7dc..b4a66d6177a 100644
--- a/lib/api/helpers/packages/npm.rb
+++ b/lib/api/helpers/packages/npm.rb
@@ -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
diff --git a/lib/api/npm_group_packages.rb b/lib/api/npm_group_packages.rb
new file mode 100644
index 00000000000..7e6da6b4b02
--- /dev/null
+++ b/lib/api/npm_group_packages.rb
@@ -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
diff --git a/lib/api/npm_instance_packages.rb b/lib/api/npm_instance_packages.rb
index e387dd65e41..8215296b617 100644
--- a/lib/api/npm_instance_packages.rb
+++ b/lib/api/npm_instance_packages.rb
@@ -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
diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb
index 171a061bf97..61409909b06 100644
--- a/lib/api/npm_project_packages.rb
+++ b/lib/api/npm_project_packages.rb
@@ -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
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3d9af536c3c..dc77dc5c157 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 82b5c1944cc..e39517033a1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb
index ce558849abd..1a25aeb4c19 100644
--- a/qa/qa/service/docker_run/base.rb
+++ b/qa/qa/service/docker_run/base.rb
@@ -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
diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb
index 218d5eecc85..c825793cc3c 100644
--- a/qa/qa/service/shellout.rb
+++ b/qa/qa/service/shellout.rb
@@ -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)
diff --git a/spec/factories/alert_management/http_integrations.rb b/spec/factories/alert_management/http_integrations.rb
index 405ec09251f..43cf8b3c6db 100644
--- a/spec/factories/alert_management/http_integrations.rb
+++ b/spec/factories/alert_management/http_integrations.rb
@@ -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
diff --git a/spec/finders/alert_management/http_integrations_finder_spec.rb b/spec/finders/alert_management/http_integrations_finder_spec.rb
index d65de2cdbbd..eb3d24f8653 100644
--- a/spec/finders/alert_management/http_integrations_finder_spec.rb
+++ b/spec/finders/alert_management/http_integrations_finder_spec.rb
@@ -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
+ context 'but blank' do
+ let(:params) { { active: nil } }
- it { is_expected.to be_empty }
+ 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) { { active: nil } }
+ let(:params) { { type_identifier: nil } }
- it { is_expected.to contain_exactly(integration) }
+ it { is_expected.to contain_exactly(integration, prometheus_integration) }
end
end
diff --git a/spec/frontend/fixtures/pipeline_details.rb b/spec/frontend/fixtures/pipeline_details.rb
new file mode 100644
index 00000000000..af9b11b0841
--- /dev/null
+++ b/spec/frontend/fixtures/pipeline_details.rb
@@ -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
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 0799bc87c8c..4bf3a779f00 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -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';
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
deleted file mode 100644
index 724ec7366d3..00000000000
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ /dev/null
@@ -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,
- },
- },
-]
-`;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 95207fd59ff..e9bce037800 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -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;
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index cc952eac1d7..9599b5e6b7b 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -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,
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 6e4b9498918..bcea140f2dd 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -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);
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 08624cc511d..b012e7f66e1 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -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: [
{
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 9d39c86ed5e..88ba84c395a 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -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;
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index 51e0e0705ff..286d79edc6c 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -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();
- });
});
});
diff --git a/spec/lib/api/helpers/packages/npm_spec.rb b/spec/lib/api/helpers/packages/npm_spec.rb
index e1316a10fb1..cfb68d2c53e 100644
--- a/spec/lib/api/helpers/packages/npm_spec.rb
+++ b/spec/lib/api/helpers/packages/npm_spec.rb
@@ -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' } }
diff --git a/spec/migrations/add_type_to_http_integrations_spec.rb b/spec/migrations/add_type_to_http_integrations_spec.rb
new file mode 100644
index 00000000000..8238c1594dc
--- /dev/null
+++ b/spec/migrations/add_type_to_http_integrations_spec.rb
@@ -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
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb
index b453b3a82e0..606b53aeacd 100644
--- a/spec/models/alert_management/http_integration_spec.rb
+++ b/spec/models/alert_management/http_integration_spec.rb
@@ -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
diff --git a/spec/requests/api/npm_group_packages_spec.rb b/spec/requests/api/npm_group_packages_spec.rb
new file mode 100644
index 00000000000..888ce548e6d
--- /dev/null
+++ b/spec/requests/api/npm_group_packages_spec.rb
@@ -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
diff --git a/spec/services/alert_management/http_integrations/create_service_spec.rb b/spec/services/alert_management/http_integrations/create_service_spec.rb
index 5200ec27dd1..bced09044eb 100644
--- a/spec/services/alert_management/http_integrations/create_service_spec.rb
+++ b/spec/services/alert_management/http_integrations/create_service_spec.rb
@@ -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
diff --git a/spec/services/alert_management/http_integrations/destroy_service_spec.rb b/spec/services/alert_management/http_integrations/destroy_service_spec.rb
index a8e9746cb85..e3d9ddfbad8 100644
--- a/spec/services/alert_management/http_integrations/destroy_service_spec.rb
+++ b/spec/services/alert_management/http_integrations/destroy_service_spec.rb
@@ -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
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 0feac6c3e72..24affa45aa5 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -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
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 403456fa48e..417bf4366c5 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -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
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index f53532d00d7..d1712a3c02a 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -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