Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-04 12:12:34 +00:00
parent c038ef70ff
commit ca4942bdc4
26 changed files with 348 additions and 27 deletions

View File

@ -18,7 +18,7 @@ variables:
# Helm chart ref used by test-on-cng pipeline
GITLAB_HELM_CHART_REF: "6cdb0e1cd4ceb7c9fd01ffa2f62c4a7a4c77a23b"
# Specific ref for cng-mirror project to trigger builds for
GITLAB_CNG_MIRROR_REF: "8c4bbd04b509dc6cc3cb0469066ef053db028607"
GITLAB_CNG_MIRROR_REF: "00736d96dbee30eaf6fa3701f0cfa99ad8621cf4"
# Makes sure some of the common scripts from pipeline-common use bundler to execute commands
RUN_WITH_BUNDLE: "true"
# Makes sure reporting script defined in .gitlab-qa-report from pipeline-common is executed from correct folder

View File

@ -84,7 +84,7 @@ workflow:
--print-deploy-args \
$(cat $CI_PROJECT_DIR/EXTRA_DEPLOY_VALUES)
artifacts:
expire_in: 1 day
expire_in: 7 days
when: always
reports:
junit: qa/tmp/rspec-*.xml

View File

@ -1 +1 @@
311c173844ce85e28e51818063bcac8e75dee41e
6093f9c42ad8d328c37423e3067f3c9e7cd1ddcf

View File

