Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-06 09:12:58 +00:00
parent 5947e68ac3
commit 6431ee6152
26 changed files with 837 additions and 202 deletions

View File

@ -0,0 +1,10 @@
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN = '[[';
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN = 'TOC';
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN = ']]';
export const TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN = '[TOC]';
export const MDAST_TEXT_NODE = 'text';
export const MDAST_EMPHASIS_NODE = 'emphasis';
export const MDAST_PARAGRAPH_NODE = 'paragraph';
export const GLFM_TABLE_OF_CONTENTS_NODE = 'tableOfContents';

View File

@ -0,0 +1,85 @@
import { first, last } from 'lodash';
import { u } from 'unist-builder';
import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents';
import {
TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN,
TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN,
TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN,
TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN,
MDAST_TEXT_NODE,
MDAST_EMPHASIS_NODE,
MDAST_PARAGRAPH_NODE,
GLFM_TABLE_OF_CONTENTS_NODE,
} from '../constants';
const isTOCTextNode = ({ type, value }) =>
type === MDAST_TEXT_NODE && value === TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN;
const isTOCEmphasisNode = ({ type, children }) =>
type === MDAST_EMPHASIS_NODE && children.length === 1 && isTOCTextNode(first(children));
const isTOCDoubleSquareBracketOpenTokenTextNode = ({ type, value }) =>
type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN;
const isTOCDoubleSquareBracketCloseTokenTextNode = ({ type, value }) =>
type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN;
/*
* Detects table of contents declaration with syntax [[_TOC_]]
*/
const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => {
if (children.length !== 3) {
return false;
}
const [firstChild, middleChild, lastChild] = children;
return (
isTOCDoubleSquareBracketOpenTokenTextNode(firstChild) &&
isTOCEmphasisNode(middleChild) &&
isTOCDoubleSquareBracketCloseTokenTextNode(lastChild)
);
};
/*
* Detects table of contents declaration with syntax [TOC]
*/
const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => {
if (children.length !== 1) {
return false;
}
const [firstChild] = children;
const { type, value } = firstChild;
return type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN;
};
const isTableOfContentsNode = (node) =>
node.type === MDAST_PARAGRAPH_NODE &&
(isTableOfContentsDoubleSquareBracketSyntax(node) ||
isTableOfContentsSingleSquareBracketSyntax(node));
export default () => {
return (tree) => {
visitParents(tree, (node, ancestors) => {
const parent = last(ancestors);
if (!parent) {
return CONTINUE;
}
if (isTableOfContentsNode(node)) {
const index = parent.children.indexOf(node);
parent.children[index] = u(GLFM_TABLE_OF_CONTENTS_NODE, {
position: node.position,
});
}
return SKIP;
});
return tree;
};
};

View File

