Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-23 03:18:16 +00:00
parent e27af51af8
commit 74e773b861
44 changed files with 1140 additions and 484 deletions

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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("<br>").html_safe
markdown_field(current_appearance, :description)
end
def brand_member_guidelines

View File

@ -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 ' \

View File

@ -15785,6 +15785,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="addonuserauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="addonuserauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="addonusercontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `AddOnUser.groups`
Groups where the user has access.
@ -16549,6 +16565,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="autocompleteduserauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="autocompleteduserauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="autocompletedusercontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `AutocompletedUser.groups`
Groups where the user has access.
@ -18725,6 +18757,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentuserauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="currentuserauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="currentusercontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `CurrentUser.groups`
Groups where the user has access.
@ -24001,6 +24049,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestassigneeauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="mergerequestassigneeauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mergerequestassigneecontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `MergeRequestAssignee.groups`
Groups where the user has access.
@ -24313,6 +24377,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestauthorauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="mergerequestauthorauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mergerequestauthorcontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `MergeRequestAuthor.groups`
Groups where the user has access.
@ -24672,6 +24752,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestparticipantauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="mergerequestparticipantauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mergerequestparticipantcontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `MergeRequestParticipant.groups`
Groups where the user has access.
@ -25020,6 +25116,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestreviewerauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="mergerequestreviewerauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="mergerequestreviewercontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `MergeRequestReviewer.groups`
Groups where the user has access.
@ -27431,6 +27543,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="projectdependenciescomponentnames"></a>`componentNames` | [`[String!]`](#string) | Filter dependencies by component names. |
| <a id="projectdependenciespackagemanagers"></a>`packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. |
| <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. |
| <a id="projectdependenciessourcetypes"></a>`sourceTypes` | [`[SbomSourceType!]`](#sbomsourcetype) | Filter dependencies by source type. |
##### `Project.deployment`
@ -30727,6 +30840,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usercoreauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="usercoreauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="usercorecontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
##### `UserCore.groups`
Groups where the user has access.
@ -34477,6 +34606,31 @@ Project member relation.
| <a id="projectmemberrelationinvited_groups"></a>`INVITED_GROUPS` | Invited Groups members. |
| <a id="projectmemberrelationshared_into_ancestors"></a>`SHARED_INTO_ANCESTORS` | Shared Into Ancestors members. |
### `ProjectSort`
Values for sorting projects.
| Value | Description |
| ----- | ----------- |
| <a id="projectsortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
| <a id="projectsortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
| <a id="projectsortid_asc"></a>`ID_ASC` | ID by ascending order. |
| <a id="projectsortid_desc"></a>`ID_DESC` | ID by descending order. |
| <a id="projectsortlatest_activity_asc"></a>`LATEST_ACTIVITY_ASC` | Latest activity by ascending order. |
| <a id="projectsortlatest_activity_desc"></a>`LATEST_ACTIVITY_DESC` | Latest activity by descending order. |
| <a id="projectsortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="projectsortname_desc"></a>`NAME_DESC` | Name by descending order. |
| <a id="projectsortpath_asc"></a>`PATH_ASC` | Path by ascending order. |
| <a id="projectsortpath_desc"></a>`PATH_DESC` | Path by descending order. |
| <a id="projectsortstars_asc"></a>`STARS_ASC` | Stars by ascending order. |
| <a id="projectsortstars_desc"></a>`STARS_DESC` | Stars by descending order. |
| <a id="projectsortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
| <a id="projectsortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. |
| <a id="projectsortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `CREATED_ASC`. |
| <a id="projectsortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `CREATED_DESC`. |
| <a id="projectsortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in GitLab 13.5. This was renamed. Use: `UPDATED_ASC`. |
| <a id="projectsortupdated_desc"></a>`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.
| <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. |
| <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. |
### `SbomSourceType`
Values for sbom source types.
| Value | Description |
| ----- | ----------- |
| <a id="sbomsourcetypecontainer_scanning"></a>`CONTAINER_SCANNING` | Source Type: container_scanning. |
| <a id="sbomsourcetypecontainer_scanning_for_registry"></a>`CONTAINER_SCANNING_FOR_REGISTRY` | Source Type: container_scanning_for_registry. |
| <a id="sbomsourcetypedependency_scanning"></a>`DEPENDENCY_SCANNING` | Source Type: dependency_scanning. |
| <a id="sbomsourcetypenil_source"></a>`NIL_SOURCE` | Enum source nil. |
### `ScanStatus`
The status of the security scan.
@ -36983,6 +37148,22 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="userauthoredmergerequestsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Merge requests updated after this timestamp. |
| <a id="userauthoredmergerequestsupdatedbefore"></a>`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 |
| ---- | ---- | ----------- |
| <a id="usercontributedprojectssort"></a>`sort` | [`ProjectSort`](#projectsort) | Sort contributed projects. |
###### `User.groups`
Groups where the user has access.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -181,7 +181,7 @@ The following languages and dependency managers are supported when using the Dep
<tr>
<td rowspan="4">Python</td>
<td rowspan="4">3.11<sup><b><a href="#notes-regarding-supported-languages-and-package-managers-7">7</a></b></sup></td>
<td><a href="https://setuptools.readthedocs.io/en/latest/">setuptools</a></td>
<td><a href="https://setuptools.readthedocs.io/en/latest/">setuptools</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-8">8</a></b></sup></td>
<td><code>setup.py</code></td>
<td>N</td>
</tr>
@ -279,6 +279,12 @@ The following languages and dependency managers are supported when using the Dep
Support for prior Python versions was <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/441201">deprecated</a> in GitLab 16.9 and <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/441491">removed</a> in GitLab 17.0.
</p>
</li>
<li>
<a id="notes-regarding-supported-languages-and-package-managers-8"></a>
<p>
Excludes both <code>pip</code> and <code>setuptools</code> from the report as they are required by the installer.
</p>
</li>
</ol>
<!-- markdownlint-enable MD044 -->

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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.

View File

@ -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'))

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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&not[service_name]=serviceName2` +
`&severity_name=info&not[severity_name]=warning` +
`&severity_number=9&not[severity_number]=10` +
`&trace_id=traceId` +
`&span_id=spanId` +
`&fingerprint=fingerprint` +
`&trace_flags=1&not[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('');
});
});
});
});

View File

@ -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',
});
});

View File

@ -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',
});
});

View File

@ -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');

View File

@ -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}`, () => {

View File

@ -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),

View File

@ -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', () => {

View File

@ -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(),

View File

@ -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),

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -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`

View File

@ -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', () => {

View File

@ -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

View File

@ -38,6 +38,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
groupCount
projectMemberships
starredProjects
contributedProjects
callouts
namespace
timelogs

View File

@ -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('<p>1</p>')
expect(helper.custom_sign_in_description).to eq('1<br>2<br>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('<p>1</p>')
end
end

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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