@ -3,7 +3,7 @@ import { __ } from '~/locale';
import { validateAdditionalProperties } from '~/tracking/utils';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
import { joinPaths, isAbsolute } from './lib/utils/url_utility';
export const DEFAULT_PER_PAGE = 20;
@ -963,8 +963,30 @@ const Api = {
});
},
/**
* buildUrl (1) replaces `:version` placeholder by `gon.api_version` and
* (2) prepends the url with `gon.relative_url_root`, if the `url` argument
* is not an absolute URL (http://...).
*
* Using `gon.relative_url_root` is vital for GitLab instances installed on
* relative paths: https://docs.gitlab.com/install/relative_url/.
*
* In Rails, **API** paths, like `api_v..._path`, do not include the
* `gon.relative_url_root`. Since Rails doesn't provide `api_v..._url` helpers,
* `expose_url` backend method can be used as an alternative to `buildUrl`.
*
* buildUrl('/api/:version/projects/1') => '/[relative_url_root]/api/v4/projects/1'
*
* @param {string} url -
*/
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
const withVersion = url.replace(':version', gon.api_version);
if (isAbsolute(withVersion)) {
return withVersion;
}
return joinPaths(gon.relative_url_root || '', withVersion);
},
fetchFeatureFlagUserLists(id, page) {

View File

@ -17,7 +17,7 @@ export const createRouter = () => {
{ name: ACTIVE_TAB_SHARED, path: '/groups/:group*/-/shared' },
{ name: ACTIVE_TAB_SHARED_GROUPS, path: '/groups/:group*/-/shared_groups' },
{ name: ACTIVE_TAB_INACTIVE, path: '/groups/:group*/-/inactive' },
{ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, path: '/:group*' },
{ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, path: '/(groups)?/:group*' },
];
const router = new VueRouter({

View File

@ -29,12 +29,8 @@ export default {
<template>
<div>
<gl-link
:href="group.webUrl"
target="_blank"
class="gl-inline-flex gl-h-7 gl-items-center gl-gap-2"
>
{{ group.fullPath }} <gl-icon name="external-link" class="gl-fill-icon-link" />
<gl-link :href="group.webUrl" target="_blank">
{{ group.fullPath }}&nbsp;<gl-icon name="external-link" class="gl-fill-icon-link" />
</gl-link>
<div v-if="group.flags.isFinished && fullLastImportPath" class="gl-text-sm gl-text-subtle">
<gl-sprintf :message="s__('BulkImport|Last imported to %{link}')">

View File

@ -2,6 +2,13 @@
module Groups
class ObservabilityController < Groups::ApplicationController
content_security_policy do |p|
next if p.directives.blank? || ENV['O11Y_URL'].blank?
frame_src_values = Array.wrap(p.directives['frame-src']) | ["'self'", ENV['O11Y_URL'].to_s]
p.frame_src(*frame_src_values)
end
before_action :authenticate_user!
before_action :authorize_read_observability!

View File

@ -50,6 +50,11 @@ module Mutations
required: false,
description: 'Input for labels widget.'
argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyCreateInputType,
required: false,
description: 'Input for hierarchy widget.',
experiment: { milestone: '18.2' }
field :updated_work_item_count, GraphQL::Types::Int,
null: true,
description: 'Number of work items that were successfully updated.'

View File

@ -115,6 +115,8 @@
- 1
- - authz_user_group_member_roles_destroy_for_shared_group
- 1
- - authz_user_group_member_roles_update_for_group
- 1
- - auto_devops
- 2
- - auto_merge
@ -277,6 +279,8 @@
- 1
- - compliance_management_compliance_framework_projects_compliance_enqueue
- 1
- - compliance_management_compliance_violation_detection
- 1
- - compliance_management_framework_export_mailer
- 1
- - compliance_management_merge_requests_compliance_violations

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class UpdateUserGroupMemberRolesIndexes < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
OLD_INDEX_NAME = 'unique_user_group_member_roles_all_ids'
OLD_INDEX_COLUMNS = %i[user_id group_id shared_with_group_id member_role_id]
def up
remove_concurrent_index_by_name :user_group_member_roles, name: OLD_INDEX_NAME
add_concurrent_index :user_group_member_roles, :user_id,
name: 'index_user_group_member_roles_on_user_id'
add_concurrent_index :user_group_member_roles, %i[user_id group_id],
name: 'unique_user_group_member_roles_user_group', unique: true,
where: 'shared_with_group_id IS NULL'
add_concurrent_index :user_group_member_roles, %i[user_id group_id shared_with_group_id],
name: 'unique_user_group_member_roles_user_group_shared_with_group',
unique: true, where: 'shared_with_group_id IS NOT NULL'
end
def down
remove_concurrent_index_by_name :user_group_member_roles,
name: 'index_user_group_member_roles_on_user_id',
if_exists: true
remove_concurrent_index_by_name :user_group_member_roles,
name: 'unique_user_group_member_roles_user_group',
if_exists: true
remove_concurrent_index_by_name :user_group_member_roles,
name: 'unique_user_group_member_roles_user_group_shared_with_group',
if_exists: true
add_concurrent_index :user_group_member_roles, OLD_INDEX_COLUMNS,
name: OLD_INDEX_NAME, unique: true
end
end

View File

@ -0,0 +1 @@
34deeac3e26ea820bbc31642d64f4cbb7f88186359c4fce3938c86b6953e0acd

View File

@ -38271,6 +38271,8 @@ CREATE INDEX index_user_group_member_roles_on_member_role_id ON user_group_membe
CREATE INDEX index_user_group_member_roles_on_shared_with_group_id ON user_group_member_roles USING btree (shared_with_group_id);
CREATE INDEX index_user_group_member_roles_on_user_id ON user_group_member_roles USING btree (user_id);
CREATE INDEX index_user_highest_roles_on_user_id_and_highest_access_level ON user_highest_roles USING btree (user_id, highest_access_level);
CREATE INDEX index_user_id_and_notification_email_to_notification_settings ON notification_settings USING btree (user_id, notification_email, id) WHERE (notification_email IS NOT NULL);
@ -39421,7 +39423,9 @@ CREATE UNIQUE INDEX unique_streaming_event_type_filters_destination_id ON audit_
CREATE UNIQUE INDEX unique_streaming_instance_event_type_filters_destination_id ON audit_events_streaming_instance_event_type_filters USING btree (instance_external_audit_event_destination_id, audit_event_type);
CREATE UNIQUE INDEX unique_user_group_member_roles_all_ids ON user_group_member_roles USING btree (user_id, group_id, shared_with_group_id, member_role_id);
CREATE UNIQUE INDEX unique_user_group_member_roles_user_group ON user_group_member_roles USING btree (user_id, group_id) WHERE (shared_with_group_id IS NULL);
CREATE UNIQUE INDEX unique_user_group_member_roles_user_group_shared_with_group ON user_group_member_roles USING btree (user_id, group_id, shared_with_group_id) WHERE (shared_with_group_id IS NOT NULL);
CREATE UNIQUE INDEX unique_user_id_setting_type_and_settings_context_hash ON vs_code_settings USING btree (user_id, setting_type, settings_context_hash);

View File

@ -396,7 +396,7 @@ These are only examples and may not work on all setups. Further adjustments may
- **Data received**: `rate(node_network_receive_bytes_total{device!="lo"}[5m])`
- **Disk read IOPS**: `sum by (instance) (rate(node_disk_reads_completed_total[1m]))`
- **Disk write IOPS**: `sum by (instance) (rate(node_disk_writes_completed_total[1m]))`
- **RPS via GitLab transaction count**: `sum(irate(gitlab_transaction_duration_seconds_count{controller!~'HealthController|MetricsController|'}[1m])) by (controller, action)`
- **RPS via GitLab transaction count**: `sum(irate(gitlab_transaction_duration_seconds_count{controller!~'HealthController|MetricsController'}[1m])) by (controller, action)`
## Prometheus as a Grafana data source

View File

@ -82,7 +82,7 @@ Each architecture is designed to handle specific RPS targets for different types
Finding out the RPS can depend notably on the specific environment setup and monitoring stack. Some potential options include:
- [GitLab Prometheus](../monitoring/prometheus/_index.md#sample-prometheus-queries) with queries like `sum(irate(gitlab_transaction_duration_seconds_count{controller!~'HealthController|MetricsController|'}[1m])) by (controller, action)`.
- [GitLab Prometheus](../monitoring/prometheus/_index.md#sample-prometheus-queries) with queries like `sum(irate(gitlab_transaction_duration_seconds_count{controller!~'HealthController|MetricsController'}[1m])) by (controller, action)`.
- [`get-rps` script](https://gitlab.com/gitlab-com/support/toolbox/dotfiles/-/blob/main/scripts/get-rps.rb?ref_type=heads) from GitLab Support.
- Other monitoring solutions.
- Load Balancer statistics.

View File

@ -2224,6 +2224,34 @@ Input type: `AiAgentUpdateInput`
| <a id="mutationaiagentupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiagentupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
### `Mutation.aiCatalogAgentCreate`
{{< details >}}
**Introduced** in GitLab 18.2.
**Status**: Experiment.
{{< /details >}}
Input type: `AiCatalogAgentCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationaicatalogagentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaicatalogagentcreatedescription"></a>`description` | [`String!`](#string) | Description for the agent. |
| <a id="mutationaicatalogagentcreatename"></a>`name` | [`String!`](#string) | Name for the agent. |
| <a id="mutationaicatalogagentcreateprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | Project for the agent. |
| <a id="mutationaicatalogagentcreatesystemprompt"></a>`systemPrompt` | [`String!`](#string) | System prompt for the agent. |
| <a id="mutationaicatalogagentcreateuserprompt"></a>`userPrompt` | [`String!`](#string) | User prompt for the agent. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationaicatalogagentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaicatalogagentcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
| <a id="mutationaicatalogagentcreateitem"></a>`item` | [`AiCatalogItem`](#aicatalogitem) | Item created. |
### `Mutation.aiDuoWorkflowCreate`
{{< details >}}
@ -13048,6 +13076,7 @@ Input type: `WorkItemBulkUpdateInput`
| <a id="mutationworkitembulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitembulkupdateconfidential"></a>`confidential` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdatehealthstatuswidget"></a>`healthStatusWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetHealthStatusInput`](#workitemwidgethealthstatusinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdatehierarchywidget"></a>`hierarchyWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdateids"></a>`ids` | [`[WorkItemID!]!`](#workitemid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
| <a id="mutationworkitembulkupdateiterationwidget"></a>`iterationWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetIterationInput`](#workitemwidgetiterationinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdatelabelswidget"></a>`labelsWidget` | [`WorkItemWidgetLabelsUpdateInput`](#workitemwidgetlabelsupdateinput) | Input for labels widget. |
@ -21755,6 +21784,7 @@ An AI catalog item.
| <a id="aicatalogitemid"></a>`id` | [`ID!`](#id) | ID of the item. |
| <a id="aicatalogitemitemtype"></a>`itemType` | [`AiCatalogItemType!`](#aicatalogitemtype) | Type of the item. |
| <a id="aicatalogitemname"></a>`name` | [`String!`](#string) | Name of the item. |
| <a id="aicatalogitemproject"></a>`project` | [`Project`](#project) | Project for the item. |
### `AiConversationsThread`
@ -25253,6 +25283,7 @@ Presets for GitLab Duo Chat window based on current context.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="contextpresetairesourcedata"></a>`aiResourceData` | [`String`](#string) | Serialized representation of the AI resource in the current context. |
| <a id="contextpresetquestions"></a>`questions` | [`[String!]`](#string) | Array of questions that the user can ask GitLab Duo Chat from the current page. |
### `ContributionAnalyticsContribution`

View File

@ -25,6 +25,7 @@ module Gitlab
def log_events(entity_type, entity_events)
event_class = ENTITY_TYPE_TO_CLASS[entity_type.to_s]
if entity_events.one?
[event_class.create!(build_event_attributes(entity_events.first))]
else
@ -65,3 +66,5 @@ module Gitlab
end
end
end
Gitlab::Audit::Logging.prepend_mod

View File

@ -14,7 +14,7 @@ variables:
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
#
DS_EXCLUDED_ANALYZERS: ""
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_EXCLUDED_PATHS: "spec, test, tests, tmp, node_modules"
DS_MAJOR_VERSION: 6
DS_SCHEMA_MODEL: 15

View File

@ -19,7 +19,7 @@ variables:
AST_ENABLE_MR_PIPELINES: "true"
#
DS_EXCLUDED_ANALYZERS: ""
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_EXCLUDED_PATHS: "spec, test, tests, tmp, node_modules"
DS_MAJOR_VERSION: 6
DS_SCHEMA_MODEL: 15
# Use this variable to enforce the new Dependency Scanning analyzer for all projects
@ -318,7 +318,7 @@ dependency-scanning:
exists:
- '**/{conda-lock.yml,pubspec.lock,Podfile.lock,Cargo.lock,Package.resolved}'
variables:
DS_EXCLUDED_PATHS: 'spec, test, tests, tmp, **/build.gradle, **/build.gradle.kts, **/build.sbt, **/pom.xml, **/requirements.txt, **/requirements.pip, **/Pipfile, **/Pipfile.lock, **/requires.txt, **/setup.py, **/poetry.lock, **/uv.lock, **/packages.lock.json, **/conan.lock, **/package-lock.json, **/npm-shrinkwrap.json, **/pnpm-lock.yaml, **/yarn.lock, **/composer.lock, **/Gemfile.lock, **/gems.locked, **/go.graph, **/ivy-report.xml, **/maven.graph.json, **/dependencies.lock, **/pipdeptree.json, **/pipenv.graph.json, **/dependencies-compile.dot'
DS_EXCLUDED_PATHS: 'spec, test, tests, tmp, node_modules, **/build.gradle, **/build.gradle.kts, **/build.sbt, **/pom.xml, **/requirements.txt, **/requirements.pip, **/Pipfile, **/Pipfile.lock, **/requires.txt, **/setup.py, **/poetry.lock, **/uv.lock, **/packages.lock.json, **/conan.lock, **/package-lock.json, **/npm-shrinkwrap.json, **/pnpm-lock.yaml, **/yarn.lock, **/composer.lock, **/Gemfile.lock, **/gems.locked, **/go.graph, **/ivy-report.xml, **/maven.graph.json, **/dependencies.lock, **/pipdeptree.json, **/pipenv.graph.json, **/dependencies-compile.dot'
# 2. Don't run the job in a *branch pipeline* if *MR pipelines* for AST are enabled and there's an open merge request.
- if: $AST_ENABLE_MR_PIPELINES == "true" &&
@ -348,4 +348,4 @@ dependency-scanning:
exists:
- '**/{conda-lock.yml,pubspec.lock,Podfile.lock,Cargo.lock,Package.resolved}'
variables:
DS_EXCLUDED_PATHS: 'spec, test, tests, tmp, **/build.gradle, **/build.gradle.kts, **/build.sbt, **/pom.xml, **/requirements.txt, **/requirements.pip, **/Pipfile, **/Pipfile.lock, **/requires.txt, **/setup.py, **/poetry.lock, **/uv.lock, **/packages.lock.json, **/conan.lock, **/package-lock.json, **/npm-shrinkwrap.json, **/pnpm-lock.yaml, **/yarn.lock, **/composer.lock, **/Gemfile.lock, **/gems.locked, **/go.graph, **/ivy-report.xml, **/maven.graph.json, **/dependencies.lock, **/pipdeptree.json, **/pipenv.graph.json, **/dependencies-compile.dot'
DS_EXCLUDED_PATHS: 'spec, test, tests, tmp, node_modules, **/build.gradle, **/build.gradle.kts, **/build.sbt, **/pom.xml, **/requirements.txt, **/requirements.pip, **/Pipfile, **/Pipfile.lock, **/requires.txt, **/setup.py, **/poetry.lock, **/uv.lock, **/packages.lock.json, **/conan.lock, **/package-lock.json, **/npm-shrinkwrap.json, **/pnpm-lock.yaml, **/yarn.lock, **/composer.lock, **/Gemfile.lock, **/gems.locked, **/go.graph, **/ivy-report.xml, **/maven.graph.json, **/dependencies.lock, **/pipdeptree.json, **/pipenv.graph.json, **/dependencies-compile.dot'

View File

@ -12,9 +12,7 @@ module Gitlab
end
def self.frame_src
base_urls = "https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.googletagmanager.com/ns.html"
ENV['O11Y_URL'].present? ? "#{base_urls} #{ENV['O11Y_URL']}" : base_urls
"https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.googletagmanager.com/ns.html"
end
def self.script_src

View File

@ -173,6 +173,40 @@ RSpec.describe 'Group show page', feature_category: :groups_and_projects do
expect(page).to have_content(content)
end
end
describe 'tab frontend routing' do
context 'when route is not prefixed with group' do
before do
group.add_developer(user)
sign_in(user)
visit group_path(group)
end
it 'still allows for tab navigation and reloading', :js do
click_link _('Shared projects')
wait_for_requests
page.refresh
expect(page).to have_link('Shared projects')
end
end
context 'when route is prefixed with group' do
before do
group.add_developer(user)
sign_in(user)
visit group_canonical_path(group)
end
it 'still allows for tab navigation and reloading', :js do
click_link _('Shared projects')
wait_for_requests
page.refresh
expect(page).to have_link('Shared projects')
end
end
end
end
context 'when signed out' do

View File

@ -87,7 +87,7 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
end
end
context "with access control", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/509488' do
context "with access control" do
before do
stub_licensed_features(protected_refs_for_users: false)
end
@ -95,8 +95,7 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
include_examples "protected tags > access control > CE"
end
context 'when the users for protected tags feature is off',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/509488' do
context 'when the users for protected tags feature is off' do
before do
stub_licensed_features(protected_refs_for_users: false)
end

View File

@ -23,6 +23,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
setUrlFragment: jest.requireActual('~/lib/utils/url_utility').setUrlFragment,
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
}));
describe('AlertManagementTable', () => {

View File

@ -28,7 +28,15 @@ describe('Api', () => {
});
describe('buildUrl', () => {
it('adds URL root and fills in API version', () => {
describe('when input is an absolute URL', () => {
it('fills in API version but does not add relative URL root', () => {
const input = 'https://gitlab.com/api/:version/foo/bar';
expect(Api.buildUrl(input)).toEqual(`https://gitlab.com/api/${dummyApiVersion}/foo/bar`);
});
});
it('adds relative URL root and fills in API version', () => {
const input = '/api/:version/foo/bar';
const expectedOutput = `${dummyUrlRoot}/api/${dummyApiVersion}/foo/bar`;

View File

@ -28,6 +28,13 @@ RSpec.describe 'Bulk update work items', feature_category: :team_planning do
}
end
before_all do
# Ensure support bot user is created so creation doesn't count towards query limit
# and we don't try to obtain an exclusive lease within a transaction.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/509629
Users::Internal.support_bot_id
end
context 'when Gitlab is FOSS only' do
unless Gitlab.ee?
context 'when parent is a group' do
@ -206,6 +213,24 @@ RSpec.describe 'Bulk update work items', feature_category: :team_planning do
end
end
context 'when updating parent' do
let_it_be(:parent_work_item) { create(:work_item, :issue, project: project) }
let_it_be(:task_work_item) { create(:work_item, :task, project: project) }
let_it_be(:issue_work_item) { updatable_work_items.first }
let_it_be(:updatable_work_items) { [task_work_item, issue_work_item] }
let(:additional_arguments) { { hierarchy_widget: { parent_id: parent_work_item.to_gid.to_s } } }
it 'updates the parent for the appropriate work item(s)' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to not_change { issue_work_item.reload.work_item_parent }.from(nil)
.and change { task_work_item.reload.work_item_parent }.from(nil).to(parent_work_item)
expect(mutation_response).to include('updatedWorkItemCount' => 1)
end
end
context 'when updating multiple attributes simultaneously' do
let_it_be(:assignee) { create(:user, developer_of: group) }
let_it_be(:milestone) { create(:milestone, project: project) }

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Groups::ObservabilityController, feature_category: :observability do
include ContentSecurityPolicyHelpers
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
@ -98,4 +100,144 @@ RSpec.describe Groups::ObservabilityController, feature_category: :observability
end
end
end
describe 'Content Security Policy' do
subject(:csp_header) { response.headers['Content-Security-Policy'] }
before do
stub_feature_flags(observability_sass_features: group)
end
context 'when O11Y_URL environment variable is set' do
let(:o11y_url) { 'https://observability.example.com' }
before do
stub_env('O11Y_URL', o11y_url)
end
context 'when CSP directives are present' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
p.frame_src "'self'", 'https://existing-frame.example.com'
end
end
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'adds O11Y_URL to frame-src directive' do
frame_src_values = find_csp_directive('frame-src', header: csp_header)
expect(frame_src_values).to include("'self'", 'https://existing-frame.example.com', o11y_url)
end
end
context 'when CSP frame-src directive is not present' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
p.script_src "'self'"
end
end
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'creates frame-src directive with O11Y_URL' do
frame_src_values = find_csp_directive('frame-src', header: csp_header)
expect(frame_src_values).to include("'self'", o11y_url)
end
end
context 'when CSP has no directives' do
let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'does not add frame-src directive' do
expect(csp_header).to be_blank
end
end
end
context 'when O11Y_URL environment variable is not set' do
before do
stub_env('O11Y_URL', nil)
end
context 'when CSP directives are present' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
p.frame_src "'self'", 'https://existing-frame.example.com'
end
end
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'does not modify frame-src directive' do
frame_src_values = find_csp_directive('frame-src', header: csp_header)
expect(frame_src_values).to contain_exactly("'self'", 'https://existing-frame.example.com')
end
end
context 'when CSP has no directives' do
let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'does not add frame-src directive' do
expect(csp_header).to be_blank
end
end
end
context 'when O11Y_URL environment variable is empty string' do
before do
stub_env('O11Y_URL', '')
end
context 'when CSP directives are present' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
p.frame_src "'self'", 'https://existing-frame.example.com'
end
end
before do
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'does not modify frame-src directive' do
frame_src_values = find_csp_directive('frame-src', header: csp_header)
expect(frame_src_values).to contain_exactly("'self'", 'https://existing-frame.example.com')
end
end
end
context 'when CSP directives are blank' do
let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
before do
stub_env('O11Y_URL', 'https://observability.example.com')
stub_csp_for_controller(described_class, csp)
get group_observability_path(group, 'services')
end
it 'does not add frame-src directive' do
expect(csp_header).to be_blank
end
end
end
end

View File

@ -61,7 +61,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do
end
end
context 'when deploy key is already selected for protected branch' do
context 'when deploy key is already selected for protected tag' do
let(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0') }
let(:write_access_key) { create(:deploy_key, user: user, write_access_to: project) }