@ -6,6 +6,8 @@ import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
import remarkRehype, { all } from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import glfmTableOfContents from './glfm_extensions/table_of_contents';
import * as glfmMdastToHastHandlers from './mdast_to_hast_handlers/glfm_mdast_to_hast_handlers';
const skipFrontmatterHandler = (language) => (h, node) =>
h(node.position, 'frontmatter', { language }, [{ type: 'text', value: node.value }]);
@ -65,19 +67,22 @@ const skipRenderingHandlers = {
all(h, node),
);
},
tableOfContents: (h, node) => h(node.position, 'tableOfContents'),
toml: skipFrontmatterHandler('toml'),
yaml: skipFrontmatterHandler('yaml'),
json: skipFrontmatterHandler('json'),
};
const createParser = ({ skipRendering = [] }) => {
const createParser = ({ skipRendering }) => {
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkFrontmatter, ['yaml', 'toml', { type: 'json', marker: ';' }])
.use(glfmTableOfContents)
.use(remarkRehype, {
allowDangerousHtml: true,
handlers: {
...glfmMdastToHastHandlers,
...pick(skipRenderingHandlers, skipRendering),
},
})
@ -99,13 +104,13 @@ const compilerFactory = (renderer) =>
* tree in any desired representation
*
* @param {String} params.markdown Markdown to parse
* @param {(tree: MDast -> any)} params.renderer A function that accepts mdast
* @param {Function} params.renderer A function that accepts mdast
* AST tree and returns an object of any type that represents the result of
* rendering the tree. See the references below to for more information
* about MDast.
*
* MDastTree documentation https://github.com/syntax-tree/mdast
* @returns {Promise<any>} Returns a promise with the result of rendering
* @returns {Promise} Returns a promise with the result of rendering
* the MDast tree
*/
export const render = async ({ markdown, renderer, skipRendering = [] }) => {

View File

@ -0,0 +1 @@
export const tableOfContents = (h, node) => h(node.position, 'nav');

View File

@ -7,6 +7,7 @@ import Poll from '~/lib/utils/poll';
import StatusIcon from '../extensions/status_icon.vue';
import ActionButtons from '../action_buttons.vue';
import { EXTENSION_ICONS } from '../../constants';
import ContentSection from './widget_content_section.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@ -17,6 +18,7 @@ export default {
StatusIcon,
GlButton,
GlLoadingIcon,
ContentSection,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -92,15 +94,16 @@ export default {
isCollapsed: true,
isLoading: false,
isLoadingExpandedContent: false,
error: null,
summaryError: null,
contentError: null,
};
},
computed: {
collapseButtonLabel() {
return sprintf(this.isCollapsed ? __('Show details') : __('Hide details'));
},
statusIcon() {
return this.error ? EXTENSION_ICONS.failed : this.statusIconName;
summaryStatusIcon() {
return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName;
},
},
watch: {
@ -114,7 +117,7 @@ export default {
try {
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
} catch {
this.error = this.errorText;
this.summaryError = this.errorText;
}
this.isLoading = false;
@ -130,12 +133,12 @@ export default {
},
async fetchExpandedContent() {
this.isLoadingExpandedContent = true;
this.error = null;
this.contentError = null;
try {
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
} catch {
this.error = this.errorText;
this.contentError = this.errorText;
// Reset these values so that we allow refetching
this.isExpandedForTheFirstTime = true;
@ -178,20 +181,26 @@ export default {
});
},
},
failedStatusIcon: EXTENSION_ICONS.failed,
};
</script>
<template>
<section class="media-section" data-testid="widget-extension">
<div class="media gl-p-5">
<status-icon :level="1" :name="widgetName" :is-loading="isLoading" :icon-name="statusIcon" />
<status-icon
:level="1"
:name="widgetName"
:is-loading="isLoading"
:icon-name="summaryStatusIcon"
/>
<div
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
>
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
<span v-else>{{ error }}</span>
<span v-if="summaryError">{{ summaryError }}</span>
<slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot>
</div>
<action-buttons
v-if="actionButtons.length > 0"
@ -217,14 +226,24 @@ export default {
</div>
</div>
<div
v-if="!isCollapsed"
v-if="!isCollapsed || contentError"
class="mr-widget-grouped-section gl-relative"
data-testid="widget-extension-collapsed-section"
>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
<slot v-else name="content">{{ content }}</slot>
<content-section
v-else-if="contentError"
class="report-block-container"
:status-icon-name="$options.failedStatusIcon"
:widget-name="widgetName"
>
{{ contentError }}
</content-section>
<slot v-else name="content">
{{ content }}
</slot>
</div>
</section>
</template>

View File

@ -0,0 +1,35 @@
<script>
import { EXTENSION_ICONS } from '../../constants';
import StatusIcon from '../extensions/status_icon.vue';
export default {
components: {
StatusIcon,
},
props: {
statusIconName: {
type: String,
default: '',
required: false,
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
},
widgetName: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="gl-px-7">
<div class="gl-pl-4 gl-display-flex">
<status-icon
v-if="statusIconName"
:level="2"
:name="widgetName"
:icon-name="statusIconName"
/>
<slot name="default"></slot>
</div>
</div>
</template>

View File

@ -142,6 +142,16 @@ module TodosHelper
todos_filter_params.values.none?
end
def no_todos_messages
[
s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
s_('Todos|Isn\'t an empty To-Do List beautiful?'),
s_('Todos|Give yourself a pat on the back!'),
s_('Todos|Nothing left to do. High five!'),
s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"')
]
end
def todos_filter_path(options = {})
without = options.delete(:without)

View File

@ -93,7 +93,7 @@
.text-content.gl-text-center
- if todos_filter_empty?
%h4
= Gitlab.config.gitlab.no_todos_messages.sample
= no_todos_messages.sample
%p
= (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe
- else

View File

@ -214,7 +214,6 @@ Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['content_security_policy'] ||= {}
Settings.gitlab['allowed_hosts'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
Settings.gitlab['max_request_duration_seconds'] ||= 57

View File

@ -0,0 +1,23 @@
---
key_path: usage_activity_by_stage.manage.count_user_auth
description: Number of unique user logins
product_section: dev
product_stage: manage
product_group: authentication_and_authorization
product_category: system_access
value_type: number
status: active
milestone: "15.4"
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96321"
time_frame: all
data_source: database
instrumentation_class: CountUserAuthMetric
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -1,11 +0,0 @@
# When the todo list on the user's dashboard becomes empty, a random message
# from the list below will be shown.
#
# If you come up with a fun one, please feel free to contribute it to GitLab!
# https://about.gitlab.com/contributing/
---
- Good job! Looks like you don't have anything left on your To-Do List
- Isn't an empty To-Do List beautiful?
- Give yourself a pat on the back!
- Nothing left to do. High five!
- Henceforth, you shall be known as "To-Do Destroyer"

View File

@ -1157,16 +1157,13 @@ A site profile contains:
- **Target URL**: The URL that DAST runs against.
- **Excluded URLs**: A comma-separated list of URLs to exclude from the scan.
- **Request headers**: A comma-separated list of HTTP request headers, including names and values. These headers are added to every request made by DAST.
- **Authentication (for website)**:
- **Authentication**:
- **Authenticated URL**: The URL of the page containing the sign-in HTML form on the target website. The username and password are submitted with the login form to create an authenticated scan.
- **Username**: The username used to authenticate to the website.
- **Password**: The password used to authenticate to the website.
- **Username form field**: The name of username field at the sign-in HTML form.
- **Password form field**: The name of password field at the sign-in HTML form.
- **Submit form field**: The `id` or `name` of the element that when clicked submits the sign-in HTML form.
- **Authentication (for API scan)**:
- **Username**: The username used to authenticate to the API.
- **Password**: The password used to authenticate to the API.
When an API site type is selected, a [host override](#host-override) is used to ensure the API being scanned is on the same host as the target. This is done to reduce the risk of running an active scan against the wrong API.

View File

@ -212,7 +212,17 @@ module API
recheck_mergeability_of(merge_requests: merge_requests) unless options[:skip_merge_status_recheck]
present_cached merge_requests, expires_in: 8.hours, cache_context: -> (mr) { "#{current_user&.cache_key}:#{mr.merge_status}" }, **options
present_cached merge_requests,
expires_in: 8.hours,
cache_context: -> (mr) do
[
current_user&.cache_key,
mr.merge_status,
mr.merge_request_assignees.map(&:cache_key),
mr.merge_request_reviewers.map(&:cache_key)
].join(":")
end,
**options
end
desc 'Create a merge request' do

View File

@ -0,0 +1,112 @@
# frozen_string_literal: true
module Gitlab
module Analytics
# This class generates a date => value hash without gaps in the data points.
#
# Simple usage:
#
# > # We have the following data for the last 5 day:
# > input = { 3.days.ago.to_date => 10, Date.today => 5 }
#
# > # Format this data, so we can chart the complete date range:
# > Gitlab::Analytics::DateFiller.new(input, from: 4.days.ago, to: Date.today, default_value: 0).fill
# > {
# > Sun, 28 Aug 2022=>0,
# > Mon, 29 Aug 2022=>10,
# > Tue, 30 Aug 2022=>0,
# > Wed, 31 Aug 2022=>0,
# > Thu, 01 Sep 2022=>5
# > }
#
# Parameters:
#
# **input**
# A Hash containing data for the series or the chart. The key is a Date object
# or an object which can be converted to Date.
#
# **from**
# Start date of the range
#
# **to**
# End date of the range
#
# **period**
# Specifies the period in wich the dates should be generated. Options:
#
# - :day, generate date-value pair for each day in the given period
# - :week, generate date-value pair for each week (beginning of the week date)
# - :month, generate date-value pair for each week (beginning of the month date)
#
# Note: the Date objects in the `input` should follow the same pattern (beginning of ...)
#
# **default_value**
#
# Which value use when the `input` Hash does not contain data for the given day.
#
# **date_formatter**
#
# How to format the dates in the resulting hash.
class DateFiller
DEFAULT_DATE_FORMATTER = -> (date) { date }
PERIOD_STEPS = {
day: 1.day,
week: 1.week,
month: 1.month
}.freeze
def initialize(
input,
from:,
to:,
period: :day,
default_value: nil,
date_formatter: DEFAULT_DATE_FORMATTER)
@input = input.transform_keys(&:to_date)
@from = from.to_date
@to = to.to_date
@period = period
@default_value = default_value
@date_formatter = date_formatter
end
def fill
data = {}
current_date = from
loop do
transformed_date = transform_date(current_date)
break if transformed_date > to
formatted_date = date_formatter.call(transformed_date)
value = input.delete(transformed_date)
data[formatted_date] = value.nil? ? default_value : value
current_date = (current_date + PERIOD_STEPS.fetch(period)).to_date
end
raise "Input contains values which doesn't fall under the given period!" if input.any?
data
end
private
attr_reader :input, :from, :to, :period, :default_value, :date_formatter
def transform_date(date)
case period
when :day
date.beginning_of_day.to_date
when :week
date.beginning_of_week.to_date
when :month
date.beginning_of_month.to_date
else
raise "Unknown period given: #{period}"
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class CountUserAuthMetric < DatabaseMetric
operation :distinct_count, column: :user_id
relation do
AuthenticationEvent.success
end
end
end
end
end
end

View File

@ -41117,6 +41117,18 @@ msgstr ""
msgid "Todos|Filter by project"
msgstr ""
msgid "Todos|Give yourself a pat on the back!"
msgstr ""
msgid "Todos|Good job! Looks like you don't have anything left on your To-Do List"
msgstr ""
msgid "Todos|Henceforth, you shall be known as \"To-Do Destroyer\""
msgstr ""
msgid "Todos|Isn't an empty To-Do List beautiful?"
msgstr ""
msgid "Todos|It's how you always know what to work on next."
msgstr ""
@ -41126,6 +41138,9 @@ msgstr ""
msgid "Todos|Nothing is on your to-do list. Nice work!"
msgstr ""
msgid "Todos|Nothing left to do. High five!"
msgstr ""
msgid "Todos|Undo mark all as done"
msgstr ""

View File

@ -172,6 +172,7 @@
"three": "^0.143.0",
"timeago.js": "^4.0.2",
"unified": "^10.1.2",
"unist-builder": "^3.0.0",
"unist-util-visit-parents": "^5.1.0",
"url-loader": "^4.1.1",
"uuid": "8.1.0",

View File

@ -24,12 +24,6 @@ describe('gfm', () => {
};
describe('render', () => {
it('processes Commonmark and provides an ast to the renderer function', async () => {
const result = await markdownToAST('This is text');
expect(result.type).toBe('root');
});
it('transforms raw HTML into individual nodes in the AST', async () => {
const result = await markdownToAST('<strong>This is bold text</strong>');
@ -46,216 +40,270 @@ describe('gfm', () => {
);
});
it('returns the result of executing the renderer function', async () => {
const rendered = { value: 'rendered tree' };
describe('with custom renderer', () => {
it('processes Commonmark and provides an ast to the renderer function', async () => {
const result = await markdownToAST('This is text');
const result = await render({
markdown: '<strong>This is bold text</strong>',
renderer: () => {
return rendered;
},
expect(result.type).toBe('root');
});
expect(result).toEqual(rendered);
it('returns the result of executing the renderer function', async () => {
const rendered = { value: 'rendered tree' };
const result = await render({
markdown: '<strong>This is bold text</strong>',
renderer: () => {
return rendered;
},
});
expect(result).toEqual(rendered);
});
});
describe('when skipping the rendering of footnote reference and definition nodes', () => {
it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
const result = await markdownToAST(
`footnote reference [^footnote]
describe('footnote references and footnote definitions', () => {
describe('when skipping the rendering of footnote reference and definition nodes', () => {
it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
const result = await markdownToAST(
`footnote reference [^footnote]
[^footnote]: Footnote definition`,
['footnoteReference', 'footnoteDefinition'],
);
['footnoteReference', 'footnoteDefinition'],
);
expectInRoot(
result,
expect.objectContaining({
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'footnotereference',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
]),
}),
);
expectInRoot(
result,
expect.objectContaining({
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'footnotereference',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
]),
}),
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'footnotedefinition',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'footnotedefinition',
properties: {
identifier: 'footnote',
label: 'footnote',
},
}),
);
});
});
});
describe('when skipping the rendering of code blocks', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
describe('code blocks', () => {
describe('when skipping the rendering of code blocks', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
\`\`\`javascript
console.log('Hola');
\`\`\`\
`,
['code'],
);
['code'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'codeblock',
properties: {
language: 'javascript',
},
}),
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'codeblock',
properties: {
language: 'javascript',
},
}),
);
});
});
});
describe('when skipping the rendering of reference definitions', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
describe('reference definitions', () => {
describe('when skipping the rendering of reference definitions', () => {
it('transforms code nodes into codeblock html tags', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab]
[gitlab]: https://gitlab.com "GitLab"
`,
['definition'],
);
['definition'],
);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'referencedefinition',
properties: {
identifier: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com',
},
children: [
{
type: 'text',
value: '[gitlab]: https://gitlab.com "GitLab"',
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'referencedefinition',
properties: {
identifier: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com',
},
],
}),
);
children: [
{
type: 'text',
value: '[gitlab]: https://gitlab.com "GitLab"',
},
],
}),
);
});
});
});
describe('when skipping the rendering of link and image references', () => {
it('transforms linkReference and imageReference nodes into html tags', async () => {
const result = await markdownToAST(
`
describe('link and image references', () => {
describe('when skipping the rendering of link and image references', () => {
it('transforms linkReference and imageReference nodes into html tags', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab] and ![GitLab Logo][gitlab-logo]
[gitlab]: https://gitlab.com "GitLab"
[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
`,
['linkReference', 'imageReference'],
);
['linkReference', 'imageReference'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: 'https://gitlab.com',
isReference: 'true',
identifier: 'gitlab',
title: 'GitLab',
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: 'https://gitlab.com',
isReference: 'true',
identifier: 'gitlab',
title: 'GitLab',
}),
}),
}),
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: 'https://gitlab.com/gitlab-logo.png',
isReference: 'true',
identifier: 'gitlab-logo',
title: 'GitLab Logo',
alt: 'GitLab Logo',
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: 'https://gitlab.com/gitlab-logo.png',
isReference: 'true',
identifier: 'gitlab-logo',
title: 'GitLab Logo',
alt: 'GitLab Logo',
}),
}),
}),
]),
}),
);
});
]),
}),
);
});
it('normalizes the urls extracted from the reference definitions', async () => {
const result = await markdownToAST(
`
it('normalizes the urls extracted from the reference definitions', async () => {
const result = await markdownToAST(
`
[gitlab][gitlab] and ![GitLab Logo][gitlab]
[gitlab]: /url\\bar*baz
`,
['linkReference', 'imageReference'],
);
['linkReference', 'imageReference'],
);
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: '/url%5Cbar*baz',
}),
}),
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: '/url%5Cbar*baz',
}),
}),
]),
}),
);
});
});
});
describe('frontmatter', () => {
describe('when skipping the rendering of frontmatter types', () => {
it.each`
type | input
${'yaml'} | ${'---\ntitle: page\n---'}
${'toml'} | ${'+++\ntitle: page\n+++'}
${'json'} | ${';;;\ntitle: page\n;;;'}
`('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
const result = await markdownToAST(input, [type]);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'frontmatter',
properties: {
language: type,
},
children: [
{
type: 'text',
value: 'title: page',
},
],
}),
);
});
});
});
describe('table of contents', () => {
it.each`
markdown
${'[[_TOC_]]'}
${' [[_TOC_]]'}
${'[[_TOC_]] '}
${'[TOC]'}
${' [TOC]'}
${'[TOC] '}
`('parses $markdown and produces a table of contents section', async ({ markdown }) => {
const result = await markdownToAST(markdown);
expectInRoot(
result,
expect.objectContaining({
tagName: 'p',
children: expect.arrayContaining([
expect.objectContaining({
type: 'element',
tagName: 'a',
properties: expect.objectContaining({
href: '/url%5Cbar*baz',
}),
}),
expect.objectContaining({
type: 'element',
tagName: 'img',
properties: expect.objectContaining({
src: '/url%5Cbar*baz',
}),
}),
]),
type: 'element',
tagName: 'nav',
}),
);
});
});
describe('when skipping the rendering of table of contents', () => {
it('transforms table of contents nodes into html tableofcontents tags', async () => {
const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'tableofcontents',
}),
);
});
});
});
describe('when skipping the rendering of frontmatter types', () => {
it.each`
type | input
${'yaml'} | ${'---\ntitle: page\n---'}
${'toml'} | ${'+++\ntitle: page\n+++'}
${'json'} | ${';;;\ntitle: page\n;;;'}
`('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
const result = await markdownToAST(input, [type]);
expectInRoot(
result,
expect.objectContaining({
type: 'element',
tagName: 'frontmatter',
properties: {
language: type,
},
children: [
{
type: 'text',
value: 'title: page',
},
],
}),
);
});
});
});

