diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-16 12:09:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-16 12:09:06 +0000 |
commit | 0045970352e8729b2797591beb88a7df884d84f4 (patch) | |
tree | b9cd4c5aaaa26ce4a3c944ec5cfdbd7ad44b796d | |
parent | 613868af23d7c0e09210857518895edd6678f5e9 (diff) | |
download | gitlab-ce-0045970352e8729b2797591beb88a7df884d84f4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |