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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-15 21:09:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-15 21:09:09 +0300
commitda1962d9ac710f95d350d2645c87f5a663123cf2 (patch)
tree1725ade126a9b4ae0148cd100cee94c44f9ce9f3 /spec
parente69e3f1eb695b4e852c56e7ddf8c52915ae2631b (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap112
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js32
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js8
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js8
-rw-r--r--spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap126
-rw-r--r--spec/frontend/projects/components/remove_modal_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js22
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js34
-rw-r--r--spec/haml_lint/linter/documentation_links_spec.rb82
-rw-r--r--spec/lib/api/entities/merge_request_approvals_spec.rb36
-rw-r--r--spec/lib/gitlab/danger/changelog_spec.rb40
-rw-r--r--spec/lib/gitlab/utils/markdown_spec.rb63
-rw-r--r--spec/models/concerns/approvable_base_spec.rb34
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb18
-rw-r--r--spec/requests/api/ci/runner_spec.rb27
17 files changed, 656 insertions, 115 deletions
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 65c658536a3..161c2bade05 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -10,57 +10,81 @@ exports[`Code navigation popover component renders popover 1`] = `
style="left: 0px;"
/>
- <div
- class="overflow-auto code-navigation-popover-container"
+ <gl-tabs-stub
+ contentclass="gl-py-0"
+ nav-class="gl-hidden"
+ theme="indigo"
>
- <div
- class=""
+ <gl-tab-stub
+ title="Definition"
>
- <pre
- class="border-0 bg-transparent m-0 code highlight text-wrap"
+ <div
+ class="overflow-auto code-navigation-popover-container"
>
- <span
- class="line"
- lang="javascript"
+ <div
+ class=""
>
- <span
- class="k"
+ <pre
+ class="border-0 bg-transparent m-0 code highlight text-wrap"
>
- function
- </span>
- <span>
- main() {
- </span>
- </span>
- <span
- class="line"
- lang="javascript"
+ <span
+ class="line"
+ lang="javascript"
+ >
+ <span
+ class="k"
+ >
+ function
+ </span>
+ <span>
+ main() {
+ </span>
+ </span>
+ <span
+ class="line"
+ lang="javascript"
+ >
+ <span>
+ }
+ </span>
+ </span>
+ </pre>
+ </div>
+ </div>
+
+ <div
+ class="popover-body border-top"
+ >
+ <gl-button-stub
+ category="tertiary"
+ class="w-100"
+ data-testid="go-to-definition-btn"
+ href="http://gitlab.com/test.js"
+ icon=""
+ size="medium"
+ target="_blank"
+ variant="default"
>
- <span>
- }
- </span>
- </span>
- </pre>
- </div>
- </div>
-
- <div
- class="popover-body border-top"
- >
- <gl-button-stub
- category="tertiary"
- class="w-100"
- data-testid="go-to-definition-btn"
- href="http://gitlab.com/test.js"
- icon=""
- size="medium"
- target="_blank"
- variant="default"
+
+ Go to definition
+
+ </gl-button-stub>
+ </div>
+ </gl-tab-stub>
+
+ <gl-tab-stub
+ class="py-2"
+ data-testid="references-tab"
>
+
+ <p
+ class="gl-my-4 gl-px-4"
+ >
+
+ No references found
- Go to definition
-
- </gl-button-stub>
- </div>
+ </p>
+ </gl-tab-stub>
+ </gl-tabs-stub>
</div>
`;
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index d5a72ab8af8..7b323cfab72 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -40,6 +40,17 @@ const MOCK_DOCS_DATA = Object.freeze({
definition_path: 'test.js#L20',
});
+const MOCK_DATA_WITH_REFERENCES = Object.freeze({
+ hover: [
+ {
+ language: null,
+ value: 'console.log',
+ },
+ ],
+ references: [{ path: 'index.js' }, { path: 'app.js' }],
+ definition_path: 'test.js#L20',
+});
+
let wrapper;
function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }) {
@@ -64,6 +75,16 @@ describe('Code navigation popover component', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('srender references tab with empty text when no references exist', () => {
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_CODE_DATA,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ });
+
+ expect(wrapper.find('[data-testid="references-tab"]').text()).toContain('No references found');
+ });
+
it('renders link with hash to current file', () => {
factory({
position: { x: 0, y: 0, height: 0 },
@@ -75,6 +96,17 @@ describe('Code navigation popover component', () => {
expect(wrapper.find('[data-testid="go-to-definition-btn"]').attributes('href')).toBe('#L20');
});
+ it('renders list of references', () => {
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_DATA_WITH_REFERENCES,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ });
+
+ expect(wrapper.find('[data-testid="references-tab"]').exists()).toBe(true);
+ expect(wrapper.findAll('[data-testid="reference-link"]').length).toBe(2);
+ });
+
describe('code output', () => {
it('renders code output', () => {
factory({
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 7915544814a..3d27b9c3051 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -88,17 +88,17 @@ describe('IntegrationForm', () => {
expect(findJiraTriggerFields().exists()).toBe(true);
});
- describe('featureFlag jiraIntegration is false', () => {
+ describe('featureFlag jiraIssuesIntegration is false', () => {
it('does not render JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIntegration: false });
+ createComponent({ type: 'jira' }, { jiraIssuesIntegration: false });
expect(findJiraIssuesFields().exists()).toBe(false);
});
});
- describe('featureFlag jiraIntegration is true', () => {
+ describe('featureFlag jiraIssuesIntegration is true', () => {
it('renders JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIntegration: true });
+ createComponent({ type: 'jira' }, { jiraIssuesIntegration: true });
expect(findJiraIssuesFields().exists()).toBe(true);
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 85e680fe216..e769580b587 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -595,6 +595,14 @@ describe('URL utility', () => {
);
});
+ it('handles arrays properly when railsArraySyntax=true', () => {
+ const url = 'https://gitlab.com/test';
+
+ expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toEqual(
+ 'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar',
+ );
+ });
+
it('removes all existing URL params and sets a new param when cleanParams=true', () => {
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';
diff --git a/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap b/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap
new file mode 100644
index 00000000000..4d5b6c56a34
--- /dev/null
+++ b/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap
@@ -0,0 +1,126 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Project remove modal initialized matches the snapshot 1`] = `
+<form
+ action="some/path"
+ method="post"
+>
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <b-button-stub
+ class="[object Object]"
+ event="click"
+ role="button"
+ routertag="a"
+ size="md"
+ tabindex="0"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Remove project
+ </span>
+ </b-button-stub>
+
+ <b-modal-stub
+ canceltitle="Cancel"
+ cancelvariant="secondary"
+ footerclass="bg-gray-light gl-p-5"
+ headerclosecontent="&times;"
+ headercloselabel="Close"
+ id="remove-project-modal"
+ ignoreenforcefocusselector=""
+ lazy="true"
+ modalclass="gl-modal,"
+ oktitle="OK"
+ okvariant="danger"
+ size="sm"
+ title=""
+ titletag="h4"
+ >
+
+ <div>
+ <p
+ class="gl-text-red-500 gl-font-weight-bold"
+ >
+ This can lead to data loss.
+ </p>
+
+ <p
+ class="gl-mb-0"
+ >
+ This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.
+ </p>
+
+ <p>
+ <gl-sprintf-stub
+ message="Please type %{phrase_code} to proceed or close this modal to cancel."
+ />
+ </p>
+
+ <gl-form-input-stub
+ id="confirm_name_input"
+ name="confirm_name_input"
+ type="text"
+ />
+ </div>
+
+ <template />
+
+ <template>
+ Confirmation required
+ </template>
+
+ <template />
+
+ <template />
+
+ <template />
+
+ <template>
+ <div
+ class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"
+ >
+ <b-button-stub
+ class="[object Object]"
+ disabled="true"
+ event="click"
+ routertag="a"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Confirm
+
+ </span>
+ </b-button-stub>
+ </div>
+ </template>
+ </b-modal-stub>
+</form>
+`;
diff --git a/spec/frontend/projects/components/remove_modal_spec.js b/spec/frontend/projects/components/remove_modal_spec.js
new file mode 100644
index 00000000000..339aee65b99
--- /dev/null
+++ b/spec/frontend/projects/components/remove_modal_spec.js
@@ -0,0 +1,62 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlModal } from '@gitlab/ui';
+import ProjectRemoveModal from '~/projects/components/remove_modal.vue';
+
+describe('Project remove modal', () => {
+ let wrapper;
+
+ const findFormElement = () => wrapper.find('form').element;
+ const findConfirmButton = () => wrapper.find(GlModal).find(GlButton);
+
+ const defaultProps = {
+ formPath: 'some/path',
+ confirmPhrase: 'foo',
+ warningMessage: 'This can lead to data loss.',
+ };
+
+ const createComponent = (data = {}) => {
+ wrapper = shallowMount(ProjectRemoveModal, {
+ propsData: defaultProps,
+ data: () => data,
+ stubs: {
+ GlButton,
+ GlModal,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('initialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('user input matches the confirmPhrase', () => {
+ beforeEach(() => {
+ createComponent({ userInput: defaultProps.confirmPhrase });
+ });
+
+ it('the confirm button is not dislabled', () => {
+ expect(findConfirmButton().attributes('disabled')).toBe(undefined);
+ });
+
+ describe('and when the confirmation button is clicked', () => {
+ beforeEach(() => {
+ findConfirmButton().vm.$emit('click');
+ });
+
+ it('submits the form element', () => {
+ expect(findFormElement().submit).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
index 2253db7cbd0..0007aed5c4d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
@@ -2,8 +2,9 @@ import {
buildUneditableOpenTokens,
buildUneditableCloseToken,
buildUneditableCloseTokens,
- buildUneditableInlineTokens,
buildUneditableTokens,
+ buildUneditableInlineTokens,
+ buildUneditableHtmlAsTextTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import {
@@ -12,6 +13,7 @@ import {
uneditableOpenTokens,
uneditableCloseToken,
uneditableCloseTokens,
+ uneditableBlockTokens,
uneditableInlineTokens,
uneditableTokens,
} from './mock_data';
@@ -41,6 +43,15 @@ describe('Build Uneditable Token renderer helper', () => {
});
});
+ describe('buildUneditableTokens', () => {
+ it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
+ const result = buildUneditableTokens(originToken);
+
+ expect(result).toHaveLength(3);
+ expect(result).toStrictEqual(uneditableTokens);
+ });
+ });
+
describe('buildUneditableInlineTokens', () => {
it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
const result = buildUneditableInlineTokens(originInlineToken);
@@ -50,12 +61,20 @@ describe('Build Uneditable Token renderer helper', () => {
});
});
- describe('buildUneditableTokens', () => {
- it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
- const result = buildUneditableTokens(originToken);
+ describe('buildUneditableHtmlAsTextTokens', () => {
+ it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
+ const htmlBlockNode = {
+ type: 'htmlBlock',
+ literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>',
+ };
+ const result = buildUneditableHtmlAsTextTokens(htmlBlockNode);
+ const { type, content } = result[1];
+
+ expect(type).toBe('text');
+ expect(content).not.toMatch(/ data-tomark-pass /);
expect(result).toHaveLength(3);
- expect(result).toStrictEqual(uneditableTokens);
+ expect(result).toStrictEqual(uneditableBlockTokens);
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
index 0c010a20d98..433f41774b4 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
@@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.');
// Token spec helpers
-const buildUneditableOpenToken = type => {
+const buildMockUneditableOpenToken = type => {
return {
type: 'openTag',
tagName: type,
@@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => {
};
};
-const buildUneditableCloseToken = type => {
+const buildMockUneditableCloseToken = type => {
return { type: 'closeTag', tagName: type };
};
@@ -31,8 +31,8 @@ export const originToken = {
type: 'text',
content: '{:.no_toc .hidden-md .hidden-lg}',
};
-export const uneditableCloseToken = buildUneditableCloseToken('div');
-export const uneditableOpenTokens = [buildUneditableOpenToken('div'), originToken];
+export const uneditableCloseToken = buildMockUneditableCloseToken('div');
+export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
@@ -41,7 +41,17 @@ export const originInlineToken = {
content: '<i>Inline</i> content',
};
export const uneditableInlineTokens = [
- buildUneditableOpenToken('span'),
+ buildMockUneditableOpenToken('a'),
originInlineToken,
- buildUneditableCloseToken('span'),
+ buildMockUneditableCloseToken('a'),
+];
+
+export const uneditableBlockTokens = [
+ buildMockUneditableOpenToken('div'),
+ {
+ type: 'text',
+ tagName: null,
+ content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
+ },
+ buildMockUneditableCloseToken('div'),
];
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
new file mode 100644
index 00000000000..a6c712eeb31
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
@@ -0,0 +1,38 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
+import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { normalTextNode } from './mock_data';
+
+const htmlBlockNode = {
+ firstChild: null,
+ literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
+ type: 'htmlBlock',
+};
+
+describe('Render HTML renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument is an html block', () => {
+ expect(renderer.canRender(htmlBlockNode)).toBe(true);
+ });
+
+ it('should return false when the argument is not an html block', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ const htmlBlockNodeToMark = {
+ firstChild: null,
+ literal: '<div data-to-mark ></div>',
+ type: 'htmlBlock',
+ };
+
+ it.each`
+ node
+ ${htmlBlockNode}
+ ${htmlBlockNodeToMark}
+ `('should return uneditable tokens wrapping the $node as a token', ({ node }) => {
+ expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node));
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js
deleted file mode 100644
index c863b86ebf6..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html';
-import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-
-import { normalTextNode } from './mock_data';
-
-const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>';
-const htmlBlockNode = {
- firstChild: null,
- literal: htmlLiteral,
- type: 'htmlBlock',
-};
-
-describe('Render HTML renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument is an html block', () => {
- expect(renderer.canRender(htmlBlockNode)).toBe(true);
- });
-
- it('should return false when the argument is not an html block', () => {
- expect(renderer.canRender(normalTextNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should return uneditable tokens wrapping the origin token', () => {
- const origin = jest.fn();
- const context = { origin };
-
- expect(renderer.render(htmlBlockNode, context)).toStrictEqual(
- buildUneditableTokens(origin()),
- );
- });
- });
-});
diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb
new file mode 100644
index 00000000000..68de8317b82
--- /dev/null
+++ b/spec/haml_lint/linter/documentation_links_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'haml_lint'
+require 'haml_lint/spec'
+require Rails.root.join('haml_lint/linter/documentation_links')
+
+RSpec.describe HamlLint::Linter::DocumentationLinks do
+ include_context 'linter'
+
+ context 'when link_to points to the existing file path' do
+ let(:haml) { "= link_to 'Description', help_page_path('README.md')" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when link_to points to the existing file with valid anchor' do
+ let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'overview'), target: '_blank'" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when link_to points to the existing file path without .md extension' do
+ let(:haml) { "= link_to 'Description', help_page_path('README')" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when anchor is not correct' do
+ let(:haml) { "= link_to 'Description', help_page_path('README.md', anchor: 'wrong')" }
+
+ it { is_expected.to report_lint }
+
+ context 'when help_page_path has multiple options' do
+ let(:haml) { "= link_to 'Description', help_page_path('README.md', key: :value, anchor: 'wrong')" }
+
+ it { is_expected.to report_lint }
+ end
+ end
+
+ context 'when file path is wrong' do
+ let(:haml) { "= link_to 'Description', help_page_path('wrong.md'), target: '_blank'" }
+
+ it { is_expected.to report_lint }
+ end
+
+ context 'when link with wrong file path is assigned to a variable' do
+ let(:haml) { "- my_link = link_to 'Description', help_page_path('wrong.md')" }
+
+ it { is_expected.to report_lint }
+ end
+
+ context 'when it is a broken code' do
+ let(:haml) { "= I am broken! ]]]]" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when anchor belongs to a different element' do
+ let(:haml) { "= link_to 'Description', help_page_path('README.md'), target: (anchor: 'blank')" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when a simple help_page_path' do
+ let(:haml) { "- url = help_page_path('wrong.md')" }
+
+ it { is_expected.to report_lint }
+ end
+
+ context 'when link is not a string' do
+ let(:haml) { "- url = help_page_path(help_url)" }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'when link is a part of the tag' do
+ let(:haml) { ".data-form{ data: { url: help_page_path('wrong.md') } }" }
+
+ it { is_expected.to report_lint }
+ end
+end
diff --git a/spec/lib/api/entities/merge_request_approvals_spec.rb b/spec/lib/api/entities/merge_request_approvals_spec.rb
new file mode 100644
index 00000000000..cbbb037100a
--- /dev/null
+++ b/spec/lib/api/entities/merge_request_approvals_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::MergeRequestApprovals do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+
+ subject { described_class.new(merge_request, current_user: user).as_json }
+
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'serializes an approved merge request' do
+ create(:approval, merge_request: merge_request, user: user)
+
+ is_expected.to eq({
+ user_has_approved: true,
+ user_can_approve: false,
+ approved: true,
+ approved_by: [{
+ user: API::Entities::UserBasic.new(user).as_json
+ }]
+ })
+ end
+
+ it 'serializes a merge request that is not approved' do
+ is_expected.to eq({
+ user_has_approved: false,
+ user_can_approve: true,
+ approved: false,
+ approved_by: []
+ })
+ end
+end
diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb
index 3e9990a45fe..f5954cd8c1e 100644
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ b/spec/lib/gitlab/danger/changelog_spec.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'rspec-parameterized'
require_relative 'danger_spec_helper'
require 'gitlab/danger/changelog'
RSpec.describe Gitlab::Danger::Changelog do
- using RSpec::Parameterized::TableSyntax
include DangerSpecHelper
let(:added_files) { nil }
@@ -26,34 +24,36 @@ RSpec.describe Gitlab::Danger::Changelog do
subject(:changelog) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) }
describe '#needed?' do
- subject { changelog.needed? }
+ let(:category_with_changelog) { :backend }
+ let(:label_with_changelog) { 'frontend' }
+ let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first }
+ let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first }
- where(:categories, :labels) do
- { backend: nil } | %w[backend backstage]
- { frontend: nil, docs: nil } | ['ci-build']
- { engineering_productivity: nil, none: nil } | ['meta']
- end
+ subject { changelog.needed? }
- with_them do
- let(:changes_by_category) { categories }
- let(:mr_labels) { labels }
+ context 'when MR contains only categories requiring no changelog' do
+ let(:changes_by_category) { { category_without_changelog => nil } }
+ let(:mr_labels) { [] }
- it "is falsy when categories and labels require no changelog" do
+ it 'is falsey' do
is_expected.to be_falsy
end
end
- where(:categories, :labels) do
- { frontend: nil, docs: nil } | ['database::review pending', 'feature']
- { backend: nil } | ['backend', 'technical debt']
- { engineering_productivity: nil, none: nil } | ['frontend']
+ context 'when MR contains a label that require no changelog' do
+ let(:changes_by_category) { { category_with_changelog => nil } }
+ let(:mr_labels) { [label_with_changelog, label_without_changelog] }
+
+ it 'is falsey' do
+ is_expected.to be_falsy
+ end
end
- with_them do
- let(:changes_by_category) { categories }
- let(:mr_labels) { labels }
+ context 'when MR contains a category that require changelog and a category that require no changelog' do
+ let(:changes_by_category) { { category_with_changelog => nil, category_without_changelog => nil } }
+ let(:mr_labels) { [] }
- it "is truthy when categories and labels require a changelog" do
+ it 'is truthy' do
is_expected.to be_truthy
end
end
diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb
new file mode 100644
index 00000000000..001ff5bc487
--- /dev/null
+++ b/spec/lib/gitlab/utils/markdown_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Utils::Markdown do
+ let(:klass) do
+ Class.new do
+ include Gitlab::Utils::Markdown
+ end
+ end
+
+ subject(:object) { klass.new }
+
+ describe '#string_to_anchor' do
+ subject { object.string_to_anchor(string) }
+
+ let(:string) { 'My Header' }
+
+ it 'converts string to anchor' do
+ is_expected.to eq 'my-header'
+ end
+
+ context 'when string has punctuation' do
+ let(:string) { 'My, Header!' }
+
+ it 'removes punctuation' do
+ is_expected.to eq 'my-header'
+ end
+ end
+
+ context 'when string starts and ends with spaces' do
+ let(:string) { ' My Header ' }
+
+ it 'removes extra spaces' do
+ is_expected.to eq 'my-header'
+ end
+ end
+
+ context 'when string has multiple spaces and dashes in the middle' do
+ let(:string) { 'My - - - Header' }
+
+ it 'removes consecutive dashes' do
+ is_expected.to eq 'my-header'
+ end
+ end
+
+ context 'when string contains only digits' do
+ let(:string) { '123' }
+
+ it 'adds anchor prefix' do
+ is_expected.to eq 'anchor-123'
+ end
+ end
+
+ context 'when string is empty' do
+ let(:string) { '' }
+
+ it 'returns an empty string' do
+ is_expected.to eq ''
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_base_spec.rb
new file mode 100644
index 00000000000..e4aded1b8d0
--- /dev/null
+++ b/spec/models/concerns/approvable_base_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ApprovableBase do
+ describe '#has_approved?' do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+
+ subject { merge_request.has_approved?(user) }
+
+ context 'when a user has not approved' do
+ it 'returns false' do
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'when a user has approved' do
+ let!(:approval) { create(:approval, merge_request: merge_request, user: user) }
+
+ it 'returns false' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when a user is nil' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ is_expected.to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index e7184d23767..f1e581efd44 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -613,4 +613,22 @@ RSpec.describe MergeRequestPresenter do
end
end
end
+
+ describe '#api_approvals_path' do
+ subject { described_class.new(resource, current_user: user).api_approvals_path }
+
+ it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approvals")) }
+ end
+
+ describe '#api_approve_path' do
+ subject { described_class.new(resource, current_user: user).api_approve_path }
+
+ it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/approve")) }
+ end
+
+ describe '#api_unapprove_path' do
+ subject { described_class.new(resource, current_user: user).api_unapprove_path }
+
+ it { is_expected.to eq(expose_path("/api/v4/projects/#{project.id}/merge_requests/#{resource.iid}/unapprove")) }
+ end
end
diff --git a/spec/requests/api/ci/runner_spec.rb b/spec/requests/api/ci/runner_spec.rb
index 8106b7195c8..c8718309bf2 100644
--- a/spec/requests/api/ci/runner_spec.rb
+++ b/spec/requests/api/ci/runner_spec.rb
@@ -1823,13 +1823,36 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['ProcessLsif']).to be_truthy
end
+ it 'adds ProcessLsifReferences header' do
+ authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['ProcessLsifReferences']).to be_truthy
+ end
+
context 'code_navigation feature flag is disabled' do
- it 'does not add ProcessLsif header' do
+ it 'responds with a forbidden error' do
stub_feature_flags(code_navigation: false)
+ authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['ProcessLsif']).to be_falsy
+ expect(json_response['ProcessLsifReferences']).to be_falsy
+ end
+ end
+ end
+ context 'code_navigation_references feature flag is disabled' do
+ it 'sets ProcessLsifReferences header to false' do
+ stub_feature_flags(code_navigation_references: false)
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
- expect(response).to have_gitlab_http_status(:forbidden)
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['ProcessLsif']).to be_truthy
+ expect(json_response['ProcessLsifReferences']).to be_falsy
+ end
end
end
end