View File

@ -0,0 +1,39 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => {
let wrapper;
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(WidgetContentSection, {
propsData: {
widgetName: 'MyWidget',
...propsData,
},
slots,
});
};
it('does not render the status icon when it is not provided', () => {
createComponent();
expect(findStatusIcon().exists()).toBe(false);
});
it('renders the status icon when provided', () => {
createComponent({ propsData: { statusIconName: 'failed' } });
expect(findStatusIcon().exists()).toBe(true);
});
it('renders the default slot', () => {
createComponent({
slots: {
default: 'Hello world',
},
});
expect(wrapper.findByText('Hello world').exists()).toBe(true);
});
});

View File

@ -43,7 +43,7 @@ describe('MR Widget', () => {
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
expect(fetchCollapsedData).toHaveBeenCalled();
expect(wrapper.vm.error).toBe(null);
expect(wrapper.vm.summaryError).toBe(null);
});
it('sets the error text when fetch method fails', async () => {

View File

@ -258,6 +258,21 @@ RSpec.describe TodosHelper do
end
end
describe '#no_todos_messages' do
context 'when getting todos messsages' do
it 'return these sentences' do
expected_sentences = [
s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
s_('Todos|Isn\'t an empty To-Do List beautiful?'),
s_('Todos|Give yourself a pat on the back!'),
s_('Todos|Nothing left to do. High five!'),
s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"')
]
expect(helper.no_todos_messages).to eq(expected_sentences)
end
end
end
describe '#todo_author_display?' do
using RSpec::Parameterized::TableSyntax

View File

@ -0,0 +1,136 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Analytics::DateFiller do
let(:default_value) { 0 }
let(:formatter) { Gitlab::Analytics::DateFiller::DEFAULT_DATE_FORMATTER }
subject(:filler_result) do
described_class.new(data,
from: from,
to: to,
period: period,
default_value: default_value,
date_formatter: formatter).fill.to_a
end
context 'when unknown period is given' do
it 'raises error' do
input = { 3.days.ago.to_date => 10, Date.today => 5 }
expect do
described_class.new(input, from: 4.days.ago, to: Date.today, period: :unknown).fill
end.to raise_error(/Unknown period given/)
end
end
context 'when period=:day' do
let(:from) { Date.new(2021, 5, 25) }
let(:to) { Date.new(2021, 6, 5) }
let(:period) { :day }
let(:expected_result) do
{
Date.new(2021, 5, 25) => 1,
Date.new(2021, 5, 26) => default_value,
Date.new(2021, 5, 27) => default_value,
Date.new(2021, 5, 28) => default_value,
Date.new(2021, 5, 29) => default_value,
Date.new(2021, 5, 30) => default_value,
Date.new(2021, 5, 31) => default_value,
Date.new(2021, 6, 1) => default_value,
Date.new(2021, 6, 2) => default_value,
Date.new(2021, 6, 3) => 10,
Date.new(2021, 6, 4) => default_value,
Date.new(2021, 6, 5) => default_value
}
end
let(:data) do
{
Date.new(2021, 6, 3) => 10, # deliberatly not sorted
Date.new(2021, 5, 27) => nil,
Date.new(2021, 5, 25) => 1
}
end
it { is_expected.to eq(expected_result.to_a) }
context 'when a custom default value is given' do
let(:default_value) { 'MISSING' }
it do
is_expected.to eq(expected_result.to_a)
end
end
context 'when a custom date formatter is given' do
let(:formatter) { -> (date) { date.to_s } }
it do
expected_result.transform_keys!(&:to_s)
is_expected.to eq(expected_result.to_a)
end
end
context 'when the data contains dates outside of the requested period' do
before do
data[Date.new(2022, 6, 1)] = 5
end
it 'raises error' do
expect { filler_result }.to raise_error(/Input contains values which doesn't/)
end
end
end
context 'when period=:week' do
let(:from) { Date.new(2021, 5, 16) }
let(:to) { Date.new(2021, 6, 7) }
let(:period) { :week }
let(:data) do
{
Date.new(2021, 5, 24) => nil,
Date.new(2021, 6, 7) => 10
}
end
let(:expected_result) do
{
Date.new(2021, 5, 10) => 0,
Date.new(2021, 5, 17) => 0,
Date.new(2021, 5, 24) => 0,
Date.new(2021, 5, 31) => 0,
Date.new(2021, 6, 7) => 10
}
end
it do
is_expected.to eq(expected_result.to_a)
end
end
context 'when period=:month' do
let(:from) { Date.new(2021, 5, 1) }
let(:to) { Date.new(2021, 7, 1) }
let(:period) { :month }
let(:data) do
{
Date.new(2021, 5, 1) => 100
}
end
let(:expected_result) do
{
Date.new(2021, 5, 1) => 100,
Date.new(2021, 6, 1) => 0,
Date.new(2021, 7, 1) => 0
}
end
it do
is_expected.to eq(expected_result.to_a)
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUserAuthMetric do
context 'with all time frame' do
let(:expected_value) { 2 }
before do
user = create(:user)
user2 = create(:user)
create(:authentication_event, user: user, provider: :ldapmain, result: :success)
create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success)
create(:authentication_event, user: user2, provider: :group_saml, result: :success)
create(:authentication_event, user: user2, provider: :group_saml, result: :success)
create(:authentication_event, user: user, provider: :group_saml, result: :failed)
end
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
end
context 'with 28d time frame' do
let(:expected_value) { 1 }
before do
user = create(:user)
user2 = create(:user)
create(:authentication_event, created_at: 1.year.ago, user: user, provider: :ldapmain, result: :success)
create(:authentication_event, created_at: 1.week.ago, user: user2, provider: :ldapsecondary, result: :success)
end
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }
end
end

View File

@ -37,6 +37,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:usage_activity_by_stage][:plan]).to include(:issues)
end
it 'includes usage_activity_by_stage metrics' do
expect(subject[:usage_activity_by_stage][:manage]).to include(:count_user_auth)
end
it 'includes usage_activity_by_stage_monthly keys' do
expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
end

View File

@ -215,14 +215,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
groups: 2,
users_created: 10,
omniauth_providers: ['google_oauth2'],
user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
user_auth_by_provider: {
'group_saml' => 2,
'ldap' => 4,
'standard' => 0,
'two-factor' => 0,
'two-factor-via-u2f-device' => 0,
"two-factor-via-webauthn-device" => 0
}
)
expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
events: be_within(error_rate).percent_of(2),
groups: 1,
users_created: 6,
omniauth_providers: ['google_oauth2'],
user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
user_auth_by_provider: {
'group_saml' => 1,
'ldap' => 2,
'standard' => 0,
'two-factor' => 0,
'two-factor-via-u2f-device' => 0,
"two-factor-via-webauthn-device" => 0
}
)
end

View File

@ -1023,6 +1023,22 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'a non-cached MergeRequest api request', 1
end
context 'when the assignees change' do
before do
merge_request.assignees << create(:user)
end
it_behaves_like 'a non-cached MergeRequest api request', 1
end
context 'when the reviewers change' do
before do
merge_request.reviewers << create(:user)
end
it_behaves_like 'a non-cached MergeRequest api request', 1
end
context 'when another user requests' do
before do
sign_in(user2)