提交 831b6108 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 3a9076e0
/* eslint-disable consistent-return, func-names, array-callback-return */
import $ from 'jquery';
import { intersection } from 'lodash';
import { difference, intersection, union } from 'lodash';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
import { __ } from './locale';
......@@ -36,43 +34,6 @@ export default {
return new Flash(__('Issue update failed'));
},
getSelectedIssues() {
return this.issues.has('.selected-issuable:checked');
},
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(labelId => {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
},
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach(id => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
},
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
......@@ -93,35 +54,37 @@ export default {
},
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
formData.update.add_label_ids = this.$labelDropdown.data('user-checked');
formData.update.remove_label_ids = this.$labelDropdown.data('user-unchecked');
}
return formData;
},
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
const dirtyLabelIds = $labelSelect.data('marked') || [];
const chosenLabelIds = [...this.getOriginalMarkedIds(), ...dirtyLabelIds];
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', chosenLabelIds);
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
const userCheckedIds = $labelSelect.data('user-checked') || [];
const userUncheckedIds = $labelSelect.data('user-unchecked') || [];
// Common labels plus user checked labels minus user unchecked labels
const checkedIdsToShow = difference(
union(this.getOriginalCommonIds(), userCheckedIds),
userUncheckedIds,
);
// Indeterminate labels minus user checked labels minus user unchecked labels
const indeterminateIdsToShow = difference(
this.getOriginalIndeterminateIds(),
userCheckedIds,
userUncheckedIds,
);
$labelSelect.data('marked', checkedIdsToShow);
$labelSelect.data('indeterminate', indeterminateIdsToShow);
},
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
......
......@@ -313,6 +313,7 @@ export default {
<gl-label
v-for="label in issuable.labels"
:key="label.id"
data-qa-selector="issuable-label"
:target="labelHref(label)"
:background-color="label.color"
:description="label.description"
......
......@@ -275,9 +275,13 @@ export default {
const {
label_name: labels,
milestone_title: milestoneTitle,
'not[label_name]': excludedLabels,
'not[milestone_title]': excludedMilestone,
...filters
} = this.getQueryObject();
// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/227880
if (milestoneTitle) {
filters.milestone = milestoneTitle;
}
......@@ -288,6 +292,14 @@ export default {
filters.state = 'opened';
}
if (excludedLabels) {
filters['not[labels]'] = excludedLabels;
}
if (excludedMilestone) {
filters['not[milestone]'] = excludedMilestone;
}
Object.assign(filters, sortOrderMap[this.sortKey]);
this.filters = filters;
......
......@@ -3,7 +3,7 @@
/* global ListLabel */
import $ from 'jquery';
import { isEqual, escape, sortBy, template } from 'lodash';
import { difference, isEqual, escape, sortBy, template } from 'lodash';
import { sprintf, s__, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
......@@ -560,45 +560,20 @@ export default class LabelsSelect {
IssuableBulkUpdateActions.willUpdateLabels = true;
}
// eslint-disable-next-line class-methods-use-this
setDropdownData($dropdown, isMarking, value) {
const markedIds = $dropdown.data('marked') || [];
const unmarkedIds = $dropdown.data('unmarked') || [];
const indeterminateIds = $dropdown.data('indeterminate') || [];
setDropdownData($dropdown, isChecking, labelId) {
let userCheckedIds = $dropdown.data('user-checked') || [];
let userUncheckedIds = $dropdown.data('user-unchecked') || [];
if (isMarking) {
markedIds.push(value);
let i = indeterminateIds.indexOf(value);
if (i > -1) {
indeterminateIds.splice(i, 1);
}
i = unmarkedIds.indexOf(value);
if (i > -1) {
unmarkedIds.splice(i, 1);
}
if (isChecking) {
userCheckedIds = userCheckedIds.concat(labelId);
userUncheckedIds = difference(userUncheckedIds, [labelId]);
} else {
// If marked item (not common) is unmarked
const i = markedIds.indexOf(value);
if (i > -1) {
markedIds.splice(i, 1);
}
// If an indeterminate item is being unmarked
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
// If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection)
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
userUncheckedIds = userUncheckedIds.concat(labelId);
userCheckedIds = difference(userCheckedIds, [labelId]);
}
$dropdown.data('marked', markedIds);
$dropdown.data('unmarked', unmarkedIds);
$dropdown.data('indeterminate', indeterminateIds);
$dropdown.data('user-checked', userCheckedIds);
$dropdown.data('user-unchecked', userUncheckedIds);
}
// eslint-disable-next-line class-methods-use-this
setOriginalDropdownData($container, $dropdown) {
......
......@@ -360,14 +360,14 @@ export const receiveAnnotationsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_ANNOTATIONS_SUCCESS, data);
export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_ANNOTATIONS_FAILURE);
export const fetchDashboardValidationWarnings = ({ state, dispatch }) => {
export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
/**
* Normally, the default dashboard won't throw any validation warnings.
*
* However, if a bug sneaks into the default dashboard making it invalid,
* this might come handy for our clients
*/
const dashboardPath = state.currentDashboard || DEFAULT_DASHBOARD_PATH;
const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getDashboardValidationWarnings,
......
......@@ -73,15 +73,23 @@ export default {
},
},
beforeDestroy() {
removeCustomEventListener(
this.editorApi,
CUSTOM_EVENTS.openAddImageModal,
this.onOpenAddImageModal,
);
this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode);
this.removeListeners();
},
methods: {
addListeners(editorApi) {
addCustomEventListener(editorApi, CUSTOM_EVENTS.openAddImageModal, this.onOpenAddImageModal);
editorApi.eventManager.listen('changeMode', this.onChangeMode);
},
removeListeners() {
removeCustomEventListener(
this.editorApi,
CUSTOM_EVENTS.openAddImageModal,
this.onOpenAddImageModal,
);
this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode);
},
resetInitialValue(newVal) {
this.editorInstance.invoke('setMarkdown', newVal);
},
......@@ -92,13 +100,8 @@ export default {
this.editorApi = editorApi;
registerHTMLToMarkdownRenderer(editorApi);
addCustomEventListener(
this.editorApi,
CUSTOM_EVENTS.openAddImageModal,
this.onOpenAddImageModal,
);
this.editorApi.eventManager.listen('changeMode', this.onChangeMode);
this.addListeners(editorApi);
},
onOpenAddImageModal() {
this.$refs.addImageModal.show();
......
const buildHTMLToMarkdownRender = baseRenderer => {
import { defaults, repeat } from 'lodash';
const DEFAULTS = {
subListIndentSpaces: 4,
};
const countIndentSpaces = text => {
const matches = text.match(/^\s+/m);
return matches ? matches[0].length : 0;
};
const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
const { subListIndentSpaces } = defaults(formattingPreferences, DEFAULTS);
// eslint-disable-next-line @gitlab/require-i18n-strings
const sublistNode = 'LI OL, LI UL';
return {
TEXT_NODE(node) {
return baseRenderer.getSpaceControlled(
......@@ -6,6 +22,31 @@ const buildHTMLToMarkdownRender = baseRenderer => {
node,
);
},
/*
* This converter overwrites the default indented list converter
* to allow us to parameterize the number of indent spaces for
* sublists.
*
* See the original implementation in
* https://github.com/nhn/tui.editor/blob/master/libs/to-mark/src/renderer.basic.js#L161
*/
[sublistNode](node, subContent) {
const baseResult = baseRenderer.convert(node, subContent);
// Default to 1 to prevent possible divide by 0
const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1;
const reindentedList = baseResult
.split('\n')
.map(line => {
const itemIndentSpacesCount = countIndentSpaces(line);
const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount);
const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel);
return line.replace(/^ +/, indentSpaces);
})
.join('\n');
return reindentedList;
},
};
};
......
......@@ -13,7 +13,7 @@ const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
buildToken('openTag', tagType, {
attributes: { contenteditable: false },
classNames: [
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
],
});
......
# frozen_string_literal: true
class ResourceStateEventFinder
include FinderMethods
def initialize(current_user, eventable)
@current_user = current_user
@eventable = eventable
end
def execute
return ResourceStateEvent.none unless can_read_eventable?
eventable.resource_state_events.includes(:user) # rubocop: disable CodeReuse/ActiveRecord
end
def can_read_eventable?
return unless eventable
Ability.allowed?(current_user, read_ability, eventable)
end
private
attr_reader :current_user, :eventable
def read_ability
:"read_#{eventable.class.to_ability_name}"
end
end
......@@ -14,4 +14,8 @@ class ResourceStateEvent < ResourceEvent
def self.issuable_attrs
%i(issue merge_request).freeze
end
def issuable
issue || merge_request
end
end
......@@ -8,6 +8,9 @@ module Metrics
DASHBOARD_PATH = 'config/prometheus/cluster_metrics.yml'
DASHBOARD_NAME = 'Cluster'
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '9349afc1d96329c08ab478ea0b77db94ee5cc2549b8c754fba67a7f424666b22'
SEQUENCE = [
STAGES::ClusterEndpointInserter,
STAGES::PanelIdsInserter,
......@@ -26,6 +29,12 @@ module Metrics
def allowed?
true
end
private
def dashboard_version
DASHBOARD_VERSION
end
end
end
end
......@@ -5,6 +5,15 @@ module Metrics
class PodDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/pod_metrics.yml'
DASHBOARD_NAME = 'Pod Health'
# SHA256 hash of dashboard content
DASHBOARD_VERSION = 'f12f641d2575d5dcb69e2c633ff5231dbd879ad35020567d8fc4e1090bfdb4b4'
private
def dashboard_version
DASHBOARD_VERSION
end
end
end
end
......@@ -32,8 +32,12 @@ module Metrics
private
def dashboard_version
raise NotImplementedError
end
def cache_key
"metrics_dashboard_#{dashboard_path}"
"metrics_dashboard_#{dashboard_path}_#{dashboard_version}"
end
def dashboard_path
......
......@@ -8,6 +8,9 @@ module Metrics
DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml'
DASHBOARD_NAME = N_('Default dashboard')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '1dff3e3cb76e73c8e368823c98b34c61aec0d141978450dea195a3b3dc2415d6'
SEQUENCE = [
STAGES::CustomMetricsInserter,
STAGES::MetricEndpointInserter,
......@@ -35,6 +38,12 @@ module Metrics
params[:dashboard_path].nil? && params[:environment]&.project&.self_monitoring?
end
end
private
def dashboard_version
DASHBOARD_VERSION
end
end
end
end
......@@ -8,6 +8,9 @@ module Metrics
DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
DASHBOARD_NAME = N_('Default dashboard')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
SEQUENCE = [
STAGES::CommonMetricsInserter,
STAGES::CustomMetricsInserter,
......@@ -30,6 +33,12 @@ module Metrics
}]
end
end
private
def dashboard_version
DASHBOARD_VERSION
end
end
end
end
......@@ -107,12 +107,13 @@ module Projects
create_readme if @initialize_with_readme
end
# Refresh the current user's authorizations inline (so they can access the
# project immediately after this request completes), and any other affected
# users in the background
# Add an authorization for the current user authorizations inline
# (so they can access the project immediately after this request
# completes), and any other affected users in the background
def setup_authorizations
if @project.group
current_user.refresh_authorized_projects
current_user.project_authorizations.create!(project: @project,
access_level: @project.group.max_member_access_for_user(current_user))
if Feature.enabled?(:specialized_project_authorization_workers)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
......
---
title: 'Static Site Editor: Set default sublist indent spaces to four space characters'
merge_request: 36756
author:
type: changed
---
title: Create API to retrieve resource state events
merge_request: 35210
author:
type: added
---
title: Fix bulk editing labels bug
merge_request: 36981
author:
type: fixed
---
title: Fix failing dashboard schema validation calls
merge_request: 37108
author:
type: fixed
---
title: Swap Grape over to Gitlab::Json
merge_request: 36472
author:
type: performance
---
title: Speed up project creation for users with many projects
merge_request: 37070
author:
type: performance
---
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Resource state events API
Resource state events keep track of what happens to GitLab [issues](../user/project/issues/) and
[merge requests](../user/project/merge_requests/).
Use them to track which state was set, who did it, and when it happened.
## Issues
### List project issue state events
Gets a list of all state events for a single issue.
```plaintext
GET /projects/:id/issues/:issue_iid/resource_state_events
```
| Attribute | Type | Required | Description |
| ----------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `issue_iid` | integer | yes | The IID of an issue |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_state_events"
```
Example response:
```json
[
{
"id": 142,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-20T13:38:20.077Z",
"resource_type": "Issue",
"resource_id": 11,
"state": "opened"
},
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "Issue",
"resource_id": 11,
"state": "closed"
}
]
```
### Get single issue state event
Returns a single state event for a specific project issue
```plaintext
GET /projects/:id/issues/:issue_iid/resource_state_events/:resource_state_event_id
```
Parameters:
| Attribute | Type | Required | Description |
| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
| `issue_iid` | integer | yes | The IID of an issue |
| `resource_state_event_id` | integer | yes | The ID of a state event |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_state_events/143"
```
Example response:
```json
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "Issue",
"resource_id": 11,
"state": "closed"
}
```
## Merge requests
### List project merge request state events
Gets a list of all state events for a single merge request.
```plaintext
GET /projects/:id/merge_requests/:merge_request_iid/resource_state_events
```
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
| `merge_request_iid` | integer | yes | The IID of a merge request |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_state_events"
```
Example response:
```json
[
{
"id": 142,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-20T13:38:20.077Z",
"resource_type": "MergeRequest",
"resource_id": 11,
"state": "opened"
},
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "MergeRequest",
"resource_id": 11,
"state": "closed"
}
]
```
### Get single merge request state event
Returns a single state event for a specific project merge request
```plaintext
GET /projects/:id/merge_requests/:merge_request_iid/resource_state_events/:resource_state_event_id
```
Parameters:
| Attribute | Type | Required | Description |
| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `merge_request_iid` | integer | yes | The IID of a merge request |
| `resource_state_event_id` | integer | yes | The ID of a state event |
Example request:
```shell
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/resource_state_events/120"
```
Example response:
```json
{
"id": 120,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "MergeRequest",
"resource_id": 11,
"state": "closed"
}
```
......@@ -671,6 +671,7 @@ appear to be associated to any of the services running, since they all appear to
| `suggestions` | `usage_activity_by_stage` | `create` | | EE | |
| `approval_project_rules` | `usage_activity_by_stage` | `create` | | EE | Number of project approval rules |
| `approval_project_rules_with_target_branch` | `usage_activity_by_stage` | `create` | | EE | Number of project approval rules with not default target branch |
| `merge_requests_with_added_rules` | `usage_activity_by_stage` | `create` | | EE | Merge Requests with added rules |
| `clusters` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
| `clusters_applications_prometheus` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
| `operations_dashboard_default_dashboard` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
......
......@@ -206,6 +206,12 @@ NOTE: **Note:**
Configuration files nested under subdirectories of `.gitlab/dashboards` are not
supported and will not be available in the UI.
### Navigating to a custom dashboard
Custom dashboards are uniquely identified by their filenames. In order to quickly view the custom dashboard,
just use the dashboard filename in the URL this way:
`https://gitlab-instance.example.com/project/-/metrics/custom_dashboard_name.yml`.
### Duplicating a GitLab-defined dashboard
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37238) in GitLab 12.7.
......
......@@ -61,14 +61,13 @@ The `gitlab-cov-fuzz` is a command-line tool that runs the instrumented applicat
analyzes the exception information that the fuzzer outputs. It also downloads the [corpus](#glossary)
and crash events from previous pipelines automatically. This helps your fuzz targets build on the progress of
previous fuzzing jobs. The parsed crash events and data are written to
`gl-coverage-fuzzing-report.json` and then displayed in the pipeline and security dashboard.
`gl-coverage-fuzzing-report.json`.
### Artifacts
Each fuzzing step outputs these artifacts:
- `gl-coverage-fuzzing-report.json`: Parsed by GitLab's backend to show results in the security
dashboard. This file's format may change in future releases.
- `gl-coverage-fuzzing-report.json`: This file's format may change in future releases.
- `artifacts.zip`: This file contains two directories:
- `corpus`: Holds all test cases generated by the current and all previous jobs.
- `crashes`: Holds all crash events the current job encountered as well as those not fixed in
......
......@@ -322,7 +322,7 @@ Some analyzers make it possible to filter out vulnerabilities under a given thre
| Environment variable | Default value | Description |
|-------------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `SAST_EXCLUDED_PATHS` | `spec, test, tests, tmp` | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs, or file or folder paths (for example, `doc,spec` ). Parent directories will also match patterns. |
| `SAST_BANDIT_EXCLUDED_PATHS` | | comma-separated list of paths to exclude from scan. Uses Python's [`fnmatch` syntax](https://docs.python.org/2/library/fnmatch.html); For example: `'*/tests/*'` |
| `SAST_BANDIT_EXCLUDED_PATHS` | | Comma-separated list of paths to exclude from scan. Uses Python's [`fnmatch` syntax](https://docs.python.org/2/library/fnmatch.html); For example: `'*/tests/*, */venv/*'` |
| `SAST_BRAKEMAN_LEVEL` | 1 | Ignore Brakeman vulnerabilities under given confidence level. Integer, 1=Low 3=High. |
| `SAST_DISABLE_BABEL` | `false` | Disable Babel processing for the NodeJsScan scanner. Set to `true` to disable Babel processing. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33065) in GitLab 13.2. |
| `SAST_FLAWFINDER_LEVEL` | 1 | Ignore Flawfinder vulnerabilities under given risk level. Integer, 0=No risk, 5=High risk. |
......
......@@ -49,6 +49,29 @@ These emails contain details of the alert, and a link for more information.
To send separate email notifications to users with
[Developer permissions](../permissions.md), see [Configure incidents](#configure-incidents-ultimate).
## Configure PagerDuty integration
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/119018) in GitLab 13.2.
You can set up a webhook with PagerDuty to automatically create a GitLab issue
for each PagerDuty incident. This configuration requires you to make changes
in both PagerDuty and GitLab:
1. Sign in as a user with Maintainer [permissions](../permissions.md).
1. Navigate to **{settings}** **Settings > Operations > Incidents** and expand **Incidents**.
1. Select the **PagerDuty integration** tab:
![PagerDuty incidents integration](img/pagerduty_incidents_integration_13_2.png)
1. Activate the integration, and save the changes in GitLab.
1. Copy the value of **Webhook URL**, as you'll need it in a later step.
1. Follow the steps described in the
[PagerDuty documentation](https://support.pagerduty.com/docs/webhooks)
to add the webhook URL to a PagerDuty webhook integration.
To confirm the integration is successful, trigger a test incident from PagerDuty to
confirm that a GitLab issue is created from the incident.
## Configure Prometheus alerts
You can set up Prometheus alerts in:
......
---
stage: Defend
group: container_security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Securing your deployed applications
GitLab makes it easy to secure applications deployed in [connected Kubernetes clusters](index.md).
You can benefit from the protection of a [Web Application Firewall](../../../topics/web_application_firewall/quick_start_guide.md),
[Network Policies](../../../topics/autodevops/stages.md#network-policy),
or even [Container Host Security](../../clusters/applications.md#install-falco-using-gitlab-cicd).
This page contains full end-to-end steps and instructions to connect your cluster to GitLab and
install these features, whether or not your applications are deployed through GitLab CI/CD. If you
use [Auto DevOps](../../../topics/autodevops/index.md)
to build and deploy your application with GitLab, see the documentation for the respective
[GitLab Managed Applications](../../clusters/applications.md)
above.
## Overview
At a high level, the required steps include the following:
- Connect the cluster to GitLab.
- Set up one or more runners.
- Set up a cluster management project.
- Install a Web Application Firewall, Network Policies, and/or Container Host
Security.
- Install Prometheus to get statistics and metrics in the
[threat monitoring](../../application_security/threat_monitoring/)
dashboard.
### Requirements
Minimum requirements (depending on the GitLab Manage Application you want to install):
- Your cluster is connected to GitLab (ModSecurity, Cilium, and Falco).
- At least one GitLab Runner is installed (Cilium and Falco only).
### Understanding how GitLab Managed Apps are installed
You install GitLab Managed Apps from the GitLab web interface with a one-click setup process. GitLab
uses Sidekiq (a background processing service) to facilitate this.
```mermaid
sequenceDiagram
autonumber
GitLab->>+Sidekiq: Install a GitLab Managed App
Sidekiq->>+Kubernetes: Helm install
Kubernetes-->>-Sidekiq: Installation complete
Sidekiq-->>-GitLab: Refresh UI
```
NOTE: **Note:**
This diagram uses the term _Kubernetes_ for simplicity. In practice, Sidekiq connects to a Helm
Tiller daemon running in a pod in the cluster.
Although this installation method is easier because it's a point-and-click action in the user
interface, it's inflexible and hard to debug. When something goes wrong, you can't see the
deployment logs. The Web Application Firewall feature uses this installation method.
However, the next generation of GitLab Managed Apps V2 ([CI/CD-based GitLab Managed Apps](https://gitlab.com/groups/gitlab-org/-/epics/2103))
don't use Sidekiq to deploy. All the applications are deployed using a GitLab CI/CD pipeline and
therefore GitLab Runners.
```mermaid
sequenceDiagram
autonumber
GitLab->>+GitLab: Trigger pipeline
GitLab->>+Runner: Run deployment job
Runner->>+Kubernetes: Helm install
Kubernetes-->>-Runner: Installation is complete
Runner-->>-GitLab: Report job status and update pipeline
```
Debugging is easier because you have access to the raw logs of these jobs (the Helm Tiller output is
available as an artifact in case of failure) and the flexibility is much better. Since these
deployments are only triggered when a pipeline is running (most likely when there's a new commit in
the cluster management repository), every action has a paper trail and follows the classic merge
request workflow (approvals, merge, deploy). The Network Policy (Cilium) Managed App and Container
Host Security (Falco) are deployed with this model.
## Connect the cluster to GitLab
To deploy GitLab Managed Apps to your cluster, you must first
[add your cluster](add_remove_clusters.md)
to GitLab. Then [install](../../clusters/applications.md#installing-applications)
the Web Application Firewall from the project or group Kubernetes page.
Note that your project doesn't have to be hosted or deployed through GitLab. You can manage a
cluster independent of the applications that use the cluster.
## Set up a GitLab Runner
To install CI/CD-based GitLab Managed Apps, a pipeline using a GitLab Runner must be running in
GitLab. You can [install a GitLab Runner](../../clusters/applications.md#gitlab-runner)
in the Kubernetes cluster added in the previous step, or use one of the shared runners provided by
GitLab if you're using GitLab.com.
With your cluster connected to GitLab and a GitLab Runner in place, you can proceed to the next
steps and start installing the Cilium and Falco GitLab Managed Apps to secure your applications
hosted on this cluster.
## Create a Cluster Management Project
A [Cluster Management Project](../../clusters/management_project.md)
is a GitLab project that contains a `.gitlab-ci.yml` file to deploy GitLab Managed Apps to your
cluster. This project runs the required charts with the Kubernetes
[`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
privileges.
The creation of this project starts like any other GitLab project. Use an empty
project and add a `gitlab-ci.yml` file at the root, containing this template:
```yaml
include:
- template: Managed-Cluster-Applications.gitlab-ci.yml
```
To make this project a Cluster Management Project, follow these
[instructions](../../clusters/management_project.md#selecting-a-cluster-management-project).
This project can be designated as such even if your application isn't hosted on GitLab. In this
case, create a new empty project where you can select your newly created Cluster Management Project.
## Install GitLab Container Network Policy
GitLab Container Network Policy is based on [Cilium](https://cilium.io/). To
install the Cilium GitLab Managed App, add a
`.gitlab/managed-apps/config.yaml` file to your Cluster Management project:
```yaml
# possible values are gke, eks or you can leave it blank
clusterType: gke
cilium:
installed: true
```
Your application doesn't have to be managed or deployed by GitLab to leverage this feature.
[Read more](../../clusters/applications.md#install-cilium-using-gitlab-cicd)
about configuring Container Network Policy.
## Install GitLab Container Host Security
Similarly, you can install Container Host Security, based on
[Falco](https://falco.org/), in your `.gitlab/managed-apps/config.yaml`:
```yaml
falco:
installed: true
```
[Read more] about configuring Container Host Security.
......@@ -110,6 +110,7 @@ module API
end
format :json
formatter :json, Gitlab::Json::GrapeFormatter
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
......@@ -178,6 +179,7 @@ module API
mount ::API::Discussions
mount ::API::ResourceLabelEvents
mount ::API::ResourceMilestoneEvents
mount ::API::ResourceStateEvents
mount ::API::NotificationSettings
mount ::API::Pages
mount ::API::PagesDomains
......
# frozen_string_literal: true
module API
module Entities
class ResourceStateEvent < Grape::Entity
expose :id
expose :user, using: Entities::UserBasic
expose :created_at
expose :resource_type do |event, _options|
event.issuable.class.name
end
expose :resource_id do |event, _options|
event.issuable.id
end
expose :state
end
end
end
......@@ -234,18 +234,6 @@ module API
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'buildkite' => [
......@@ -314,18 +302,6 @@ module API
name: :project_url,
type: String,
desc: 'Project URL'
},
{
required: false,
name: :description,
type: String,
desc: 'Description'
},
{
required: false,
name: :title,
type: String,
desc: 'Title'
}
],
'discord' => [
......
# frozen_string_literal: true
module API
class ResourceStateEvents < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }
[Issue, MergeRequest].each do |eventable_class|
eventable_name = eventable_class.to_s.underscore
params do
requires :id, type: String, desc: "The ID of a project"
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{eventable_class.to_s.downcase} resource state events" do
success Entities::ResourceStateEvent
end
params do
requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
use :pagination
end
get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events" do
eventable = find_noteable(eventable_class, params[:eventable_iid])
events = ResourceStateEventFinder.new(current_user, eventable).execute
present paginate(events), with: Entities::ResourceStateEvent
end
desc "Get a single #{eventable_class.to_s.downcase} resource state event" do
success Entities::ResourceStateEvent
end
params do
requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
requires :event_id, type: Integer, desc: 'The ID of a resource state event'
end
get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id" do
eventable = find_noteable(eventable_class, params[:eventable_iid])
event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id])
present event, with: Entities::ResourceStateEvent
end
end
end
end
end
......@@ -23,6 +23,7 @@ variables:
paths:
- corpus
- crashes
- gl-coverage-fuzzing-report.json
reports:
coverage_fuzzing: gl-coverage-fuzzing-report.json
when: always
......
......@@ -67,6 +67,15 @@ module Gitlab
::JSON.pretty_generate(object, opts)
end
# Feature detection for using Oj instead of the `json` gem.
#
# @return [Boolean]
def enable_oj?
return false unless feature_table_exists?
Feature.enabled?(:oj_json, default_enabled: true)
end
private
# Convert JSON string into Ruby through toggleable adapters.
......@@ -176,13 +185,6 @@ module Gitlab
raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) }
end
# @return [Boolean]
def enable_oj?
return false unless feature_table_exists?
Feature.enabled?(:oj_json, default_enabled: true)
end
# There are a variety of database errors possible when checking the feature
# flags at the wrong time during boot, e.g. during migrations. We don't care
# about these errors, we just need to ensure that we skip feature detection
......@@ -195,5 +197,28 @@ module Gitlab
false
end
end
# GrapeFormatter is a JSON formatter for the Grape API.
# This is set in lib/api/api.rb
class GrapeFormatter
# Convert an object to JSON.
#
# This will default to the built-in Grape formatter if either :oj_json or :grape_gitlab_json
# flags are disabled.
#
# The `env` param is ignored because it's not needed in either our formatter or Grape's,
# but it is passed through for consistency.
#
# @param object [Object]
# @return [String]
def self.call(object, env = nil)
if Gitlab::Json.enable_oj? && Feature.enabled?(:grape_gitlab_json, default_enabled: true)
Gitlab::Json.dump(object)
else
Grape::Formatter::Json.call(object, env)
end
end
end
end
end
......@@ -680,6 +680,8 @@ module Gitlab
clear_memoization(:unique_visit_service)
clear_memoization(:deployment_minimum_id)
clear_memoization(:deployment_maximum_id)
clear_memoization(:approval_merge_request_rule_minimum_id)
clear_memoization(:approval_merge_request_rule_maximum_id)
end
# rubocop: disable CodeReuse/ActiveRecord
......
......@@ -3,11 +3,10 @@ LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
ENV DEBIAN_FRONTEND="noninteractive"
ENV DOCKER_VERSION="17.09.0-ce"
ENV CHROME_VERSION="83.0.4103.61-1"
ENV CHROME_DRIVER_VERSION="83.0.4103.39"
ENV CHROME_VERSION="84.0.4147.89-1"
ENV CHROME_DRIVER_VERSION="84.0.4147.30"
ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}"
ENV K3D_VERSION="1.3.4"
##
# Add support for stretch-backports
......@@ -48,12 +47,6 @@ RUN wget -q "https://chromedriver.storage.googleapis.com/${CHROME_DRIVER_VERSION
RUN unzip chromedriver_linux64.zip -d /usr/local/bin
RUN rm -f chromedriver_linux64.zip
##
# Install K3d local cluster support
# https://github.com/rancher/k3d
#
RUN curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | TAG="v${K3D_VERSION}" bash
##
# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s
# clusters
......
......@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe 'Issues > Labels bulk assignment' do
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:frontend) { create(:label, project: project, title: 'frontend') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
let!(:issue1) { create(:issue, project: project, title: "Issue 1", labels: [frontend]) }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
context 'as an allowed user', :js do
before do
......@@ -51,20 +52,55 @@ RSpec.describe 'Issues > Labels bulk assignment' do
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'frontend'
expect(find("#issue_#{issue2.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
context 'to a issue' do
context 'to some issues' do
before do
check "selected_issue_#{issue1.id}"
check "selected_issue_#{issue2.id}"
open_labels_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'frontend'
expect(find("#issue_#{issue2.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
context 'to an issue' do
before do
check "selected_issue_#{issue1.id}"
open_labels_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'frontend'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
context 'to an issue by selecting the label first' do
before do
open_labels_dropdown ['bug']
check "selected_issue_#{issue1.id}"
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'frontend'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
end
......
......@@ -20,14 +20,10 @@ RSpec.describe 'Filter issues', :js do
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
def expect_no_issues_list
page.within '.issues-list' do
expect(page).to have_no_selector('.issue')
end
expect(page).to have_no_selector('.issue')
end
before do
stub_feature_flags(vue_issuables_list: false)
project.add_maintainer(user)
create(:issue, project: project, author: user2, title: "Bug report 1")
......@@ -90,7 +86,7 @@ RSpec.describe 'Filter issues', :js do
end
it 'does not have the != option' do
input_filtered_search("label:", submit: false)
input_filtered_search("label:", submit: false, extra_space: false)
wait_for_requests
within('#js-dropdown-operator') do
......@@ -346,7 +342,7 @@ RSpec.describe 'Filter issues', :js do
context 'issue label clicked' do
it 'filters and displays in search bar' do
find('.issues-list .issue .issuable-main-info .issuable-info a .gl-label-text', text: multiple_words_label.title).click
find('[data-qa-selector="issuable-label"]', text: multiple_words_label.title).click
expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
......
......@@ -13,12 +13,8 @@ RSpec.describe 'Issue prioritization' do
let(:label_4) { create(:label, title: 'label_4', project: project, priority: 4) }
let(:label_5) { create(:label, title: 'label_5', project: project) } # no priority
before do
stub_feature_flags(vue_issuables_list: false)
end
# According to https://gitlab.com/gitlab-org/gitlab-foss/issues/14189#note_4360653
context 'when issues have one label' do
context 'when issues have one label', :js do
it 'Are sorted properly' do
# Issues
issue_1 = create(:issue, title: 'issue_1', project: project)
......@@ -48,7 +44,7 @@ RSpec.describe 'Issue prioritization' do
end
end
context 'when issues have multiple labels' do
context 'when issues have multiple labels', :js do
it 'Are sorted properly' do
# Issues
issue_1 = create(:issue, title: 'issue_1', project: project)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ResourceStateEventFinder do
let_it_be(:user) { create(:user) }
describe '#execute' do
subject { described_class.new(user, issue).execute }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let!(:event) { create(:resource_state_event, issue: issue) }
it 'returns events accessible by user' do
project.add_guest(user)
expect(subject).to eq [event]
end
context 'when issues are private' do
let(:project) { create(:project, :public, :issues_private) }
it 'does not return any events' do
expect(subject).to be_empty
end
end
context 'when issue is not accesible to the user' do
let(:project) { create(:project, :private) }
it 'does not return any events' do
expect(subject).to be_empty
end
end
end
describe '#can_read_eventable?' do
let(:project) { create(:project, :private) }
subject { described_class.new(user, eventable).can_read_eventable? }
context 'when eventable is an Issue' do
let(:eventable) { create(:issue, project: project) }
context 'when issue is readable' do
before do
project.add_developer(user)
end
it { is_expected.to be_truthy }
end
context 'when issue is not readable' do
it { is_expected.to be_falsey }
end
end
context 'when eventable is a MergeRequest' do
let(:eventable) { create(:merge_request, source_project: project) }
context 'when merge request is readable' do
before do
project.add_developer(user)
end
it { is_expected.to be_truthy }
end
context 'when merge request is not readable' do
it { is_expected.to be_falsey }
end
end
end
end
......@@ -303,7 +303,7 @@ describe('Issuables list component', () => {
describe('when page is not present in params', () => {
const query =
'?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0';
'?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
beforeEach(() => {
setUrl(query);
......@@ -320,7 +320,11 @@ describe('Issuables list component', () => {
it('applies filters and sorts', () => {
expect(wrapper.vm.hasFilters).toBe(true);
expect(wrapper.vm.filters).toEqual(expectedFilters);
expect(wrapper.vm.filters).toEqual({
...expectedFilters,
'not[milestone]': ['13'],
'not[labels]': ['Afterpod'],
});
expect(apiSpy).toHaveBeenCalledWith(
expect.objectContaining({
......@@ -329,6 +333,8 @@ describe('Issuables list component', () => {
with_labels_details: true,
page: 1,
per_page: PAGE_SIZE,
'not[milestone]': ['13'],
'not[labels]': ['Afterpod'],
},
}),
);
......
......@@ -872,6 +872,11 @@ describe('Monitoring store actions', () => {
state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/dashboard_with_warnings.yml';
// testAction doesn't have access to getters. The state is passed in as getters
// instead of the actual getters inside the testAction method implementation.
// All methods downstream that needs access to getters will throw and error.
// For that reason, the result of the getter is set as a state variable.
state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
mockMutate = jest.spyOn(gqClient, 'mutate');
mutationVariables = {
......@@ -879,7 +884,7 @@ describe('Monitoring store actions', () => {
variables: {
projectPath: state.projectPath,
environmentName: state.currentEnvironmentName,
dashboardPath: state.currentDashboard,
dashboardPath: state.fullDashboardPath,
},
};
});
......
......@@ -10,15 +10,41 @@ describe('HTMLToMarkdownRenderer', () => {
trim: jest.fn(input => `trimmed ${input}`),
getSpaceCollapsedText: jest.fn(input => `space collapsed ${input}`),
getSpaceControlled: jest.fn(input => `space controlled ${input}`),
convert: jest.fn(),
};
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
});
describe('TEXT_NODE visitor', () => {
it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe(
`space controlled trimmed space collapsed ${NODE.nodeValue}`,
);
});
});
describe('LI OL, LI UL visitor', () => {
const oneLevelNestedList = '\n * List item 1\n * List item 2';
const twoLevelNestedList = '\n * List item 1\n * List item 2';
const spaceInContentList = '\n * List item 1\n * List item 2';
it.each`
list | indentSpaces | result
${oneLevelNestedList} | ${2} | ${'\n * List item 1\n * List item 2'}
${oneLevelNestedList} | ${3} | ${'\n * List item 1\n * List item 2'}
${oneLevelNestedList} | ${6} | ${'\n * List item 1\n * List item 2'}
${twoLevelNestedList} | ${4} | ${'\n * List item 1\n * List item 2'}
${spaceInContentList} | ${1} | ${'\n * List item 1\n * List item 2'}
`('changes the list indentation to $indentSpaces spaces', ({ list, indentSpaces, result }) => {
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
subListIndentSpaces: indentSpaces,
});
baseRenderer.convert.mockReturnValueOnce(list);
expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result);
expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list);
});
});
});
......@@ -18,7 +18,7 @@ const buildMockUneditableOpenToken = type => {
tagName: type,
attributes: { contenteditable: false },
classNames: [
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
],
};
};
......
......@@ -324,6 +324,12 @@ RSpec.describe Gitlab::Json do
end
it_behaves_like "json"
describe "#enable_oj?" do
it "returns true" do
expect(subject.enable_oj?).to be(true)
end
end
end
context "json gem" do
......@@ -332,5 +338,73 @@ RSpec.describe Gitlab::Json do
end
it_behaves_like "json"
describe "#enable_oj?" do
it "returns false" do
expect(subject.enable_oj?).to be(false)
end
end
end
describe Gitlab::Json::GrapeFormatter do
subject { described_class.call(obj, env) }
let(:obj) { { test: true } }
let(:env) { {} }
let(:result) { "{\"test\":true}" }
context "oj is enabled" do
before do
stub_feature_flags(oj_json: true)
end
context "grape_gitlab_json flag is enabled" do
before do
stub_feature_flags(grape_gitlab_json: true)
end
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Gitlab::Json" do
expect(Gitlab::Json).to receive(:dump).with(obj)
subject
end
end
context "grape_gitlab_json flag is disabled" do
before do
stub_feature_flags(grape_gitlab_json: false)
end
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Grape::Formatter::Json" do
expect(Grape::Formatter::Json).to receive(:call).with(obj, env)
subject
end
end
end
context "oj is disabled" do
before do
stub_feature_flags(oj_json: false)
end
it "generates JSON" do
expect(subject).to eq(result)
end
it "uses Grape::Formatter::Json" do
expect(Grape::Formatter::Json).to receive(:call).with(obj, env)
subject
end
end
end
end
......@@ -20,7 +20,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
it 'clears memoized values' do
values = %i(issue_minimum_id issue_maximum_id
user_minimum_id user_maximum_id unique_visit_service
deployment_minimum_id deployment_maximum_id)
deployment_minimum_id deployment_maximum_id
approval_merge_request_rule_minimum_id
approval_merge_request_rule_maximum_id)
values.each do |key|
expect(described_class).to receive(:clear_memoization).with(key)
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::ResourceStateEvents do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) }
before_all do
project.add_developer(user)
end
shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name|
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do
let!(:event) { create_event }
it "returns an array of resource state events" do
url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events"
get api(url, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(event.id)
expect(json_response.first['state']).to eq(event.state.to_s)
end
it "returns a 404 error when eventable id not found" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 when not authorized" do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
private_user = create(:user)
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do
let!(:event) { create_event }
it "returns a resource state event by id" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(event.id)
expect(json_response['state']).to eq(event.state.to_s)
end
it "returns 404 when not authorized" do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
private_user = create(:user)
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns a 404 error if resource state event not found" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'pagination' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/220192
it 'returns the second page' do
create_event
event2 = create_event
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq '2'
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(event2.id)
end
end
def create_event(state: :opened)
create(:resource_state_event, eventable.class.name.underscore => eventable, state: state)
end
end
context 'when eventable is an Issue' do
it_behaves_like 'resource_state_events API', 'projects', 'issues', 'iid' do
let(:parent) { project }
let(:eventable) { create(:issue, project: project, author: user) }
end
end
context 'when eventable is a Merge Request' do
it_behaves_like 'resource_state_events API', 'projects', 'merge_requests', 'iid' do
let(:parent) { project }
let(:eventable) { create(:merge_request, source_project: project, target_project: project, author: user) }
end
end
end
......@@ -36,10 +36,18 @@ RSpec.describe Metrics::Dashboard::ClusterDashboardService, :use_clean_rails_mem
describe '#get_dashboard' do
let(:service_params) { [project, user, { cluster: cluster, cluster_type: :project }] }
let(:service_call) { described_class.new(*service_params).get_dashboard }
let(:service_call) { subject.get_dashboard }
subject { described_class.new(*service_params) }
it_behaves_like 'valid dashboard service response'
it_behaves_like 'caches the unprocessed dashboard for subsequent calls'
it_behaves_like 'refreshes cache when dashboard_version is changed'
it_behaves_like 'dashboard_version contains SHA256 hash of dashboard file content' do
let(:dashboard_path) { described_class::DASHBOARD_PATH }
let(:dashboard_version) { subject.send(:dashboard_version) }
end
context 'when called with a non-system dashboard' do
let(:dashboard_path) { 'garbage/dashboard/path' }
......
......@@ -47,6 +47,11 @@ RSpec.describe Metrics::Dashboard::PodDashboardService, :use_clean_rails_memory_
it_behaves_like 'valid dashboard service response'
it_behaves_like 'caches the unprocessed dashboard for subsequent calls'
it_behaves_like 'refreshes cache when dashboard_version is changed'
it_behaves_like 'updates gitlab_metrics_dashboard_processing_time_ms metric'
it_behaves_like 'dashboard_version contains SHA256 hash of dashboard file content' do
let(:dashboard_version) { subject.send(:dashboard_version) }
end
end
end
......@@ -32,7 +32,13 @@ RSpec.describe Metrics::Dashboard::SelfMonitoringDashboardService, :use_clean_ra
it_behaves_like 'valid dashboard service response'
it_behaves_like 'raises error for users with insufficient permissions'
it_behaves_like 'caches the unprocessed dashboard for subsequent calls'
it_behaves_like 'refreshes cache when dashboard_version is changed'
it_behaves_like 'updates gitlab_metrics_dashboard_processing_time_ms metric'
it_behaves_like 'dashboard_version contains SHA256 hash of dashboard file content' do
let(:dashboard_path) { described_class::DASHBOARD_PATH }
let(:dashboard_version) { subject.send(:dashboard_version) }
end
end
describe '.all_dashboard_paths' do
......
......@@ -28,8 +28,13 @@ RSpec.describe Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memo
it_behaves_like 'valid dashboard service response'
it_behaves_like 'raises error for users with insufficient permissions'
it_behaves_like 'caches the unprocessed dashboard for subsequent calls'
it_behaves_like 'refreshes cache when dashboard_version is changed'
it_behaves_like 'updates gitlab_metrics_dashboard_processing_time_ms metric'
it_behaves_like 'dashboard_version contains SHA256 hash of dashboard file content' do
let(:dashboard_version) { subject.send(:dashboard_version) }
end
context 'when called with a non-system dashboard' do
let(:dashboard_path) { 'garbage/dashboard/path' }
......
......@@ -676,10 +676,6 @@ RSpec.describe Projects::CreateService, '#execute' do
end
it 'updates authorization for current_user' do
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user).and_call_original
)
project = create_project(user, opts)
expect(
......@@ -711,10 +707,6 @@ RSpec.describe Projects::CreateService, '#execute' do
end
it 'updates authorization for current_user' do
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user).and_call_original
)
project = create_project(user, opts)
expect(
......
......@@ -45,9 +45,8 @@ module FilteredSearchHelpers
all_count = open_count + closed_count
expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: open_count)
end
expect(page).to have_selector('.issue', count: open_count)
end
# Enables input to be added character by character
......
......@@ -101,6 +101,16 @@ RSpec.shared_examples 'a resource event for issues' do
expect(events).to be_empty
end
end
if described_class.method_defined?(:issuable)
describe '#issuable' do
let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) }
it 'returns the expected issuable' do
expect(event1.issuable).to eq(issue2)
end
end
end
end
RSpec.shared_examples 'a resource event for merge requests' do
......@@ -132,4 +142,14 @@ RSpec.shared_examples 'a resource event for merge requests' do
expect(events).to be_empty
end
end
if described_class.method_defined?(:issuable)
describe '#issuable' do
let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) }
it 'returns the expected issuable' do
expect(event1.issuable).to eq(merge_request2)
end
end
end
end
......@@ -39,6 +39,33 @@ RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do
end
end
# This spec is applicable for predefined/out-of-the-box dashboard services.
RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do
specify do
allow_next_instance_of(described_class) do |service|
allow(service).to receive(:dashboard_version).and_return('1', '2')
end
expect(File).to receive(:read).twice.and_call_original
service = described_class.new(*service_params)
service.get_dashboard
service.get_dashboard
end
end
# This spec is applicable for predefined/out-of-the-box dashboard services.
# This shared_example requires the following variables to be defined:
# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml'
# dashboard_version: The version string used in the cache_key.
RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do
specify do
dashboard = File.read(Rails.root.join(dashboard_path))
expect(Digest::SHA256.hexdigest(dashboard)).to eq(dashboard_version)
end
end
RSpec.shared_examples 'valid embedded dashboard service response' do
let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
......
......@@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905"
integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg==
"@gitlab/ui@17.31.0":
version "17.31.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.31.0.tgz#928e4eb395b610a913ad37ddb1bff0dfca5fa0b0"
integrity sha512-97tmfUtSbicU2ZJKX3oDUHJY6CPgBXITf2394xOz9g6ULfO0sw0m2iTG7jY+NgwpRLC29Sxy/7ePE+8WsbbOIg==
"@gitlab/ui@17.33.0":
version "17.33.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.33.0.tgz#d0b3e8bf268fe9a28e97a6aa71b5a02059078206"
integrity sha512-WszO9zRhO5EkbLrpujPMDgrHIymkSGXRRwg32TzqASSrl4WymdgSpGYVXdTu95c1n3n7pngkjOEdh/90aM89FQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册