Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js2
-rw-r--r--app/assets/javascripts/tracking/constants.js4
-rw-r--r--app/assets/javascripts/tracking/index.js3
-rw-r--r--app/assets/javascripts/tracking/tracking.js40
-rw-r--r--app/assets/javascripts/tracking/utils.js24
-rw-r--r--app/graphql/mutations/customer_relations/organizations/create.rb9
-rw-r--r--app/graphql/mutations/customer_relations/organizations/update.rb52
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/analytics/cycle_analytics_helper.rb18
-rw-r--r--app/helpers/routing/pseudonymization_helper.rb20
-rw-r--r--app/services/customer_relations/organizations/base_service.rb17
-rw-r--r--app/services/customer_relations/organizations/create_service.rb10
-rw-r--r--app/services/customer_relations/organizations/update_service.rb24
-rw-r--r--app/views/layouts/_snowplow.html.haml1
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml5
-rw-r--r--doc/api/graphql/reference/index.md28
-rw-r--r--doc/user/admin_area/analytics/dev_ops_report.md2
-rw-r--r--doc/user/clusters/agent/ci_cd_tunnel.md5
-rw-r--r--doc/user/clusters/agent/index.md4
-rw-r--r--doc/user/clusters/agent/repository.md35
-rw-r--r--lib/gitlab/saas.rb12
-rw-r--r--lib/gitlab/subscription_portal.rb32
-rw-r--r--qa/qa/runtime/browser.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb39
-rw-r--r--spec/features/groups/issues_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb9
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb3
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js8
-rw-r--r--spec/frontend/tracking_spec.js114
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/create_spec.rb8
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/update_spec.rb74
-rw-r--r--spec/helpers/analytics/cycle_analytics_helper_spec.rb61
-rw-r--r--spec/helpers/routing/pseudonymization_helper_spec.rb21
-rw-r--r--spec/services/customer_relations/organizations/update_service_spec.rb56
35 files changed, 694 insertions, 58 deletions
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 5aa86d24dd2..e39cd224199 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -9,7 +9,6 @@ import {
import createFlash from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
-import { appendExtension } from '../utils';
import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
@@ -184,8 +183,8 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
} = initialData;
dispatch('filters/setEndpoints', {
- labelsEndpoint: appendExtension(labelsPath),
- milestonesEndpoint: appendExtension(milestonesPath),
+ labelsEndpoint: labelsPath,
+ milestonesEndpoint: milestonesPath,
groupEndpoint: groupPath,
projectEndpoint: fullPath,
});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 888ef68e69a..fa02fdf914a 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -149,5 +149,3 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '',
};
});
-
-export const appendExtension = (path) => (path.indexOf('.') > -1 ? path : `${path}.json`);
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 598111e4086..062a3404355 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -24,3 +24,7 @@ export const LOAD_ACTION_ATTR_SELECTOR = '[data-track-action="render"]';
export const DEPRECATED_EVENT_ATTR_SELECTOR = '[data-track-event]';
export const DEPRECATED_LOAD_EVENT_ATTR_SELECTOR = '[data-track-event="render"]';
+
+export const URLS_CACHE_STORAGE_KEY = 'gl-snowplow-pseudonymized-urls';
+
+export const REFERRER_TTL = 24 * 60 * 60 * 1000;
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index b7240bc6d6d..7e99ecb4f4e 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -39,6 +39,9 @@ export function initDefaultTrackers() {
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
+ // must be before initializing the trackers
+ Tracking.setAnonymousUrls();
+
window.snowplow('enableActivityTracking', 30, 30);
// must be after enableActivityTracking
const standardContext = getStandardContext();
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
index a1f745bc172..657e0a79911 100644
--- a/app/assets/javascripts/tracking/tracking.js
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -1,7 +1,14 @@
import { LOAD_ACTION_ATTR_SELECTOR, DEPRECATED_LOAD_EVENT_ATTR_SELECTOR } from './constants';
import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
import getStandardContext from './get_standard_context';
-import { getEventHandlers, createEventPayload, renameKey, addExperimentContext } from './utils';
+import {
+ getEventHandlers,
+ createEventPayload,
+ renameKey,
+ addExperimentContext,
+ getReferrersCache,
+ addReferrersCacheEntry,
+} from './utils';
export default class Tracking {
static queuedEvents = [];
@@ -159,6 +166,37 @@ export default class Tracking {
}
/**
+ * Replaces the URL and referrer for the default web context
+ * if the replacements are available.
+ *
+ * @returns {undefined}
+ */
+ static setAnonymousUrls() {
+ const { snowplowPseudonymizedPageUrl: pageUrl } = window.gl;
+
+ if (!pageUrl) {
+ return;
+ }
+
+ const referrers = getReferrersCache();
+ const pageLinks = Object.seal({ url: '', referrer: '', originalUrl: window.location.href });
+
+ pageLinks.url = `${pageUrl}${window.location.hash}`;
+ window.snowplow('setCustomUrl', pageLinks.url);
+
+ if (document.referrer) {
+ const node = referrers.find((links) => links.originalUrl === document.referrer);
+
+ if (node) {
+ pageLinks.referrer = node.url;
+ window.snowplow('setReferrerUrl', pageLinks.referrer);
+ }
+ }
+
+ addReferrersCacheEntry(referrers, pageLinks);
+ }
+
+ /**
* Returns an implementation of this class in the form of
* a Vue mixin.
*
diff --git a/app/assets/javascripts/tracking/utils.js b/app/assets/javascripts/tracking/utils.js
index 1189b2168ad..3507872b511 100644
--- a/app/assets/javascripts/tracking/utils.js
+++ b/app/assets/javascripts/tracking/utils.js
@@ -6,6 +6,8 @@ import {
LOAD_ACTION_ATTR_SELECTOR,
DEPRECATED_EVENT_ATTR_SELECTOR,
DEPRECATED_LOAD_EVENT_ATTR_SELECTOR,
+ URLS_CACHE_STORAGE_KEY,
+ REFERRER_TTL,
} from './constants';
export const addExperimentContext = (opts) => {
@@ -100,3 +102,25 @@ export const renameKey = (o, oldKey, newKey) => {
return ret;
};
+
+export const filterOldReferrersCacheEntries = (cache) => {
+ const now = Date.now();
+
+ return cache.filter((entry) => entry.timestamp && entry.timestamp > now - REFERRER_TTL);
+};
+
+export const getReferrersCache = () => {
+ try {
+ const referrers = JSON.parse(window.localStorage.getItem(URLS_CACHE_STORAGE_KEY) || '[]');
+
+ return filterOldReferrersCacheEntries(referrers);
+ } catch {
+ return [];
+ }
+};
+
+export const addReferrersCacheEntry = (cache, entry) => {
+ const referrers = JSON.stringify([{ ...entry, timestamp: Date.now() }, ...cache]);
+
+ window.localStorage.setItem(URLS_CACHE_STORAGE_KEY, referrers);
+};
diff --git a/app/graphql/mutations/customer_relations/organizations/create.rb b/app/graphql/mutations/customer_relations/organizations/create.rb
index 7e5324b21d1..3fa7b0327ca 100644
--- a/app/graphql/mutations/customer_relations/organizations/create.rb
+++ b/app/graphql/mutations/customer_relations/organizations/create.rb
@@ -38,15 +38,10 @@ module Mutations
def resolve(args)
group = authorized_find!(id: args[:group_id])
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
result = ::CustomerRelations::Organizations::CreateService.new(group: group, current_user: current_user, params: args).execute
-
- if result.success?
- { organization: result.payload }
- else
- { errors: result.errors }
- end
+ { organization: result.payload, errors: result.errors }
end
def find_object(id:)
diff --git a/app/graphql/mutations/customer_relations/organizations/update.rb b/app/graphql/mutations/customer_relations/organizations/update.rb
new file mode 100644
index 00000000000..c6ae62193f9
--- /dev/null
+++ b/app/graphql/mutations/customer_relations/organizations/update.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Mutations
+ module CustomerRelations
+ module Organizations
+ class Update < Mutations::BaseMutation
+ include ResolvesIds
+
+ graphql_name 'CustomerRelationsOrganizationUpdate'
+
+ authorize :admin_organization
+
+ field :organization,
+ Types::CustomerRelations::OrganizationType,
+ null: false,
+ description: 'Organization after the mutation.'
+
+ argument :id, ::Types::GlobalIDType[::CustomerRelations::Organization],
+ required: true,
+ description: 'Global ID of the organization.'
+
+ argument :name,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Name of the organization.'
+
+ argument :default_rate,
+ GraphQL::Types::Float,
+ required: false,
+ description: 'Standard billing rate for the organization.'
+
+ argument :description,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Description or notes for the organization.'
+
+ def resolve(args)
+ organization = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Organization))
+ raise_resource_not_available_error! unless organization
+
+ group = organization.group
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
+
+ authorize!(group)
+
+ result = ::CustomerRelations::Organizations::UpdateService.new(group: group, current_user: current_user, params: args).execute(organization)
+ { organization: result.payload, errors: result.errors }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 40d4f86de00..ea50af1c554 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -35,6 +35,7 @@ module Types
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::CustomEmoji::Destroy, feature_flag: :custom_emoji
mount_mutation Mutations::CustomerRelations::Organizations::Create
+ mount_mutation Mutations::CustomerRelations::Organizations::Update
mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update
mount_mutation Mutations::Environments::CanaryIngress::Update
diff --git a/app/helpers/analytics/cycle_analytics_helper.rb b/app/helpers/analytics/cycle_analytics_helper.rb
index c43ac545bf8..35a5d4f469d 100644
--- a/app/helpers/analytics/cycle_analytics_helper.rb
+++ b/app/helpers/analytics/cycle_analytics_helper.rb
@@ -7,5 +7,23 @@ module Analytics
Analytics::CycleAnalytics::StagePresenter.new(stage_params)
end
end
+
+ def cycle_analytics_initial_data(project, group = nil)
+ base_data = { project_id: project.id, group_path: project.group&.path, request_path: project_cycle_analytics_path(project), full_path: project.full_path }
+ svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
+ api_paths = group.present? ? cycle_analytics_group_api_paths(group) : cycle_analytics_project_api_paths(project)
+
+ base_data.merge(svgs, api_paths)
+ end
+
+ private
+
+ def cycle_analytics_group_api_paths(group)
+ { milestones_path: group_milestones_path(group, format: :json), labels_path: group_labels_path(group, format: :json), group_path: group_path(group), group_id: group&.id }
+ end
+
+ def cycle_analytics_project_api_paths(project)
+ { milestones_path: project_milestones_path(project, format: :json), labels_path: project_labels_path(project, format: :json), group_path: project.parent&.path, group_id: project.parent&.id }
+ end
end
end
diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb
index b83a2b30d06..1d9320f0106 100644
--- a/app/helpers/routing/pseudonymization_helper.rb
+++ b/app/helpers/routing/pseudonymization_helper.rb
@@ -6,6 +6,8 @@ module Routing
return unless Feature.enabled?(:mask_page_urls, type: :ops)
mask_params(Rails.application.routes.recognize_path(request.original_fullpath))
+ rescue ActionController::RoutingError, URI::InvalidURIError
+ nil
end
private
@@ -19,31 +21,37 @@ module Routing
end
def url_without_namespace_type(request_params)
- masked_url = "#{request.protocol}#{request.host_with_port}/"
+ masked_url = "#{request.protocol}#{request.host_with_port}"
masked_url += case request_params[:controller]
when 'groups'
- "namespace:#{group.id}/"
+ "/namespace:#{group.id}"
when 'projects'
- "namespace:#{project.namespace.id}/project:#{project.id}/"
+ "/namespace:#{project.namespace.id}/project:#{project.id}"
when 'root'
''
+ else
+ "#{request.path}"
end
+ masked_url += request.query_string.present? ? "?#{request.query_string}" : ''
+
masked_url
end
def url_with_namespace_type(request_params, namespace_type)
- masked_url = "#{request.protocol}#{request.host_with_port}/"
+ masked_url = "#{request.protocol}#{request.host_with_port}"
if request_params.has_key?(:project_id)
- masked_url += "namespace:#{project.namespace.id}/project:#{project.id}/-/#{namespace_type}/"
+ masked_url += "/namespace:#{project.namespace.id}/project:#{project.id}/-/#{namespace_type}"
end
if request_params.has_key?(:id)
- masked_url += namespace_type == 'blob' ? ':repository_path' : request_params[:id]
+ masked_url += namespace_type == 'blob' ? '/:repository_path' : "/#{request_params[:id]}"
end
+ masked_url += request.query_string.present? ? "?#{request.query_string}" : ''
+
masked_url
end
end
diff --git a/app/services/customer_relations/organizations/base_service.rb b/app/services/customer_relations/organizations/base_service.rb
new file mode 100644
index 00000000000..63261534b37
--- /dev/null
+++ b/app/services/customer_relations/organizations/base_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module CustomerRelations
+ module Organizations
+ class BaseService < ::BaseGroupService
+ private
+
+ def allowed?
+ current_user&.can?(:admin_organization, group)
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+ end
+ end
+end
diff --git a/app/services/customer_relations/organizations/create_service.rb b/app/services/customer_relations/organizations/create_service.rb
index 33dcb4b8976..9c223796eaf 100644
--- a/app/services/customer_relations/organizations/create_service.rb
+++ b/app/services/customer_relations/organizations/create_service.rb
@@ -2,7 +2,7 @@
module CustomerRelations
module Organizations
- class CreateService < ::BaseGroupService
+ class CreateService < BaseService
# returns the created organization
def execute
return error_no_permissions unless allowed?
@@ -18,14 +18,6 @@ module CustomerRelations
private
- def allowed?
- current_user&.can?(:admin_organization, group)
- end
-
- def error(message)
- ServiceResponse.error(message: message)
- end
-
def error_no_permissions
error('You have insufficient permissions to create an organization for this group')
end
diff --git a/app/services/customer_relations/organizations/update_service.rb b/app/services/customer_relations/organizations/update_service.rb
new file mode 100644
index 00000000000..9d8f908db14
--- /dev/null
+++ b/app/services/customer_relations/organizations/update_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module CustomerRelations
+ module Organizations
+ class UpdateService < BaseService
+ def execute(organization)
+ return error_no_permissions unless allowed?
+ return error_updating(organization) unless organization.update(params)
+
+ ServiceResponse.success(payload: organization)
+ end
+
+ private
+
+ def error_no_permissions
+ error('You have insufficient permissions to update an organization for this group')
+ end
+
+ def error_updating(organization)
+ error(organization&.errors&.full_messages || 'Failed to update organization')
+ end
+ end
+ end
+end
diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml
index eb63c98e13f..fc3b12acc46 100644
--- a/app/views/layouts/_snowplow.html.haml
+++ b/app/views/layouts/_snowplow.html.haml
@@ -11,3 +11,4 @@
gl = window.gl || {};
gl.snowplowStandardContext = #{Gitlab::Tracking::StandardContext.new.to_context.to_json.to_json}
+ gl.snowplowPseudonymizedPageUrl = #{masked_page_url.to_json};
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 4e7899664fd..f398ac6ede7 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,7 +1,4 @@
- page_title _("Value Stream Analytics")
- add_page_specific_style 'page_bundles/cycle_analytics'
-- svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
-- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group), group_path: group_path(@group), group_id: @group&.id } : { milestones_path: project_milestones_path(@project), labels_path: project_labels_path(@project), group_path: @project.parent&.path, group_id: @project.parent&.id }
-- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs, api_paths)
-#js-cycle-analytics{ data: initial_data }
+#js-cycle-analytics{ data: cycle_analytics_initial_data(@project, @group) }
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b5722933bea..6b3140d62a2 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1430,6 +1430,28 @@ Input type: `CustomerRelationsOrganizationCreateInput`
| <a id="mutationcustomerrelationsorganizationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationcustomerrelationsorganizationcreateorganization"></a>`organization` | [`CustomerRelationsOrganization`](#customerrelationsorganization) | Organization after the mutation. |
+### `Mutation.customerRelationsOrganizationUpdate`
+
+Input type: `CustomerRelationsOrganizationUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcustomerrelationsorganizationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcustomerrelationsorganizationupdatedefaultrate"></a>`defaultRate` | [`Float`](#float) | Standard billing rate for the organization. |
+| <a id="mutationcustomerrelationsorganizationupdatedescription"></a>`description` | [`String`](#string) | Description or notes for the organization. |
+| <a id="mutationcustomerrelationsorganizationupdateid"></a>`id` | [`CustomerRelationsOrganizationID!`](#customerrelationsorganizationid) | Global ID of the organization. |
+| <a id="mutationcustomerrelationsorganizationupdatename"></a>`name` | [`String`](#string) | Name of the organization. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationcustomerrelationsorganizationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationcustomerrelationsorganizationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationcustomerrelationsorganizationupdateorganization"></a>`organization` | [`CustomerRelationsOrganization!`](#customerrelationsorganization) | Organization after the mutation. |
+
### `Mutation.dastOnDemandScanCreate`
Input type: `DastOnDemandScanCreateInput`
@@ -16654,6 +16676,12 @@ A `CustomEmojiID` is a global ID. It is encoded as a string.
An example `CustomEmojiID` is: `"gid://gitlab/CustomEmoji/1"`.
+### `CustomerRelationsOrganizationID`
+
+A `CustomerRelationsOrganizationID` is a global ID. It is encoded as a string.
+
+An example `CustomerRelationsOrganizationID` is: `"gid://gitlab/CustomerRelations::Organization/1"`.
+
### `DastProfileID`
A `DastProfileID` is a global ID. It is encoded as a string.
diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md
index 9ce012366ef..7ddddfc5e53 100644
--- a/doc/user/admin_area/analytics/dev_ops_report.md
+++ b/doc/user/admin_area/analytics/dev_ops_report.md
@@ -20,7 +20,7 @@ To see DevOps Report:
## DevOps Score
NOTE:
-To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping).
+To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping). This is because DevOps Score is a comparative tool, so your score data must be centrally processed by GitLab Inc. first.
You can use the DevOps score to compare your DevOps status to other organizations.
diff --git a/doc/user/clusters/agent/ci_cd_tunnel.md b/doc/user/clusters/agent/ci_cd_tunnel.md
index 023affe43d4..1ea5168f30c 100644
--- a/doc/user/clusters/agent/ci_cd_tunnel.md
+++ b/doc/user/clusters/agent/ci_cd_tunnel.md
@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327409) in GitLab 14.1.
> - The pre-configured `KUBECONFIG` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324275) in GitLab 14.2.
+> - The ability to authorize groups was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
The CI/CD Tunnel enables users to access Kubernetes clusters from GitLab CI/CD jobs even if there is no network
connectivity between GitLab Runner and a cluster. GitLab Runner does not have to be running in the same cluster.
@@ -29,10 +30,8 @@ jobs provide a `KUBECONFIG` variable compatible with `kubectl`.
Also, each Agent has a separate context (`kubecontext`). By default,
there isn't any context selected.
-
Contexts are named in the following format: `<agent-configuration-project-path>:<agent-name>`.
-
-You can get the list of available contexts by running `kubectl config get-contexts`.
+To get the list of available contexts, run `kubectl config get-contexts`.
## Example for a `kubectl` command using the CI/CD Tunnel
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index ad9b538ef51..d2dc57c0849 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -106,7 +106,8 @@ To use the KAS:
### Define a configuration repository
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository.
+> - Group authorization was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
To configure an Agent, you need:
@@ -125,6 +126,7 @@ In your repository, add the Agent configuration file under:
Your `config.yaml` file specifies all configurations of the Agent, such as:
- The manifest projects to synchronize.
+- The groups that can access this Agent via the [CI/CD Tunnel](ci_cd_tunnel.md).
- The address of the `hubble-relay` for the Network Security policy integrations.
As an example, a minimal Agent configuration that sets up only the manifest
diff --git a/doc/user/clusters/agent/repository.md b/doc/user/clusters/agent/repository.md
index a3a3e4c29b0..ea57ded3320 100644
--- a/doc/user/clusters/agent/repository.md
+++ b/doc/user/clusters/agent/repository.md
@@ -9,6 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3834) in GitLab 13.11, the Kubernetes Agent became available on GitLab.com.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332227) in GitLab 14.0, the `resource_inclusions` and `resource_exclusions` attributes were removed and `reconcile_timeout`, `dry_run_strategy`, `prune`, `prune_timeout`, `prune_propagation_policy`, and `inventory_policy` attributes were added.
+> - The `ci_access` attribute was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
@@ -147,6 +148,40 @@ gitops:
- glob: '/**/*.yaml'
```
+## Authorize groups to use an Agent
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
+
+If you use the same cluster across multiple projects, you can set up the CI/CD Tunnel
+to grant the Agent access to one or more groups. This way, all the projects that belong
+to the authorized groups can access the same Agent. This enables you to save resources and
+have a scalable setup.
+
+When you authorize a group, the agent's Kubernetes context is automatically injected
+into every project of the authorized group, and users can select the connection as
+described in the [CI/CD Tunnel documentation](ci_cd_tunnel.md).
+To authorize a group to access the Agent through the [CI/CD Tunnel](ci_cd_tunnel.md),
+use the `ci_access` attribute in your `config.yaml` configuration file.
+
+An Agent can only authorize groups in the same group hierarchy as the Agent's configuration project. At most
+100 groups can be authorized per Agent.
+
+To authorize a group:
+
+1. Edit your `.config.yaml` file under the `.gitlab/agents/<agent name>` directory.
+1. Add the `ci_access` attribute.
+1. Add the `groups` attribute into `ci_access`.
+1. Add the group `id` into `groups`, identifying the authorized group through its path.
+
+For example:
+
+```yaml
+ci_access:
+ # This agent is accessible from CI jobs in projects in these groups
+ groups:
+ - id: group/subgroup
+```
+
## Surface network security alerts from cluster to GitLab
The GitLab Agent provides an [integration with Cilium](index.md#kubernetes-network-security-alerts).
diff --git a/lib/gitlab/saas.rb b/lib/gitlab/saas.rb
index 4066c8a7906..e87c2a0b700 100644
--- a/lib/gitlab/saas.rb
+++ b/lib/gitlab/saas.rb
@@ -24,6 +24,18 @@ module Gitlab
def self.registry_prefix
'registry.gitlab.com'
end
+
+ def self.customer_support_url
+ 'https://support.gitlab.com'
+ end
+
+ def self.customer_license_support_url
+ 'https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293'
+ end
+
+ def self.gitlab_com_status_url
+ 'https://status.gitlab.com'
+ end
end
end
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
index ab2e1404cd2..78fa5009bc4 100644
--- a/lib/gitlab/subscription_portal.rb
+++ b/lib/gitlab/subscription_portal.rb
@@ -13,6 +13,38 @@ module Gitlab
def self.payment_form_url
"#{self.subscriptions_url}/payment_forms/cc_validation"
end
+
+ def self.subscriptions_comparison_url
+ 'https://about.gitlab.com/pricing/gitlab-com/feature-comparison'
+ end
+
+ def self.subscriptions_graphql_url
+ "#{self.subscriptions_url}/graphql"
+ end
+
+ def self.subscriptions_more_minutes_url
+ "#{self.subscriptions_url}/buy_pipeline_minutes"
+ end
+
+ def self.subscriptions_more_storage_url
+ "#{self.subscriptions_url}/buy_storage"
+ end
+
+ def self.subscriptions_manage_url
+ "#{self.subscriptions_url}/subscriptions"
+ end
+
+ def self.subscriptions_plans_url
+ "#{self.subscriptions_url}/plans"
+ end
+
+ def self.subscription_portal_admin_email
+ ENV.fetch('SUBSCRIPTION_PORTAL_ADMIN_EMAIL', 'gl_com_api@gitlab.com')
+ end
+
+ def self.subscription_portal_admin_token
+ ENV.fetch('SUBSCRIPTION_PORTAL_ADMIN_TOKEN', 'customer_admin_token')
+ end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index f208a93d302..0566bc237bb 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -237,6 +237,8 @@ module QA
else
browser.manage.delete_cookie("gitlab_canary")
end
+
+ browser.navigate.refresh
end
##
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
new file mode 100644
index 00000000000..2b1c956039f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Manage', :mixed_env, :smoke, only: { subdomain: :staging } do
+ describe 'basic user' do
+ it 'remains logged in when redirected from canary to non-canary node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2251' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+ Support::Retrier.retry_until(sleep_interval: 0.5) do
+ Page::Main::Login.perform(&:can_sign_in?)
+ end
+
+ Runtime::Browser::Session.target_canary(true)
+ Flow::Login.sign_in
+
+ verify_session_on_canary(true)
+
+ Runtime::Browser::Session.target_canary(false)
+
+ verify_session_on_canary(false)
+
+ Support::Retrier.retry_until(sleep_interval: 0.5) do
+ Page::Main::Menu.perform(&:sign_out)
+
+ Page::Main::Login.perform(&:can_sign_in?)
+ end
+ end
+
+ def verify_session_on_canary(enable_canary)
+ Page::Main::Menu.perform do |menu|
+ aggregate_failures 'testing session log in' do
+ expect(menu.canary?).to be(enable_canary)
+ expect(menu).to have_personal_area
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index d2cdaaa43b9..489beb70ab3 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Group issues page' do
# However,`:js` option forces Capybara to use Selenium that doesn't support`:has`
context "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
- expect(find('[data-testid="rss-feed-link"]')['href']).to have_content(user.feed_token)
+ expect(page).to have_link 'Subscribe to RSS feed', href: /feed_token=#{user.feed_token}/
end
end
end
@@ -46,7 +46,7 @@ RSpec.describe 'Group issues page' do
# Note: please see the above
context "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
- expect(find('[data-testid="rss-feed-link"]')['href']).not_to have_content('feed_token')
+ expect(page).not_to have_link 'Subscribe to RSS feed', href: /feed_token/
end
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 88a7b890daa..edf3df7c16e 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -565,21 +565,18 @@ RSpec.describe 'Filter issues', :js do
end
it 'maintains filter' do
- # Closed
- find('.issues-state-filters [data-state="closed"]').click
+ click_link 'Closed'
wait_for_requests
expect(page).to have_selector('.issues-list .issue', count: 1)
expect(page).to have_link(closed_issue.title)
- # Opened
- find('.issues-state-filters [data-state="opened"]').click
+ click_link 'Open'
wait_for_requests
expect(page).to have_selector('.issues-list .issue', count: 4)
- # All
- find('.issues-state-filters [data-state="all"]').click
+ click_link 'All'
wait_for_requests
expect(page).to have_selector('.issues-list .issue', count: 5)
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 644d7cc4611..2d8587d886f 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -16,9 +16,6 @@ RSpec.describe 'Visual tokens', :js do
let(:filtered_search) { find('.filtered-search') }
let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
- let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
- let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
- let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
def is_input_focused
page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index a4b638ddd42..97b5bd03e18 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -14,8 +14,8 @@ import {
} from '../mock_data';
const { id: groupId, path: groupPath } = currentGroup;
-const mockMilestonesPath = 'mock-milestones';
-const mockLabelsPath = 'mock-labels';
+const mockMilestonesPath = 'mock-milestones.json';
+const mockLabelsPath = 'mock-labels.json';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockEndpoints = {
@@ -83,8 +83,8 @@ describe('Project Value Stream Analytics actions', () => {
const payload = { endpoints: mockEndpoints };
const mockFilterEndpoints = {
groupEndpoint: 'foo',
- labelsEndpoint: 'mock-labels.json',
- milestonesEndpoint: 'mock-milestones.json',
+ labelsEndpoint: mockLabelsPath,
+ milestonesEndpoint: mockMilestonesPath,
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
};
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index b59c3cda055..21fed51ff10 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -1,7 +1,9 @@
import { setHTMLFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
+import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({
@@ -15,9 +17,11 @@ describe('Tracking', () => {
let bindDocumentSpy;
let trackLoadEventsSpy;
let enableFormTracking;
+ let setAnonymousUrlsSpy;
beforeAll(() => {
window.gl = window.gl || {};
+ window.gl.snowplowUrls = {};
window.gl.snowplowStandardContext = {
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
@@ -74,6 +78,7 @@ describe('Tracking', () => {
enableFormTracking = jest
.spyOn(Tracking, 'enableFormTracking')
.mockImplementation(() => null);
+ setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null);
});
it('should activate features based on what has been enabled', () => {
@@ -105,6 +110,11 @@ describe('Tracking', () => {
expect(trackLoadEventsSpy).toHaveBeenCalled();
});
+ it('calls the anonymized URLs method', () => {
+ initDefaultTrackers();
+ expect(setAnonymousUrlsSpy).toHaveBeenCalled();
+ });
+
describe('when there are experiment contexts', () => {
const experimentContexts = [
{
@@ -295,6 +305,110 @@ describe('Tracking', () => {
});
});
+ describe('.setAnonymousUrls', () => {
+ afterEach(() => {
+ window.gl.snowplowPseudonymizedPageUrl = '';
+ localStorage.removeItem(URLS_CACHE_STORAGE_KEY);
+ });
+
+ it('does nothing if URLs are not provided', () => {
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalled();
+ expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).toBe(null);
+ });
+
+ it('sets the page URL when provided and populates the cache', () => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', TEST_HOST);
+ expect(JSON.parse(localStorage.getItem(URLS_CACHE_STORAGE_KEY))[0]).toStrictEqual({
+ url: TEST_HOST,
+ referrer: '',
+ originalUrl: window.location.href,
+ timestamp: Date.now(),
+ });
+ });
+
+ it('appends the hash/fragment to the pseudonymized URL', () => {
+ const hash = 'first-heading';
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+ window.location.hash = hash;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', `${TEST_HOST}#${hash}`);
+ });
+
+ it('does not set the referrer URL by default', () => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String));
+ });
+
+ describe('with referrers cache', () => {
+ const testUrl = '/namespace:1/project:2/-/merge_requests/5';
+ const testOriginalUrl = '/my-namespace/my-project/-/merge_requests/';
+ const setUrlsCache = (data) =>
+ localStorage.setItem(URLS_CACHE_STORAGE_KEY, JSON.stringify(data));
+
+ beforeEach(() => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+ Object.defineProperty(document, 'referrer', { value: '', configurable: true });
+ });
+
+ it('does nothing if a referrer can not be found', () => {
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: TEST_HOST,
+ timestamp: Date.now(),
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String));
+ });
+
+ it('sets referrer URL from the page URL found in cache', () => {
+ Object.defineProperty(document, 'referrer', { value: testOriginalUrl });
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: testOriginalUrl,
+ timestamp: Date.now(),
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setReferrerUrl', testUrl);
+ });
+
+ it('ignores and removes old entries from the cache', () => {
+ const oldTimestamp = Date.now() - (REFERRER_TTL + 1);
+ Object.defineProperty(document, 'referrer', { value: testOriginalUrl });
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: testOriginalUrl,
+ timestamp: oldTimestamp,
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', testUrl);
+ expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).not.toContain(oldTimestamp);
+ });
+ });
+ });
+
describe.each`
term
${'event'}
diff --git a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
index 4741a96b397..ab430b9240b 100644
--- a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
@@ -5,8 +5,6 @@ require 'spec_helper'
RSpec.describe Mutations::CustomerRelations::Organizations::Create do
let_it_be(:user) { create(:user) }
- let(:group) { create(:group) }
-
let(:valid_params) do
attributes_for(:organization,
group: group,
@@ -25,6 +23,8 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
end
context 'when the user does not have permission' do
+ let_it_be(:group) { create(:group) }
+
before do
group.add_guest(user)
end
@@ -35,7 +35,9 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
end
context 'when the user has permission' do
- before do
+ let_it_be(:group) { create(:group) }
+
+ before_all do
group.add_reporter(user)
end
diff --git a/spec/graphql/mutations/customer_relations/organizations/update_spec.rb b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
new file mode 100644
index 00000000000..f5aa6c00301
--- /dev/null
+++ b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::CustomerRelations::Organizations::Update do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:name) { 'GitLab' }
+ let_it_be(:default_rate) { 1000.to_f }
+ let_it_be(:description) { 'VIP' }
+
+ let(:organization) { create(:organization, group: group) }
+ let(:attributes) do
+ {
+ id: organization.to_global_id,
+ name: name,
+ default_rate: default_rate,
+ description: description
+ }
+ end
+
+ describe '#resolve' do
+ subject(:resolve_mutation) do
+ described_class.new(object: nil, context: { current_user: user }, field: nil).resolve(
+ attributes
+ )
+ end
+
+ context 'when the user does not have permission to update an organization' do
+ let_it_be(:group) { create(:group) }
+
+ before do
+ group.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when the organization does not exist' do
+ let_it_be(:group) { create(:group) }
+
+ it 'raises an error' do
+ attributes[:id] = 'gid://gitlab/CustomerRelations::Organization/999'
+
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when the user has permission to update an organization' do
+ let_it_be(:group) { create(:group) }
+
+ before_all do
+ group.add_reporter(user)
+ end
+
+ it 'updates the organization with correct values' do
+ expect(resolve_mutation[:organization]).to have_attributes(attributes)
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_organization) }
+end
diff --git a/spec/helpers/analytics/cycle_analytics_helper_spec.rb b/spec/helpers/analytics/cycle_analytics_helper_spec.rb
new file mode 100644
index 00000000000..d906646e25c
--- /dev/null
+++ b/spec/helpers/analytics/cycle_analytics_helper_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Analytics::CycleAnalyticsHelper do
+ describe '#cycle_analytics_initial_data' do
+ let(:user) { create(:user, name: 'fake user', username: 'fake_user') }
+ let(:image_path_keys) { [:empty_state_svg_path, :no_data_svg_path, :no_access_svg_path] }
+ let(:api_path_keys) { [:milestones_path, :labels_path] }
+ let(:additional_data_keys) { [:full_path, :group_id, :group_path, :project_id, :request_path] }
+ let(:group) { create(:group) }
+
+ subject(:cycle_analytics_data) { helper.cycle_analytics_initial_data(project, group) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when a group is present' do
+ let(:project) { create(:project, group: group) }
+
+ it "sets the correct data keys" do
+ expect(cycle_analytics_data.keys)
+ .to match_array(api_path_keys + image_path_keys + additional_data_keys)
+ end
+
+ it "sets group paths" do
+ expect(cycle_analytics_data)
+ .to include({
+ full_path: project.full_path,
+ group_path: "/#{project.namespace.name}",
+ group_id: project.namespace.id,
+ request_path: "/#{project.full_path}/-/value_stream_analytics",
+ milestones_path: "/groups/#{group.name}/-/milestones.json",
+ labels_path: "/groups/#{group.name}/-/labels.json"
+ })
+ end
+ end
+
+ context 'when a group is not present' do
+ let(:group) { nil }
+ let(:project) { create(:project) }
+
+ it "sets the correct data keys" do
+ expect(cycle_analytics_data.keys)
+ .to match_array(image_path_keys + api_path_keys + additional_data_keys)
+ end
+
+ it "sets project name space paths" do
+ expect(cycle_analytics_data)
+ .to include({
+ full_path: project.full_path,
+ group_path: project.namespace.path,
+ group_id: project.namespace.id,
+ request_path: "/#{project.full_path}/-/value_stream_analytics",
+ milestones_path: "/#{project.full_path}/-/milestones.json",
+ labels_path: "/#{project.full_path}/-/labels.json"
+ })
+ end
+ end
+ end
+end
diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb
index f47e8185f07..10563502555 100644
--- a/spec/helpers/routing/pseudonymization_helper_spec.rb
+++ b/spec/helpers/routing/pseudonymization_helper_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end
context 'with controller for groups with subgroups and project' do
- let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{project.id}/"}
+ let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{project.id}"}
before do
allow(helper).to receive(:group).and_return(subgroup)
@@ -73,7 +73,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end
context 'with controller for groups and subgroups' do
- let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/"}
+ let(:masked_url) { "http://test.host/namespace:#{subgroup.id}"}
before do
allow(helper).to receive(:group).and_return(subgroup)
@@ -102,10 +102,25 @@ RSpec.describe ::Routing::PseudonymizationHelper do
it_behaves_like 'masked url'
end
+
+ context 'with non identifiable controller' do
+ let(:masked_url) { "http://test.host/dashboard/issues?assignee_username=root" }
+
+ before do
+ controller.request.path = '/dashboard/issues'
+ controller.request.query_string = 'assignee_username=root'
+ allow(Rails.application.routes).to receive(:recognize_path).and_return({
+ controller: 'dashboard',
+ action: 'issues'
+ })
+ end
+
+ it_behaves_like 'masked url'
+ end
end
describe 'when url has no params to mask' do
- let(:root_url) { 'http://test.host/' }
+ let(:root_url) { 'http://test.host' }
context 'returns root url' do
it 'masked_page_url' do
diff --git a/spec/services/customer_relations/organizations/update_service_spec.rb b/spec/services/customer_relations/organizations/update_service_spec.rb
new file mode 100644
index 00000000000..eb253540863
--- /dev/null
+++ b/spec/services/customer_relations/organizations/update_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CustomerRelations::Organizations::UpdateService do
+ let_it_be(:user) { create(:user) }
+
+ let(:organization) { create(:organization, name: 'Test', group: group) }
+
+ subject(:update) { described_class.new(group: group, current_user: user, params: params).execute(organization) }
+
+ describe '#execute' do
+ context 'when the user has no permission' do
+ let_it_be(:group) { create(:group) }
+
+ let(:params) { { name: 'GitLab' } }
+
+ it 'returns an error' do
+ response = update
+
+ expect(response).to be_error
+ expect(response.message).to eq('You have insufficient permissions to update an organization for this group')
+ end
+ end
+
+ context 'when user has permission' do
+ let_it_be(:group) { create(:group) }
+
+ before_all do
+ group.add_reporter(user)
+ end
+
+ context 'when name is changed' do
+ let(:params) { { name: 'GitLab' } }
+
+ it 'updates the organization' do
+ response = update
+
+ expect(response).to be_success
+ expect(response.payload.name).to eq('GitLab')
+ end
+ end
+
+ context 'when the organization is invalid' do
+ let(:params) { { name: nil } }
+
+ it 'returns an error' do
+ response = update
+
+ expect(response).to be_error
+ expect(response.message).to eq(["Name can't be blank"])
+ end
+ end
+ end
+ end
+end