diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js index 81ca75579c0..f513d3b9b30 100644 --- a/app/assets/javascripts/observability/client.js +++ b/app/assets/javascripts/observability/client.js @@ -613,7 +613,7 @@ export async function fetchLogs(logsSearchUrl, { pageToken, pageSize, filters = } } -export async function fetchLogsSearchMetadata(_logsSearchMetadataUrl, { filters = {} }) { +export async function fetchLogsSearchMetadata(logsSearchMetadataUrl, { filters = {} } = {}) { try { const params = new URLSearchParams(); @@ -626,354 +626,11 @@ export async function fetchLogsSearchMetadata(_logsSearchMetadataUrl, { filters addLogsAttributesFiltersToQueryParams(attributes, params); } - // TODO remove mocks (and add UTs) when API is ready https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2782 - // const { data } = await axios.get(logsSearchMetadataUrl, { - // withCredentials: true, - // params, - // }); - // return data; - - return { - start_ts: 1713513680617331200, - end_ts: 1714723280617331200, - summary: { - service_names: ['adservice', 'cartservice', 'quoteservice', 'recommendationservice'], - trace_flags: [0, 1], - severity_names: ['info', 'warn'], - severity_numbers: [9, 13], - }, - severity_numbers_counts: [ - { - time: 1713519360000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713545280000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713571200000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713597120000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713623040000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713648960000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713674880000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713700800000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713726720000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713752640000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713778560000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713804480000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713830400000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713856320000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713882240000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713908160000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713934080000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713960000000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1713985920000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714011840000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714037760000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714063680000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714089600000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714115520000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714141440000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714167360000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714193280000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714219200000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714245120000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714271040000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714296960000000000, - counts: { - 13: 0, - 9: 0, - }, - }, - { - time: 1714322880000000000, - counts: { - 13: 1, - 9: 26202, - }, - }, - { - time: 1714348800000000000, - counts: { - 13: 0, - 9: 53103, - }, - }, - { - time: 1714374720000000000, - counts: { - 13: 0, - 9: 52854, - }, - }, - { - time: 1714400640000000000, - counts: { - 13: 0, - 9: 49598, - }, - }, - { - time: 1714426560000000000, - counts: { - 13: 0, - 9: 45266, - }, - }, - { - time: 1714452480000000000, - counts: { - 13: 0, - 9: 44951, - }, - }, - { - time: 1714478400000000000, - counts: { - 13: 0, - 9: 45096, - }, - }, - { - time: 1714504320000000000, - counts: { - 13: 0, - 9: 45301, - }, - }, - { - time: 1714530240000000000, - counts: { - 13: 0, - 9: 44894, - }, - }, - { - time: 1714556160000000000, - counts: { - 13: 0, - 9: 45444, - }, - }, - { - time: 1714582080000000000, - counts: { - 13: 0, - 9: 45067, - }, - }, - { - time: 1714608000000000000, - counts: { - 13: 0, - 9: 45119, - }, - }, - { - time: 1714633920000000000, - counts: { - 13: 0, - 9: 45817, - }, - }, - { - time: 1714659840000000000, - counts: { - 13: 0, - 9: 44574, - }, - }, - { - time: 1714685760000000000, - counts: { - 13: 0, - 9: 44652, - }, - }, - { - time: 1714711680000000000, - counts: { - 13: 0, - 9: 20470, - }, - }, - ], - }; + const { data } = await axios.get(logsSearchMetadataUrl, { + withCredentials: true, + params, + }); + return data; } catch (e) { return reportErrorAndThrow(e); } diff --git a/app/graphql/resolvers/projects/user_contributed_projects_resolver.rb b/app/graphql/resolvers/projects/user_contributed_projects_resolver.rb new file mode 100644 index 00000000000..218b77d760f --- /dev/null +++ b/app/graphql/resolvers/projects/user_contributed_projects_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class UserContributedProjectsResolver < BaseResolver + type Types::ProjectType.connection_type, null: true + + argument :sort, Types::Projects::ProjectSortEnum, + description: 'Sort contributed projects.', + required: false, + default_value: :latest_activity_desc + + alias_method :user, :object + + def resolve(**args) + ContributedProjectsFinder.new(user).execute(current_user, order_by: args[:sort]).joined(user) + end + end + end +end diff --git a/app/graphql/types/projects/project_sort_enum.rb b/app/graphql/types/projects/project_sort_enum.rb new file mode 100644 index 00000000000..d4a391aff7c --- /dev/null +++ b/app/graphql/types/projects/project_sort_enum.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Projects + class ProjectSortEnum < SortEnum + graphql_name 'ProjectSort' + description 'Values for sorting projects' + + value 'ID_ASC', 'ID by ascending order.', value: :id_asc + value 'ID_DESC', 'ID by descending order.', value: :id_desc + value 'LATEST_ACTIVITY_ASC', 'Latest activity by ascending order.', value: :latest_activity_asc + value 'LATEST_ACTIVITY_DESC', 'Latest activity by descending order.', value: :latest_activity_desc + value 'NAME_ASC', 'Name by ascending order.', value: :name_asc + value 'NAME_DESC', 'Name by descending order.', value: :name_desc + value 'PATH_ASC', 'Path by ascending order.', value: :path_asc + value 'PATH_DESC', 'Path by descending order.', value: :path_desc + value 'STARS_ASC', 'Stars by ascending order.', value: :stars_asc + value 'STARS_DESC', 'Stars by descending order.', value: :stars_desc + end + end +end diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index 45c24f5ef30..fdf04b5db0e 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -101,6 +101,9 @@ module Types field :starred_projects, description: 'Projects starred by the user.', resolver: Resolvers::UserStarredProjectsResolver + field :contributed_projects, + description: 'Projects the user has contributed to.', + resolver: Resolvers::Projects::UserContributedProjectsResolver field :namespace, type: Types::NamespaceType, null: true, diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 403efcd0b01..c8f7bf69c6d 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -73,11 +73,7 @@ module AppearancesHelper end def custom_sign_in_description - [ - markdown_field(current_appearance, :description), - markdown(Gitlab::CurrentSettings.sign_in_text), - markdown(Gitlab::CurrentSettings.help_text) - ].compact_blank.join("
").html_safe + markdown_field(current_appearance, :description) end def brand_member_guidelines diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 51f912b5e4e..f9515bed57c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -25,6 +25,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord container_registry_import_target_plan container_registry_import_created_before ], remove_with: '17.2', remove_after: '2024-06-24' + ignore_column %i[sign_in_text help_text], remove_with: '17.3', remove_after: '2024-08-15' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f5c521c2cc6..8cd1fd7c624 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15785,6 +15785,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `AddOnUser.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `AddOnUser.groups` Groups where the user has access. @@ -16549,6 +16565,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `AutocompletedUser.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `AutocompletedUser.groups` Groups where the user has access. @@ -18725,6 +18757,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `CurrentUser.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `CurrentUser.groups` Groups where the user has access. @@ -24001,6 +24049,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `MergeRequestAssignee.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `MergeRequestAssignee.groups` Groups where the user has access. @@ -24313,6 +24377,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `MergeRequestAuthor.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `MergeRequestAuthor.groups` Groups where the user has access. @@ -24672,6 +24752,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `MergeRequestParticipant.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `MergeRequestParticipant.groups` Groups where the user has access. @@ -25020,6 +25116,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `MergeRequestReviewer.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `MergeRequestReviewer.groups` Groups where the user has access. @@ -27431,6 +27543,7 @@ four standard [pagination arguments](#pagination-arguments): | `componentNames` | [`[String!]`](#string) | Filter dependencies by component names. | | `packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. | | `sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. | +| `sourceTypes` | [`[SbomSourceType!]`](#sbomsourcetype) | Filter dependencies by source type. | ##### `Project.deployment` @@ -30727,6 +30840,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +##### `UserCore.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ##### `UserCore.groups` Groups where the user has access. @@ -34477,6 +34606,31 @@ Project member relation. | `INVITED_GROUPS` | Invited Groups members. | | `SHARED_INTO_ANCESTORS` | Shared Into Ancestors members. | +### `ProjectSort` + +Values for sorting projects. + +| Value | Description | +| ----- | ----------- | +| `CREATED_ASC` | Created at ascending order. | +| `CREATED_DESC` | Created at descending order. | +| `ID_ASC` | ID by ascending order. | +| `ID_DESC` | ID by descending order. | +| `LATEST_ACTIVITY_ASC` | Latest activity by ascending order. | +| `LATEST_ACTIVITY_DESC` | Latest activity by descending order. | +| `NAME_ASC` | Name by ascending order. | +| `NAME_DESC` | Name by descending order. | +| `PATH_ASC` | Path by ascending order. | +| `PATH_DESC` | Path by descending order. | +| `STARS_ASC` | Stars by ascending order. | +| `STARS_DESC` | Stars by descending order. | +| `UPDATED_ASC` | Updated at ascending order. | +| `UPDATED_DESC` | Updated at descending order. | +| `created_asc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `CREATED_ASC`. | +| `created_desc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `CREATED_DESC`. | +| `updated_asc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `UPDATED_ASC`. | +| `updated_desc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `UPDATED_DESC`. | + ### `RefType` Type of ref. @@ -34584,6 +34738,17 @@ Size of UI component in SAST configuration page. | `MEDIUM` | Size of UI component in SAST configuration page is medium. | | `SMALL` | Size of UI component in SAST configuration page is small. | +### `SbomSourceType` + +Values for sbom source types. + +| Value | Description | +| ----- | ----------- | +| `CONTAINER_SCANNING` | Source Type: container_scanning. | +| `CONTAINER_SCANNING_FOR_REGISTRY` | Source Type: container_scanning_for_registry. | +| `DEPENDENCY_SCANNING` | Source Type: dependency_scanning. | +| `NIL_SOURCE` | Enum source nil. | + ### `ScanStatus` The status of the security scan. @@ -36983,6 +37148,22 @@ four standard [pagination arguments](#pagination-arguments): | `updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. | | `updatedBefore` | [`Time`](#time) | Merge requests updated before this timestamp. | +###### `User.contributedProjects` + +Projects the user has contributed to. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +####### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. | + ###### `User.groups` Groups where the user has access. diff --git a/doc/ci/cloud_services/index.md b/doc/ci/cloud_services/index.md index eb02b3d1978..10431d5fb72 100644 --- a/doc/ci/cloud_services/index.md +++ b/doc/ci/cloud_services/index.md @@ -57,7 +57,11 @@ Each job can be configured with ID tokens, which are provided as a CI/CD variabl ### Authorization workflow ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram +accTitle: Authorization workflow +accDescr: The flow of authorization requests between GitLab and a cloud provider. + participant GitLab Note right of Cloud: Create OIDC identity provider Note right of Cloud: Create role with conditionals diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index 0f7d411d6e5..741b2e17ef9 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -42,19 +42,26 @@ It's not the most efficient, and if you have lots of steps it can grow quite com easier to maintain: ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% graph LR +accTitle: Basic pipelines +accDescr: Shows a pipeline that runs sequentially through the build, test, and deploy stages. + subgraph deploy stage deploy --> deploy_a deploy --> deploy_b end + subgraph test stage test --> test_a test --> test_b end + subgraph build stage build --> build_a build --> build_b end + build_a -.-> test build_b -.-> test test_a -.-> deploy @@ -121,7 +128,11 @@ In the example below, if `build_a` and `test_a` are much faster than `build_b` a `test_b`, GitLab starts `deploy_a` even if `build_b` is still running. ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% graph LR +accTitle: Pipeline using DAG +accDescr: Shows how two jobs can start without waiting for earlier stages to complete + subgraph Pipeline using DAG build_a --> test_a --> deploy_a build_b --> test_b --> deploy_b @@ -210,7 +221,11 @@ You can combine parent-child pipelines with: - [DAG pipelines](#directed-acyclic-graph-pipelines) inside of child pipelines, achieving the benefits of both. ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% graph LR +accTitle: Parent and child pipelines +accDescr: Shows that a parent pipeline can trigger independent child pipelines + subgraph Parent pipeline trigger_a -.-> build_a trigger_b -.-> build_b diff --git a/doc/ci/runners/long_polling.md b/doc/ci/runners/long_polling.md index 28be6512e02..b9c370f75b5 100644 --- a/doc/ci/runners/long_polling.md +++ b/doc/ci/runners/long_polling.md @@ -120,7 +120,11 @@ You can see an [example of how one user discovered an issue with long polling wi The diagram shows how a single runner gets a job with long polling enabled: ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram +accTitle: Long polling workflow +accDescr: The flow of a single runner getting a job with long polling enabled + autonumber participant C as Runner participant W as Workhorse diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md index 783cceffe8b..620b8235364 100644 --- a/doc/integration/mattermost/index.md +++ b/doc/integration/mattermost/index.md @@ -394,7 +394,11 @@ provider for Mattermost. You can use this to troubleshoot errors in getting the integration to work: ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram +accTitle: GitLab as OAuth 2.0 provider +accDescr: Sequence of actions that happen when a user authenticates to GitLab through Mattermost. + User->>Mattermost: GET https://mm.domain.com Note over Mattermost, GitLab: Obtain access code Mattermost->>GitLab: GET https://gitlab.domain.com/oauth/authorize diff --git a/doc/integration/partner_marketplace.md b/doc/integration/partner_marketplace.md index cbcb8f70164..582c80204f2 100644 --- a/doc/integration/partner_marketplace.md +++ b/doc/integration/partner_marketplace.md @@ -28,7 +28,11 @@ The following example shows a typical purchase flow of request and response betw - Salesforce ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram +accTitle: Purchase flow +accDescr: Shows the flow of a purchase from the customer, through the customer portal, Zuora, and Salesforce. + participant Customer participant Marketplace partner system participant Customers Portal diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 840ea93aaf7..fe32300d1fd 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -181,7 +181,7 @@ The following languages and dependency managers are supported when using the Dep Python 3.117 - setuptools + setuptools8 setup.py N @@ -279,6 +279,12 @@ The following languages and dependency managers are supported when using the Dep Support for prior Python versions was deprecated in GitLab 16.9 and removed in GitLab 17.0.

+
  • + +

    + Excludes both pip and setuptools from the report as they are required by the installer. +

    +
  • diff --git a/doc/user/application_security/gitlab_advisory_database/index.md b/doc/user/application_security/gitlab_advisory_database/index.md index 7168044cf45..59292b53afa 100644 --- a/doc/user/application_security/gitlab_advisory_database/index.md +++ b/doc/user/application_security/gitlab_advisory_database/index.md @@ -64,7 +64,11 @@ GitLab Advisory Database Terms prohibit the use of data contained in the GitLab As an example, we highlight the use of the database as a source for an Advisory Ingestion process as part of Continuous Vulnerability Scans. ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% flowchart TB +accTitle: Advisory ingestion process +accDescr: Sequence of actions that make up the advisory ingestion process. + subgraph Dependency Scanning A[GitLab Advisory Database] end diff --git a/doc/user/application_security/secret_detection/automatic_response.md b/doc/user/application_security/secret_detection/automatic_response.md index 728fba696d0..0eefe95061f 100644 --- a/doc/user/application_security/secret_detection/automatic_response.md +++ b/doc/user/application_security/secret_detection/automatic_response.md @@ -47,8 +47,10 @@ This diagram describes how a post-processing hook revokes a secret in the GitLab ```mermaid %%{init: { "fontFamily": "GitLab Sans" }}%% - sequenceDiagram +accTitle: Architecture diagram +accDescr: How a post-processing hook revokes a secret in the GitLab application. + autonumber GitLab Rails-->+GitLab Rails: gl-secret-detection-report.json GitLab Rails->>+GitLab Sidekiq: StoreScansService @@ -86,7 +88,11 @@ body. We strongly recommend that you verify incoming requests using this signatu request from GitLab. The diagram below details the necessary steps to receive, verify, and revoke leaked tokens: ```mermaid +%%{init: { "fontFamily": "GitLab Sans" }}%% sequenceDiagram +accTitle: Partner API data flow +accDescr: How a Partner API should receive and respond to leaked token revocation requests. + autonumber GitLab Token Revocation API-->>+Partner API: Send new leaked credentials Partner API-->>+GitLab Public Keys endpoint: Get active public keys diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md index 6a157ede035..5e94677f207 100644 --- a/doc/user/project/import/bitbucket_server.md +++ b/doc/user/project/import/bitbucket_server.md @@ -87,16 +87,19 @@ The following items are changed when they are imported: > - Importing approvals by email address or username [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23586) in GitLab 16.7. > - Matching user mentions with GitLab users [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/433008) in GitLab 16.8. +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153041) to import approvals only by email address in GitLab 17.1. FLAG: On self-managed GitLab, matching user mentions with GitLab users is not available. To make it available per user, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `bitbucket_server_import_stage_import_users`. On GitLab.com and GitLab Dedicated, this feature is not available. -When issues and pull requests are importing, the importer tries to find the author's email address -with a confirmed email address in the GitLab user database. If no such user is available, the -project creator is set as the author. The importer appends a note in the comment to mark the -original creator. +When issues and pull requests are importing, the importer tries to match a Bitbucket Server user's email address +with a confirmed email address in the GitLab user database. If no such user is found: + +- The project creator is used instead. The importer appends a note in the comment to mark the original creator. +- For pull request reviewers, no reviewer is assigned. +- For pull request approvers, no approval is added. `@mentions` on pull request descriptions and notes are matched to user profiles on a Bitbucket Server by using the user's email address. If a user with the same email address is not found on GitLab, the `@mention` is made static. @@ -107,11 +110,6 @@ If the project is public, GitLab only matches users who are invited to the proje The importer creates any new namespaces (groups) if they don't exist. If the namespace is taken, the repository imports under the namespace of the user who started the import process. -The importer attempts to find: - -- Reviewers by their email address in the GitLab user database. If they don't exist in GitLab, they are not added as reviewers to a merge request. -- Approvers by username or email. If they don't exist in GitLab, the approval is not added to a merge request. - ### User assignment by username > - Not recommended for production use. @@ -121,15 +119,13 @@ On self-managed GitLab and GitLab.com, by default this feature is not available. available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `bitbucket_server_user_mapping_by_username`. This feature is not ready for production use. -With this feature enabled, the importer tries to find a user in the GitLab user database with the -author's: +With this feature enabled, user email address matching is disabled. +Instead, the importer matches users in the GitLab user database with the Bitbucket Server user's: - `username` - `slug` - `displayName` -If no user matches these properties, the project creator is set as the author. - ## Troubleshooting ### General diff --git a/doc/user/project/members/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png deleted file mode 100644 index e693f9a9ac2..00000000000 Binary files a/doc/user/project/members/img/request_access_button.png and /dev/null differ diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index d197052d4c4..91b9868741a 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -329,9 +329,7 @@ To sort members: GitLab users can request to become a member of a project. 1. On the left sidebar, select **Search or go to** and find the project you want to be a member of. -1. By the project's name, select **Request Access**. - -![Request access button](img/request_access_button.png) +1. In the top right, select the vertical ellipsis (**{ellipsis_v}**) and select **Request Access**. An email is sent to the most recently active project Maintainers or Owners. Up to ten project Maintainers or Owners are notified. diff --git a/lib/gitlab/bitbucket_server_import/importers/pull_request_importer.rb b/lib/gitlab/bitbucket_server_import/importers/pull_request_importer.rb index 6d743d69c27..3aa1d88347a 100644 --- a/lib/gitlab/bitbucket_server_import/importers/pull_request_importer.rb +++ b/lib/gitlab/bitbucket_server_import/importers/pull_request_importer.rb @@ -73,7 +73,7 @@ module Gitlab return [] unless object[:reviewers].present? object[:reviewers].filter_map do |reviewer| - if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops) + if Feature.enabled?(:bitbucket_server_user_mapping_by_username, project, type: :ops) user_finder.find_user_id(by: :username, value: reviewer.dig('user', 'slug')) else user_finder.find_user_id(by: :email, value: reviewer.dig('user', 'emailAddress')) diff --git a/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event.rb b/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event.rb index b612394b485..632377229cb 100644 --- a/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event.rb +++ b/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event.rb @@ -15,8 +15,11 @@ module Gitlab event_id: approved_event[:id] ) - user_id = user_finder.find_user_id(by: :username, value: approved_event[:approver_username]) || - user_finder.find_user_id(by: :email, value: approved_event[:approver_email]) + user_id = if Feature.enabled?(:bitbucket_server_user_mapping_by_username, project, type: :ops) + user_finder.find_user_id(by: :username, value: approved_event[:approver_username]) + else + user_finder.find_user_id(by: :email, value: approved_event[:approver_email]) + end if user_id.nil? log_info( diff --git a/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer.rb b/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer.rb index 725c5bd4ac0..283c405eff2 100644 --- a/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer.rb +++ b/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer.rb @@ -76,8 +76,11 @@ module Gitlab event_id: approved_event.id ) - user_id = user_finder.find_user_id(by: :username, value: approved_event.approver_username) || - user_finder.find_user_id(by: :email, value: approved_event.approver_email) + user_id = if Feature.enabled?(:bitbucket_server_user_mapping_by_username, project, type: :ops) + user_finder.find_user_id(by: :username, value: approved_event.approver_username) + else + user_finder.find_user_id(by: :email, value: approved_event.approver_email) + end return unless user_id diff --git a/lib/gitlab/bitbucket_server_import/user_finder.rb b/lib/gitlab/bitbucket_server_import/user_finder.rb index 68bd2d4851a..fec0af16013 100644 --- a/lib/gitlab/bitbucket_server_import/user_finder.rb +++ b/lib/gitlab/bitbucket_server_import/user_finder.rb @@ -24,7 +24,7 @@ module Gitlab def uid(object) # We want this to only match either username or email depending on the flag state. # There should be no fall-through. - if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops) + if Feature.enabled?(:bitbucket_server_user_mapping_by_username, project, type: :ops) find_user_id(by: :username, value: object.is_a?(Hash) ? object[:author_username] : object.author_username) else find_user_id(by: :email, value: object.is_a?(Hash) ? object[:author_email] : object.author_email) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e090209aca1..6b82e387770 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39307,6 +39307,9 @@ msgstr "" msgid "ProductAnalytics|Back to dashboards" msgstr "" +msgid "ProductAnalytics|By providing feedback on AI-generated content, you acknowledge that GitLab may review the prompts you submitted alongside this feedback." +msgstr "" + msgid "ProductAnalytics|Collector host" msgstr "" @@ -39361,6 +39364,9 @@ msgstr "" msgid "ProductAnalytics|Events over time" msgstr "" +msgid "ProductAnalytics|Feedback acknowledgement" +msgstr "" + msgid "ProductAnalytics|For more information, see the %{linkStart}docs%{linkEnd}." msgstr "" @@ -39385,12 +39391,18 @@ msgstr "" msgid "ProductAnalytics|Help us improve Product Analytics Dashboards by sharing your experience." msgstr "" +msgid "ProductAnalytics|Helpful" +msgstr "" + msgid "ProductAnalytics|How many sessions a user has" msgstr "" msgid "ProductAnalytics|How often users returned compared to all sessions" msgstr "" +msgid "ProductAnalytics|How was the result?" +msgstr "" + msgid "ProductAnalytics|I agree to event collection and processing in this region." msgstr "" @@ -39505,6 +39517,9 @@ msgstr "" msgid "ProductAnalytics|Tell us what you think!" msgstr "" +msgid "ProductAnalytics|Thank you for your feedback." +msgstr "" + msgid "ProductAnalytics|The Product Analytics Beta on GitLab.com is offered only in the Google Cloud Platform zone %{zone}." msgstr "" @@ -39553,6 +39568,9 @@ msgstr "" msgid "ProductAnalytics|Uncheck if you would like to configure a different provider for this project." msgstr "" +msgid "ProductAnalytics|Unhelpful" +msgstr "" + msgid "ProductAnalytics|Unique Users" msgstr "" @@ -39595,6 +39613,9 @@ msgstr "" msgid "ProductAnalytics|What metric do you want to visualize?" msgstr "" +msgid "ProductAnalytics|Wrong" +msgstr "" + msgid "ProductAnalytics|You can instrument your application using a JS module or an HTML script. Follow the instructions below for the option you prefer." msgstr "" diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js index bb2479b0c08..dd658ad96be 100644 --- a/spec/frontend/observability/client_spec.js +++ b/spec/frontend/observability/client_spec.js @@ -1267,4 +1267,165 @@ describe('buildClient', () => { }); }); }); + + describe('fetchLogsSearchMetadata', () => { + const mockResponse = { + start_ts: 1713513680617331200, + end_ts: 1714723280617331200, + summary: { + service_names: ['adservice', 'cartservice', 'quoteservice', 'recommendationservice'], + trace_flags: [0, 1], + severity_names: ['info', 'warn'], + severity_numbers: [9, 13], + }, + severity_numbers_counts: [ + { + time: 1713519360000000000, + counts: { + 13: 0, + 9: 0, + }, + }, + { + time: 1713545280000000000, + counts: { + 13: 0, + 9: 0, + }, + }, + ], + }; + + beforeEach(() => { + axiosMock.onGet(logsSearchMetadataUrl).reply(200, mockResponse); + }); + + it('fetches logs metadata from the logs URL', async () => { + const result = await client.fetchLogsSearchMetadata(); + + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith(logsSearchMetadataUrl, { + withCredentials: true, + params: expect.any(URLSearchParams), + }); + expect(result).toEqual(mockResponse); + }); + + describe('filters', () => { + describe('date range filter', () => { + it('handle predefined date range value', async () => { + await client.fetchLogsSearchMetadata({ + filters: { dateRange: { value: '5m' } }, + }); + expect(getQueryParam()).toContain(`period=5m`); + }); + + it('handle custom date range value', async () => { + await client.fetchLogsSearchMetadata({ + filters: { + dateRange: { + endDate: new Date('2020-07-06'), + startDate: new Date('2020-07-05'), + value: 'custom', + }, + }, + }); + expect(getQueryParam()).toContain( + 'start_time=2020-07-05T00:00:00.000Z&end_time=2020-07-06T00:00:00.000Z', + ); + }); + + it('handles exact timestamps', async () => { + await client.fetchLogsSearchMetadata({ + filters: { + dateRange: { + timestamp: '2024-02-19T16:10:15.4433398Z', + endDate: new Date('2024-02-19'), + startDate: new Date('2024-02-19'), + value: 'custom', + }, + }, + }); + expect(getQueryParam()).toContain( + 'start_time=2024-02-19T16:10:15.4433398Z&end_time=2024-02-19T16:10:15.4433398Z', + ); + }); + }); + + describe('attributes filters', () => { + it('converts filter to proper query params', async () => { + await client.fetchLogsSearchMetadata({ + filters: { + attributes: { + service: [ + { operator: '=', value: 'serviceName' }, + { operator: '!=', value: 'serviceName2' }, + ], + severityName: [ + { operator: '=', value: 'info' }, + { operator: '!=', value: 'warning' }, + ], + severityNumber: [ + { operator: '=', value: '9' }, + { operator: '!=', value: '10' }, + ], + traceId: [{ operator: '=', value: 'traceId' }], + spanId: [{ operator: '=', value: 'spanId' }], + fingerprint: [{ operator: '=', value: 'fingerprint' }], + traceFlags: [ + { operator: '=', value: '1' }, + { operator: '!=', value: '2' }, + ], + attribute: [{ operator: '=', value: 'attr=bar' }], + resourceAttribute: [{ operator: '=', value: 'res=foo' }], + search: [{ value: 'some-search' }], + }, + }, + }); + expect(getQueryParam()).toEqual( + `service_name=serviceName¬[service_name]=serviceName2` + + `&severity_name=info¬[severity_name]=warning` + + `&severity_number=9¬[severity_number]=10` + + `&trace_id=traceId` + + `&span_id=spanId` + + `&fingerprint=fingerprint` + + `&trace_flags=1¬[trace_flags]=2` + + `&log_attr_name=attr&log_attr_value=bar` + + `&res_attr_name=res&res_attr_value=foo` + + `&body=some-search`, + ); + }); + + it('ignores unsupported operators', async () => { + await client.fetchLogsSearchMetadata({ + filters: { + attributes: { + traceId: [{ operator: '!=', value: 'traceId2' }], + spanId: [{ operator: '!=', value: 'spanId2' }], + fingerprint: [{ operator: '!=', value: 'fingerprint2' }], + attribute: [{ operator: '!=', value: 'bar' }], + resourceAttribute: [{ operator: '!=', value: 'resourceAttribute2' }], + unsupported: [{ value: 'something', operator: '=' }], + }, + }, + }); + expect(getQueryParam()).toEqual(''); + }); + }); + + it('ignores empty filter', async () => { + await client.fetchLogsSearchMetadata({ + filters: { attributes: {}, dateRange: {} }, + }); + expect(getQueryParam()).toBe(''); + }); + + it('ignores undefined filter', async () => { + await client.fetchLogsSearchMetadata({ + filters: { dateRange: undefined, attributes: undefined }, + }); + expect(getQueryParam()).toBe(''); + }); + }); + }); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js index f19ebae279e..221a992e9f5 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js @@ -4,7 +4,7 @@ import { GlEmptyState } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue'; import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue'; import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; @@ -92,7 +92,6 @@ describe('Tags List', () => { beforeEach(() => { resolver = jest.fn().mockResolvedValue(imageTagsMock()); - jest.spyOn(Tracking, 'event'); }); describe('registry list', () => { @@ -153,6 +152,16 @@ describe('Tags List', () => { }); describe('delete event', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + describe('single item', () => { beforeEach(() => { findRegistryList().vm.$emit('delete', [tags[0]]); @@ -167,7 +176,7 @@ describe('Tags List', () => { }); it('tracks a single delete event', () => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { label: 'registry_tag_delete', }); }); @@ -187,7 +196,7 @@ describe('Tags List', () => { }); it('tracks multiple delete event', () => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { label: 'bulk_registry_tag_delete', }); }); @@ -266,8 +275,10 @@ describe('Tags List', () => { describe('delete event', () => { let mutationResolver; + let trackingSpy; beforeEach(async () => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock); resolver = jest.fn().mockResolvedValue(imageTagsMock()); await mountComponent({ mutationResolver }); @@ -275,12 +286,16 @@ describe('Tags List', () => { findTagsListRow().at(0).vm.$emit('delete'); }); + afterEach(() => { + unmockTracking(); + }); + it('opens the modal', () => { expect(DeleteModal.methods.show).toHaveBeenCalled(); }); it('tracks a single delete event', () => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { label: 'registry_tag_delete', }); }); @@ -361,12 +376,22 @@ describe('Tags List', () => { }); describe('cancel event', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + it('tracks cancel_delete', async () => { await mountComponent(); findDeleteModal().vm.$emit('cancel'); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'cancel_delete', { label: 'registry_tag_delete', }); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js index 9dbdf57b587..00aa81bda66 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js @@ -24,7 +24,7 @@ import { import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { graphQLImageDetailsMock, @@ -100,10 +100,6 @@ describe('Details Page', () => { }); }; - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - describe('when isLoading is true', () => { it('shows the loader', () => { mountComponent(); @@ -173,6 +169,16 @@ describe('Details Page', () => { }); describe('cancel event', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + it('tracks cancel_delete', async () => { mountComponent(); @@ -180,7 +186,7 @@ describe('Details Page', () => { findDeleteModal().vm.$emit('cancel'); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'cancel_delete', { label: 'registry_image_delete', }); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js index c217227c398..b58e6679101 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js @@ -22,7 +22,7 @@ import { import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; import MetadataDatabaseAlert from '~/packages_and_registries/shared/components/container_registry_metadata_database_alert.vue'; @@ -671,22 +671,26 @@ describe('List Page', () => { }); describe('tracking', () => { + let trackingSpy; + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent(); fireFirstSortUpdate(); }); + afterEach(() => { + unmockTracking(); + }); + const testTrackingCall = (action) => { - expect(Tracking.event).toHaveBeenCalledWith(undefined, action, { + expect(trackingSpy).toHaveBeenCalledWith(undefined, action, { label: 'registry_repository_delete', }); }; - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - - it('send an event when delete button is clicked', () => { + it('send an event when delete button is clicked', async () => { + await waitForPromises(); findImageList().vm.$emit('delete', {}); testTrackingCall('click_button'); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js index 204134f1ee9..3cc658c6b99 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js @@ -16,7 +16,7 @@ import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants'; import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; import TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue'; import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mavenPackage, mavenFiles, npmPackage } from '../../mock_data'; @@ -229,7 +229,11 @@ describe('PackagesApp', () => { let eventSpy; beforeEach(() => { - eventSpy = jest.spyOn(Tracking, 'event'); + eventSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); }); it(`delete button on delete modal call event with ${TRACKING_ACTIONS.DELETE_PACKAGE}`, () => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index ec58a35f304..9aea186a655 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -13,7 +13,7 @@ import { stubComponent } from 'helpers/stub_component'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { s__ } from '~/locale'; import { createAlert } from '~/alert'; import { @@ -181,9 +181,16 @@ describe('Package Files', () => { }); describe('link', () => { - beforeEach(async () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); createComponent(); - await waitForPromises(); + return waitForPromises(); + }); + + afterEach(() => { + unmockTracking(); }); it('exists', () => { @@ -195,11 +202,9 @@ describe('Package Files', () => { }); it('tracks "download-file" event on click', () => { - const eventSpy = jest.spyOn(Tracking, 'event'); - findFirstRowDownloadLink().vm.$emit('click'); - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( eventCategory, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, expect.any(Object), diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js index d83d571872c..a3969f68c68 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js @@ -17,7 +17,7 @@ import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import waitForPromises from 'helpers/wait_for_promises'; import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { TRACKING_ACTION_CLICK_PIPELINE_LINK, TRACKING_ACTION_CLICK_COMMIT_LINK, @@ -194,8 +194,12 @@ describe('Package History', () => { const category = 'UI::Packages'; beforeEach(() => { + eventSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent(); - eventSpy = jest.spyOn(Tracking, 'event'); + }); + + afterEach(() => { + unmockTracking(); }); it('clicking pipeline link tracks the right action', () => { diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js index 8e22e9a3b0c..760dba47496 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js @@ -11,7 +11,7 @@ import PackageVersionsList from '~/packages_and_registries/package_registry/comp import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, @@ -248,12 +248,16 @@ describe('PackageVersionsList', () => { const { findDeletePackagesModal } = uiElements; beforeEach(async () => { - eventSpy = jest.spyOn(Tracking, 'event'); + eventSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent({ props: { canDestroy: true } }); await waitForPromises(); finderFunction().vm.$emit('delete', deletePayload); }); + afterEach(() => { + unmockTracking(); + }); + it('passes itemsToBeDeleted to the modal', () => { expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([ packageVersions()[0], @@ -308,11 +312,15 @@ describe('PackageVersionsList', () => { const { findDeletePackagesModal, findRegistryList } = uiElements; beforeEach(async () => { - eventSpy = jest.spyOn(Tracking, 'event'); + eventSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent({ props: { canDestroy: true } }); await waitForPromises(); }); + afterEach(() => { + unmockTracking(); + }); + it('binds the right props', () => { expect(uiElements.findRegistryList().props()).toMatchObject({ items: packageVersions(), diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js index 8a774321a17..554c1cc3334 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js @@ -17,7 +17,7 @@ import { CANCEL_DELETE_PACKAGES_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { defaultPackageGroupSettings, packageData } from '../../mock_data'; describe('packages_list', () => { @@ -158,21 +158,25 @@ describe('packages_list', () => { ${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage} ${'when the user can bulk destroy packages and deletes only one package'} | ${findRegistryList} | ${[firstPackage]} `('$description', ({ finderFunction, deletePayload }) => { - let eventSpy; + let trackingSpy; const category = 'UI::NpmPackages'; beforeEach(() => { - eventSpy = jest.spyOn(Tracking, 'event'); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent({ stubs: { RegistryList } }); finderFunction().vm.$emit('delete', deletePayload); }); + afterEach(() => { + unmockTracking(); + }); + it('passes itemsToBeDeleted to the modal', () => { expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([firstPackage]); }); it('requesting delete tracks the right action', () => { - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( category, REQUEST_DELETE_PACKAGE_TRACKING_ACTION, expect.any(Object), @@ -193,7 +197,7 @@ describe('packages_list', () => { }); it('tracks the right action', () => { - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( category, DELETE_PACKAGE_TRACKING_ACTION, expect.any(Object), @@ -210,7 +214,7 @@ describe('packages_list', () => { it('canceling delete tracks the right action', () => { findDeletePackagesModal().vm.$emit('cancel'); - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( category, CANCEL_DELETE_PACKAGE_TRACKING_ACTION, expect.any(Object), @@ -219,22 +223,26 @@ describe('packages_list', () => { }); describe('when the user can bulk destroy packages', () => { - let eventSpy; + let trackingSpy; const items = [firstPackage, secondPackage]; beforeEach(() => { - eventSpy = jest.spyOn(Tracking, 'event'); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); mountComponent(); findRegistryList().vm.$emit('delete', items); }); + afterEach(() => { + unmockTracking(); + }); + it('passes itemsToBeDeleted to the modal', () => { expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(items); expect(wrapper.emitted('delete')).toBeUndefined(); }); it('requesting delete tracks the right action', () => { - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( undefined, REQUEST_DELETE_PACKAGES_TRACKING_ACTION, expect.any(Object), @@ -251,7 +259,7 @@ describe('packages_list', () => { }); it('tracks the right action', () => { - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( undefined, DELETE_PACKAGES_TRACKING_ACTION, expect.any(Object), @@ -268,7 +276,7 @@ describe('packages_list', () => { it('canceling delete tracks the right action', () => { findDeletePackagesModal().vm.$emit('cancel'); - expect(eventSpy).toHaveBeenCalledWith( + expect(trackingSpy).toHaveBeenCalledWith( undefined, CANCEL_DELETE_PACKAGES_TRACKING_ACTION, expect.any(Object), diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js index 5c64d4cb697..c26a69a2f35 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js @@ -9,7 +9,7 @@ import component from '~/packages_and_registries/settings/project/components/con import { UPDATE_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants'; import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data'; describe('Container Expiration Policy Settings Form', () => { @@ -120,10 +120,6 @@ describe('Container Expiration Policy Settings Form', () => { }); }; - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - describe.each` model | finder | fieldName | type | defaultValue ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false} @@ -269,14 +265,26 @@ describe('Container Expiration Policy Settings Form', () => { }); }); - it('tracks the submit event', async () => { - mountComponentWithApollo({ - mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), + describe('tracking', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); }); - await submitForm(); + afterEach(() => { + unmockTracking(); + }); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload); + it('tracks the submit event', async () => { + mountComponentWithApollo({ + mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), + }); + + await submitForm(); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload); + }); }); it('redirects to package and registry project settings page when submitted successfully', async () => { diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js index 50b72d3ad72..84df6c67f9d 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js @@ -13,7 +13,7 @@ import { } from '~/packages_and_registries/settings/project/constants'; import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql'; import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data'; Vue.use(VueApollo); @@ -110,10 +110,6 @@ describe('Packages Cleanup Policy Settings Form', () => { }); }; - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - afterEach(() => { fakeApollo = null; }); @@ -274,18 +270,30 @@ describe('Packages Cleanup Policy Settings Form', () => { }); }); - it('tracks the submit event', () => { - mountComponentWithApollo({ - mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()), + describe('tracking', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); }); - findForm().trigger('submit'); + afterEach(() => { + unmockTracking(); + }); - expect(Tracking.event).toHaveBeenCalledWith( - undefined, - 'submit_packages_cleanup_form', - trackingPayload, - ); + it('tracks the submit event', () => { + mountComponentWithApollo({ + mutationResolver: jest.fn().mockResolvedValue(packagesCleanupPolicyMutationPayload()), + }); + + findForm().trigger('submit'); + + expect(trackingSpy).toHaveBeenCalledWith( + undefined, + 'submit_packages_cleanup_form', + trackingPayload, + ); + }); }); it('show a success toast when submit succeed', async () => { diff --git a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js index 9041cb757ab..774e2f4291a 100644 --- a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js +++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js @@ -13,7 +13,7 @@ import { PUSH_COMMAND_LABEL, COPY_PUSH_TITLE, } from '~/packages_and_registries/container_registry/explorer/constants'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; import { dockerCommands } from 'jest/packages_and_registries/container_registry/explorer/mock_data'; @@ -35,7 +35,6 @@ describe('cli_commands', () => { }; beforeEach(() => { - jest.spyOn(Tracking, 'event'); mountComponent(); }); @@ -43,13 +42,25 @@ describe('cli_commands', () => { expect(findDropdownButton().text()).toContain(QUICK_START); }); - it('clicking on the dropdown emit a tracking event', () => { - findDropdownButton().vm.$emit('shown'); - expect(Tracking.event).toHaveBeenCalledWith( - undefined, - 'click_dropdown', - expect.objectContaining({ label: 'quickstart_dropdown' }), - ); + describe('tracking', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('clicking on the dropdown emit a tracking event', () => { + findDropdownButton().vm.$emit('shown'); + expect(trackingSpy).toHaveBeenCalledWith( + undefined, + 'click_dropdown', + expect.objectContaining({ label: 'quickstart_dropdown' }), + ); + }); }); describe.each` diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js index 299535775e0..bcc274cf7cd 100644 --- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js +++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Tracking from '~/tracking'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; @@ -59,7 +59,11 @@ describe('Package code instruction', () => { const trackingLabel = 'foo_label'; beforeEach(() => { - eventSpy = jest.spyOn(Tracking, 'event'); + eventSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); }); it('should not track when no trackingAction is provided', () => { diff --git a/spec/graphql/types/project_sort_enum_spec.rb b/spec/graphql/types/project_sort_enum_spec.rb new file mode 100644 index 00000000000..baa6f1cb631 --- /dev/null +++ b/spec/graphql/types/project_sort_enum_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ProjectSort'], feature_category: :groups_and_projects do + specify { expect(described_class.graphql_name).to eq('ProjectSort') } + + it_behaves_like 'common sort values' + + it 'exposes all the existing issue sort values' do + expect(described_class.values.keys).to include( + *%w[ + ID_ASC ID_DESC LATEST_ACTIVITY_ASC LATEST_ACTIVITY_DESC + NAME_ASC NAME_DESC PATH_ASC PATH_DESC STARS_ASC STARS_DESC + ] + ) + end +end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index 8773766c3a9..2d764f86791 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -38,6 +38,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do groupCount projectMemberships starredProjects + contributedProjects callouts namespace timelogs diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb index aa8f52d1c57..6c57cbad98b 100644 --- a/spec/helpers/appearances_helper_spec.rb +++ b/spec/helpers/appearances_helper_spec.rb @@ -234,24 +234,14 @@ RSpec.describe AppearancesHelper do describe '#custom_sign_in_description' do it 'returns an empty string if no custom description is found' do allow(helper).to receive(:current_appearance).and_return(nil) - allow(Gitlab::CurrentSettings).to receive(:sign_in_text).and_return(nil) - allow(Gitlab::CurrentSettings).to receive(:help_text).and_return(nil) expect(helper.custom_sign_in_description).to eq('') end - it 'returns a custom description if all the setting options are found' do - allow(helper).to receive(:markdown_field).and_return('1') - allow(helper).to receive(:markdown).and_return('2', '3') + it 'returns a markdown of the custom description' do + allow(helper).to receive(:markdown_field).and_return('

    1

    ') - expect(helper.custom_sign_in_description).to eq('1
    2
    3') - end - - it 'returns a custom description if only one setting options is found' do - allow(helper).to receive(:markdown_field).and_return('') - allow(helper).to receive(:markdown).and_return('2', '') - - expect(helper.custom_sign_in_description).to eq('2') + expect(helper.custom_sign_in_description).to eq('

    1

    ') end end diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event_spec.rb index 036d40f254d..c7d68f01449 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes/approved_event_spec.rb @@ -61,23 +61,44 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotes::Appro end context 'when a user with a matching username does not exist' do - before do - pull_request_author.update!(username: 'another_username') + let(:approved_event) { super().merge(approver_username: 'another_username') } + + it 'does not set an approver' do + expect_log( + stage: 'import_approved_event', + message: 'skipped due to missing user', + iid: merge_request.iid, + event_id: 4 + ) + + expect { importer.execute(approved_event) } + .to not_change { merge_request.approvals.count } + .and not_change { merge_request.notes.count } + .and not_change { merge_request.reviewers.count } + + expect(merge_request.approvals).to be_empty end - it 'finds the user based on email' do - importer.execute(approved_event) + context 'when bitbucket_server_user_mapping_by_username flag is disabled' do + before do + stub_feature_flags(bitbucket_server_user_mapping_by_username: false) + end - approval = merge_request.approvals.first + it 'finds the user based on email' do + importer.execute(approved_event) - expect(approval.user).to eq(pull_request_author) + approval = merge_request.approvals.first + + expect(approval.user).to eq(pull_request_author) + end end context 'when no users match email or username' do - let_it_be(:another_author) { create(:user) } - - before do - pull_request_author.destroy! + let(:approved_event) do + super().merge( + approver_username: 'another_username', + approver_email: 'anotheremail@example.com' + ) end it 'does not set an approver' do diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb index 11c42e715eb..7f7d12eb8fc 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_request_notes_importer_spec.rb @@ -342,12 +342,27 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporte pull_request_author.update!(username: 'another_username') end - it 'finds the user based on email' do - importer.execute + it 'does not set an approver' do + expect { importer.execute } + .to not_change { merge_request.approvals.count } + .and not_change { merge_request.notes.count } + .and not_change { merge_request.reviewers.count } - approval = merge_request.approvals.first + expect(merge_request.approvals).to be_empty + end - expect(approval.user).to eq(pull_request_author) + context 'when bitbucket_server_user_mapping_by_username flag is disabled' do + before do + stub_feature_flags(bitbucket_server_user_mapping_by_username: false) + end + + it 'finds the user based on email' do + importer.execute + + approval = merge_request.approvals.first + + expect(approval.user).to eq(pull_request_author) + end end context 'when no users match email or username' do diff --git a/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb b/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb index 16aff872c6d..78d258bd444 100644 --- a/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/user_finder_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::BitbucketServerImport::UserFinder, :clean_gitlab_redis_sh let_it_be(:user) { create(:user) } let(:created_id) { 1 } - let(:project) { instance_double(Project, creator_id: created_id, id: 1) } + let(:project) { build_stubbed(:project, creator_id: created_id, id: 1) } subject(:user_finder) { described_class.new(project) } diff --git a/spec/requests/api/graphql/user/contributed_projects_query_spec.rb b/spec/requests/api/graphql/user/contributed_projects_query_spec.rb new file mode 100644 index 00000000000..87dd1c1ded6 --- /dev/null +++ b/spec/requests/api/graphql/user/contributed_projects_query_spec.rb @@ -0,0 +1,407 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Getting contributedProjects of the user', feature_category: :groups_and_projects do + include GraphqlHelpers + + let(:query) { graphql_query_for(:user, user_params, user_fields) } + let(:user_params) { { username: user.username } } + let(:user_fields) { 'contributedProjects { nodes { id } }' } + + let_it_be(:user) { create(:user) } + let_it_be(:current_user) { create(:user) } + + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:private_project) { create(:project, :private) } + let_it_be(:internal_project) { create(:project, :internal) } + + let(:path) { %i[user contributed_projects nodes] } + + before_all do + private_project.add_developer(user) + private_project.add_developer(current_user) + + travel_to(4.hours.from_now) { create(:push_event, project: private_project, author: user) } + travel_to(3.hours.from_now) { create(:push_event, project: internal_project, author: user) } + travel_to(2.hours.from_now) { create(:push_event, project: public_project, author: user) } + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + describe 'sorting' do + let(:user_fields_with_sort) { "contributedProjects(sort: #{sort_parameter}) { nodes { id } }" } + let(:query_with_sort) { graphql_query_for(:user, user_params, user_fields_with_sort) } + + context 'when sort parameter is not provided' do + it 'returns contributed projects in default order(LATEST_ACTIVITY_DESC)' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + + context 'when sort parameter for id is provided' do + context 'when ID_ASC is provided' do + let(:sort_parameter) { 'ID_ASC' } + + it 'returns contributed projects in id ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s + ]) + end + end + + context 'when ID_DESC is provided' do + let(:sort_parameter) { 'ID_DESC' } + + it 'returns contributed projects in id descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for name is provided' do + before_all do + public_project.update!(name: 'Project A') + internal_project.update!(name: 'Project B') + private_project.update!(name: 'Project C') + end + + context 'when NAME_ASC is provided' do + let(:sort_parameter) { 'NAME_ASC' } + + it 'returns contributed projects in name ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when NAME_DESC is provided' do + let(:sort_parameter) { 'NAME_DESC' } + + it 'returns contributed projects in name descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for path is provided' do + before_all do + public_project.update!(path: 'Project-1') + internal_project.update!(path: 'Project-2') + private_project.update!(path: 'Project-3') + end + + context 'when PATH_ASC is provided' do + let(:sort_parameter) { 'PATH_ASC' } + + it 'returns contributed projects in path ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when PATH_DESC is provided' do + let(:sort_parameter) { 'PATH_DESC' } + + it 'returns contributed projects in path descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for stars is provided' do + before_all do + public_project.update!(star_count: 10) + internal_project.update!(star_count: 20) + private_project.update!(star_count: 30) + end + + context 'when STARS_ASC is provided' do + let(:sort_parameter) { 'STARS_ASC' } + + it 'returns contributed projects in stars ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when STARS_DESC is provided' do + let(:sort_parameter) { 'STARS_DESC' } + + it 'returns contributed projects in stars descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for latest activity is provided' do + context 'when LATEST_ACTIVITY_ASC is provided' do + let(:sort_parameter) { 'LATEST_ACTIVITY_ASC' } + + it 'returns contributed projects in latest activity ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when LATEST_ACTIVITY_DESC is provided' do + let(:sort_parameter) { 'LATEST_ACTIVITY_DESC' } + + it 'returns contributed projects in latest activity descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for created_at is provided' do + before_all do + public_project.update!(created_at: Time.current + 1.hour) + internal_project.update!(created_at: Time.current + 2.hours) + private_project.update!(created_at: Time.current + 3.hours) + end + + context 'when CREATED_ASC is provided' do + let(:sort_parameter) { 'CREATED_ASC' } + + it 'returns contributed projects in created_at ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when CREATED_DESC is provided' do + let(:sort_parameter) { 'CREATED_DESC' } + + it 'returns contributed projects in created_at descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + + context 'when sort parameter for updated_at is provided' do + before_all do + public_project.update!(updated_at: Time.current + 1.hour) + internal_project.update!(updated_at: Time.current + 2.hours) + private_project.update!(updated_at: Time.current + 3.hours) + end + + context 'when UPDATED_ASC is provided' do + let(:sort_parameter) { 'UPDATED_ASC' } + + it 'returns contributed projects in updated_at ascending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ]) + end + end + + context 'when UPDATED_DESC is provided' do + let(:sort_parameter) { 'UPDATED_DESC' } + + it 'returns contributed projects in updated_at descending order' do + post_graphql(query_with_sort, current_user: current_user) + + expect(graphql_data_at(*path).pluck('id')).to eq([ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ]) + end + end + end + end + + describe 'accessible' do + context 'when user profile is public' do + context 'when a logged in user with membership in the private project' do + it 'returns contributed projects with visibility to the logged in user' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path)).to contain_exactly( + a_graphql_entity_for(private_project), + a_graphql_entity_for(internal_project), + a_graphql_entity_for(public_project) + ) + end + end + + context 'when a logged in user with no visibility to the private project' do + let_it_be(:current_user_2) { create(:user) } + + it 'returns contributed projects with visibility to the logged in user' do + post_graphql(query, current_user: current_user_2) + + expect(graphql_data_at(*path)).to contain_exactly( + a_graphql_entity_for(internal_project), + a_graphql_entity_for(public_project) + ) + end + end + + context 'when an anonymous user' do + it 'returns nothing' do + post_graphql(query, current_user: nil) + + expect(graphql_data_at(*path)).to be_nil + end + end + end + + context 'when user profile is private' do + let(:user_params) { { username: private_user.username } } + let_it_be(:private_user) { create(:user, :private_profile) } + + before_all do + private_project.add_developer(private_user) + private_project.add_developer(current_user) + + create(:push_event, project: private_project, author: private_user) + create(:push_event, project: internal_project, author: private_user) + create(:push_event, project: public_project, author: private_user) + end + + context 'when a logged in user' do + it 'returns no project' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path)).to be_empty + end + end + + context 'when an anonymous user' do + it 'returns nothing' do + post_graphql(query, current_user: nil) + + expect(graphql_data_at(*path)).to be_nil + end + end + + context 'when a logged in user is the user' do + it 'returns the user\'s all contributed projects' do + post_graphql(query, current_user: private_user) + + expect(graphql_data_at(*path)).to contain_exactly( + a_graphql_entity_for(private_project), + a_graphql_entity_for(internal_project), + a_graphql_entity_for(public_project) + ) + end + end + end + end + + describe 'sorting and pagination' do + let(:data_path) { [:user, :contributed_projects] } + + def pagination_query(params) + graphql_query_for(:user, user_params, "contributedProjects(#{params}) { #{page_info} nodes { id } }") + end + + context 'when sorting in latest activity ascending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :LATEST_ACTIVITY_ASC } + let(:first_param) { 1 } + let(:all_records) do + [ + public_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + private_project.to_global_id.to_s + ] + end + end + end + + context 'when sorting in latest activity descending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :LATEST_ACTIVITY_DESC } + let(:first_param) { 1 } + let(:all_records) do + [ + private_project.to_global_id.to_s, + internal_project.to_global_id.to_s, + public_project.to_global_id.to_s + ] + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb index 7a3b3d6924c..6c0aebaf7b7 100644 --- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -33,6 +33,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do groupCount projectMemberships starredProjects + contributedProjects callouts merge_request_interaction namespace