Commit dcaada67 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0993bbd2 0aa5aead
......@@ -3,7 +3,6 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
import NoteableNote from '~/notes/components/noteable_note.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PublishButton from './publish_button.vue';
export default {
......@@ -12,7 +11,6 @@ export default {
PublishButton,
GlButton,
},
mixins: [glFeatureFlagsMixin()],
props: {
draft: {
type: Object,
......@@ -63,14 +61,14 @@ export default {
this.isEditingDraft = false;
},
handleMouseEnter(draft) {
if (this.glFeatures.multilineComments && draft.position) {
if (draft.position) {
this.setSelectedCommentPositionHover(draft.position.line_range);
}
},
handleMouseLeave(draft) {
// Even though position isn't used here we still don't want to unecessarily call a mutation
// Even though position isn't used here we still don't want to unnecessarily call a mutation
// The lack of position tells us that highlighting is irrelevant in this context
if (this.glFeatures.multilineComments && draft.position) {
if (draft.position) {
this.setSelectedCommentPositionHover();
}
},
......
......@@ -3,7 +3,6 @@ import { mapGetters } from 'vuex';
import { GlSprintf, GlIcon } from '@gitlab/ui';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getStartLineNumber,
getEndLineNumber,
......@@ -16,7 +15,7 @@ export default {
GlIcon,
GlSprintf,
},
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
mixins: [resolvedStatusMixin],
props: {
draft: {
type: Object,
......@@ -71,6 +70,10 @@ export default {
return this.draft.position || this.discussion.position;
},
startLineNumber() {
if (this.position?.position_type === IMAGE_DIFF_POSITION_TYPE) {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `${this.position.x}x ${this.position.y}y`;
}
return getStartLineNumber(this.position?.line_range);
},
endLineNumber() {
......@@ -90,16 +93,12 @@ export default {
<span>
<span class="review-preview-item-header">
<gl-icon class="flex-shrink-0" :name="iconName" />
<span
class="bold text-nowrap"
:class="{ 'gl-align-items-center': glFeatures.multilineComments }"
>
<span class="bold text-nowrap gl-align-items-center">
<span class="review-preview-item-header-text block-truncated">
{{ titleText }}
</span>
<template v-if="showLinePosition">
<template v-if="!glFeatures.multilineComments">:{{ linePosition }}</template>
<template v-else-if="startLineNumber === endLineNumber">
<template v-if="startLineNumber === endLineNumber">
:<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
</template>
<gl-sprintf v-else :message="__(':%{startLine} to %{endLine}')">
......
......@@ -165,7 +165,7 @@ export default {
<template>
<div class="content discussion-form discussion-form-container discussion-notes">
<div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-500 gl-pb-3">
<div class="gl-mb-3 gl-text-gray-500 gl-pb-3">
<multiline-comment-form
v-model="commentLineStart"
:line="line"
......
......@@ -4,7 +4,6 @@ import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SYSTEM_NOTE } from '../constants';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
......@@ -18,7 +17,6 @@ export default {
NoteEditedText,
DiscussionNotesRepliesWrapper,
},
mixins: [glFeatureFlagsMixin()],
props: {
discussion: {
type: Object,
......@@ -96,14 +94,14 @@ export default {
return note.isPlaceholderNote ? note.notes[0] : note;
},
handleMouseEnter(discussion) {
if (this.glFeatures.multilineComments && discussion.position) {
if (discussion.position) {
this.setSelectedCommentPositionHover(discussion.position.line_range);
}
},
handleMouseLeave(discussion) {
// Even though position isn't used here we still don't want to unecessarily call a mutation
// Even though position isn't used here we still don't want to unnecessarily call a mutation
// The lack of position tells us that highlighting is irrelevant in this context
if (this.glFeatures.multilineComments && discussion.position) {
if (discussion.position) {
this.setSelectedCommentPositionHover();
}
},
......
......@@ -3,7 +3,6 @@ import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'lodash';
import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import httpStatusCodes from '~/lib/utils/http_status';
......@@ -38,7 +37,7 @@ export default {
directives: {
SafeHtml,
},
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
mixins: [noteable, resolvable],
props: {
note: {
type: Object,
......@@ -160,7 +159,6 @@ export default {
},
showMultiLineComment() {
if (
!this.glFeatures.multilineComments ||
!this.discussionRoot ||
this.startLineNumber.length === 0 ||
this.endLineNumber.length === 0
......
import { __ } from '~/locale';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as flash } from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
......@@ -48,11 +48,8 @@ export default class ProtectedTagEdit {
.catch(() => {
this.$allowedToCreateDropdownButton.enable();
flash(
__('Failed to update tag!'),
'alert',
document.querySelector('.js-protected-tags-list'),
);
window.scrollTo({ top: 0, behavior: 'smooth' });
flash(s__('ProjectSettings|Failed to update tag!'));
});
}
}
......@@ -69,9 +69,6 @@ table {
}
}
td {
border-color: $white-normal;
}
}
.thead-white {
......
......@@ -30,7 +30,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
......
......@@ -23,7 +23,6 @@
%th
%tbody
%tr
%td.flash-container{ colspan: 4 }
= yield
= paginate @protected_tags, theme: 'gitlab'
---
title: Fix protected branches/tags border
merge_request: 52816
author:
type: changed
---
name: multiline_comments
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37114
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/211255
milestone: '13.2'
type: development
group: group::code review
default_enabled: true
......@@ -2,3 +2,4 @@
filenames:
- ee/app/assets/javascripts/on_demand_scans/graphql/dast_scan_create.mutation.graphql
- ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/api_fuzzing_ci_configuration.query.graphql
......@@ -195,12 +195,7 @@ to expand the diff lines and leave a comment, just as you would for a changed li
### Commenting on multiple lines
> - [Introduced](https://gitlab.com/gitlab-org/ux-research/-/issues/870) in GitLab 13.2.
> - It's deployed behind a feature flag, enabled by default.
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/221268) on GitLab 13.3.
> - It's enabled on GitLab.com.
> - It can be disabled or enabled per-project.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-multiline-comments). **(FREE SELF)**
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299121) in GitLab 13.9.
GitLab provides a way to select which lines of code a comment refers to. After starting a comment
a dropdown selector is shown to select the first line that this comment refers to.
......@@ -216,25 +211,6 @@ above it.
![Multiline comment selection displayed above comment](img/multiline-comment-saved.png)
### Enable or disable multiline comments **(FREE SELF)**
The multiline comments feature is under development but ready for production use.
It is deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to enable it for your instance.
To disable it:
```ruby
Feature.disable(:multiline_comments)
```
To enable it:
```ruby
Feature.enable(:multiline_comments)
```
## Pipeline status in merge requests widgets
If you've set up [GitLab CI/CD](../../../ci/README.md) in your project,
......
import { initApiFuzzingConfiguration } from 'ee/security_configuration/api_fuzzing/api_fuzzing_configuration_init';
initApiFuzzingConfiguration();
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from './graphql/provider';
import ApiFuzzingApp from './components/app.vue';
export const initApiFuzzingConfiguration = () => {
const el = document.querySelector('.js-api-fuzzing-configuration');
if (!el) {
return undefined;
}
const {
fullPath,
apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath,
ciVariablesDocumentationPath,
projectCiSettingsPath,
} = el.dataset;
const canSetProjectCiVariables = parseBoolean(el.dataset.canSetProjectCiVariables);
return new Vue({
el,
apolloProvider,
provide: {
fullPath,
apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath,
ciVariablesDocumentationPath,
projectCiSettingsPath,
canSetProjectCiVariables,
},
render(createElement) {
return createElement(ApiFuzzingApp);
},
});
};
<script>
import { GlAlert, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import apiFuzzingCiConfigurationQuery from '../graphql/api_fuzzing_ci_configuration.query.graphql';
import ConfigurationForm from './configuration_form.vue';
export default {
components: {
GlAlert,
GlLink,
GlLoadingIcon,
GlSprintf,
ConfigurationForm,
},
inject: {
fullPath: {
from: 'fullPath',
},
apiFuzzingDocumentationPath: {
from: 'apiFuzzingDocumentationPath',
},
},
apollo: {
apiFuzzingCiConfiguration: {
query: apiFuzzingCiConfigurationQuery,
variables() {
return {
fullPath: this.fullPath,
};
},
update({ project: { apiFuzzingCiConfiguration } }) {
return apiFuzzingCiConfiguration;
},
},
},
i18n: {
title: s__('APIFuzzing|API Fuzzing Configuration'),
helpText: s__(`
APIFuzzing|Customize common API fuzzing settings to suit your requirements.
For details of more advanced configuration options, see the
%{docsLinkStart}GitLab API Fuzzing documentation%{docsLinkEnd}.`),
notice: s__(`
APIFuzzing|Use this tool to generate API fuzzing configuration YAML to copy into your
.gitlab-ci.yml file. This tool does not reflect or update your .gitlab-ci.yml file automatically.
`),
},
};
</script>
<template>
<article>
<header class="gl-mt-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
<h4>
{{ s__('APIFuzzing|API Fuzzing Configuration') }}
</h4>
<p>
<gl-sprintf :message="$options.i18n.helpText">
<template #docsLink="{ content }">
<gl-link :href="apiFuzzingDocumentationPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</header>
<gl-alert :dismissible="false" class="gl-mb-5">
{{ $options.i18n.notice }}
</gl-alert>
<gl-loading-icon v-if="$apollo.loading" size="lg" />
<configuration-form v-else :api-fuzzing-ci-configuration="apiFuzzingCiConfiguration" />
</article>
</template>
<script>
import {
GlAccordion,
GlAccordionItem,
GlAlert,
GlButton,
GlFormGroup,
GlFormText,
GlFormCheckbox,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { SCAN_MODES } from '../constants';
import DropdownInput from '../../components/dropdown_input.vue';
import DynamicFields from '../../components/dynamic_fields.vue';
import FormInput from '../../components/form_input.vue';
export default {
components: {
GlAccordion,
GlAccordionItem,
GlAlert,
GlButton,
GlFormGroup,
GlFormText,
GlFormCheckbox,
GlLink,
GlSprintf,
DropdownInput,
DynamicFields,
FormInput,
},
inject: {
apiFuzzingAuthenticationDocumentationPath: {
from: 'apiFuzzingAuthenticationDocumentationPath',
},
ciVariablesDocumentationPath: {
from: 'ciVariablesDocumentationPath',
},
projectCiSettingsPath: {
from: 'projectCiSettingsPath',
},
canSetProjectCiVariables: {
from: 'canSetProjectCiVariables',
},
},
props: {
apiFuzzingCiConfiguration: {
type: Object,
required: true,
},
},
data() {
return {
targetUrl: {
field: 'targetUrl',
label: s__('APIFuzzing|Target URL'),
description: s__('APIFuzzing|Base URL of API fuzzing target.'),
placeholder: __('Ex: Example.com'),
value: '',
},
scanMode: {
field: 'scanMode',
label: s__('APIFuzzing|Scan mode'),
description: s__('APIFuzzing|There are two ways to perform scans.'),
value: '',
defaultText: s__('APIFuzzing|Choose a method'),
options: this.apiFuzzingCiConfiguration.scanModes.map((value) => ({
value,
text: SCAN_MODES[value].scanModeLabel,
})),
},
apiSpecificationFile: {
field: 'apiSpecificationFile',
value: '',
},
authenticationEnabled: false,
authenticationSettings: [
{
type: 'string',
field: 'username',
label: s__('APIFuzzing|Username for basic authentication'),
description: s__(
'APIFuzzing|Instead of entering the username directly, enter the key of the CI variable set to the username.',
),
placeholder: s__('APIFuzzing|Ex: $TestUsername'),
value: '',
},
{
type: 'string',
field: 'password',
label: s__('APIFuzzing|Password for basic authentication'),
description: s__(
'APIFuzzing|Instead of entering the password directly, enter the key of the CI variable set to the password.',
),
placeholder: s__('APIFuzzing|Ex: $TestPassword'),
value: '',
},
],
scanProfile: {
field: 'scanProfile',
label: s__('APIFuzzing|Scan profile'),
description: 'Pre-defined profiles by GitLab.',
value: '',
defaultText: s__('APIFuzzing|Choose a profile'),
options: this.apiFuzzingCiConfiguration.scanProfiles.map(
({ name: value, description: text }) => ({
value,
text,
}),
),
},
};
},
computed: {
authAlertI18n() {
return this.canSetProjectCiVariables
? {
title: s__('APIFuzzing|Make sure your credentials are secured'),
text: s__(
`APIFuzzing|To prevent a security leak, authentication info must be added as a
%{ciVariablesLinkStart}CI variable%{ciVariablesLinkEnd}. As a user with maintainer access
rights, you can manage CI variables in the
%{ciSettingsLinkStart}Settings%{ciSettingsLinkEnd} area.`,
),
}
: {
title: s__("APIFuzzing|You may need a maintainer's help to secure your credentials."),
text: s__(
`APIFuzzing|To prevent a security leak, authentication info must be added as a
%{ciVariablesLinkStart}CI variable%{ciVariablesLinkEnd}. A user with maintainer
access rights can manage CI variables in the
%{ciSettingsLinkStart}Settings%{ciSettingsLinkEnd} area. We detected that you are not
a maintainer. Commit your changes and assign them to a maintainer to update the
credentials before merging.`,
),
};
},
scanProfileYaml() {
return this.apiFuzzingCiConfiguration.scanProfiles.find(
({ name }) => name === this.scanProfile.value,
)?.yaml;
},
},
methods: {
onSubmit() {},
},
SCAN_MODES,
};
</script>
<template>
<form @submit.prevent="onSubmit">
<form-input v-model="targetUrl.value" v-bind="targetUrl" class="gl-mb-7" />
<dropdown-input v-model="scanMode.value" v-bind="scanMode" />
<form-input
v-if="scanMode.value"
v-model="apiSpecificationFile.value"
v-bind="{ ...apiSpecificationFile, ...$options.SCAN_MODES[scanMode.value] }"
/>
<gl-form-group class="gl-my-7">
<template #label>
{{ __('Authentication') }}
<gl-form-text class="gl-mt-3">
<gl-sprintf
:message="
s__(
'APIFuzzing|Authentication is handled by providing HTTP basic authentication token as a header or cookie. %{linkStart}More information%{linkEnd}.',
)
"
>
<template #link="{ content }">
<a :href="apiFuzzingAuthenticationDocumentationPath">
{{ content }}
</a>
</template>
</gl-sprintf>
</gl-form-text>
</template>
<gl-form-checkbox
v-model="authenticationEnabled"
data-testid="api-fuzzing-enable-authentication-checkbox"
>
{{ s__('APIFuzzing|Enable authentication') }}
</gl-form-checkbox>
</gl-form-group>
<template v-if="authenticationEnabled">
<gl-alert
:title="authAlertI18n.title"
:dismissible="false"
variant="warning"
class="gl-mb-5"
data-testid="api-fuzzing-authentication-notice"
>
<gl-sprintf :message="authAlertI18n.text">
<template #ciVariablesLink="{ content }">
<gl-link :href="ciVariablesDocumentationPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #ciSettingsLink="{ content }">
<gl-link :href="projectCiSettingsPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<dynamic-fields v-model="authenticationSettings" />
</template>
<dropdown-input v-model="scanProfile.value" v-bind="scanProfile" />
<template v-if="scanProfileYaml">
<gl-accordion>
<gl-accordion-item :title="s__('APIFuzzing|Show code snippet for the profile')">
<pre data-testid="api-fuzzing-scan-profile-yaml-viewer">{{ scanProfileYaml }}</pre>
</gl-accordion-item>
</gl-accordion>
</template>
<hr />
<gl-button type="submit" variant="confirm">{{
s__('APIFuzzing|Generate code snippet')
}}</gl-button>
<gl-button>{{ __('Cancel') }}</gl-button>
</form>
</template>
import { __, s__ } from '~/locale';
export const SCAN_MODES = {
HAR: {
scanModeLabel: __('HAR (HTTP Archive)'),
label: __('HAR file path'),
placeholder: s__('APIFuzzing|Ex: Project_Test/File/example_fuzz.har'),
description: s__(
"APIFuzzing|HAR files may contain sensitive information such as authentication tokens, API keys, and session cookies. We recommend that you review the HAR files' contents before adding them to a repository.",
),
},
OPENAPI: {
scanModeLabel: __('Open API'),
label: __('Open API specification file path'),
placeholder: s__('APIFuzzing|Ex: Project_Test/File/example_fuzz.json'),
description: s__(
'APIFuzzing|We recommend that you review the JSON specifications file before adding it to a repository.',
),
},
};
query apiFuzzingCiConfiguration($fullPath: ID!) {
project(fullPath: $fullPath) {
apiFuzzingCiConfiguration {
scanModes
scanProfiles {
name
description
yaml
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
......@@ -51,6 +51,11 @@ export default {
required: false,
default: false,
},
placeholder: {
type: String,
required: false,
default: '',
},
},
computed: {
showCustomValueMessage() {
......@@ -83,6 +88,7 @@ export default {
:size="inputSize"
:value="value"
:disabled="disabled"
:placeholder="placeholder"
@input="$emit('input', $event)"
/>
......
# frozen_string_literal: true
module Projects::Security::ApiFuzzingConfigurationHelper
def api_fuzzing_configuration_data(project)
{
full_path: project.full_path,
api_fuzzing_documentation_path: help_page_path('user/application_security/api_fuzzing/index'),
api_fuzzing_authentication_documentation_path: help_page_path('user/application_security/api_fuzzing/index', anchor: 'authentication'),
ci_variables_documentation_path: help_page_path('ci/variables/README'),
project_ci_settings_path: project_settings_ci_cd_path(project),
can_set_project_ci_variables: can?(current_user, :admin_pipeline, project).to_s
}
end
end
......@@ -2,4 +2,4 @@
- breadcrumb_title _("API Fuzzing Configuration")
- page_title _("API Fuzzing Configuration")
%h1= "API fuzzing configuration"
.js-api-fuzzing-configuration{ data: api_fuzzing_configuration_data(@project) }
......@@ -38,6 +38,7 @@ describe('EE DiffLineNoteForm', () => {
diff_refs: {
head_sha: headSha || null,
},
highlighted_diff_lines: [],
})),
},
},
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import { GlAlert, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { stripTypenames } from 'helpers/graphql_helpers';
import createMockApollo from 'helpers/mock_apollo_helper';
import apiFuzzingCiConfigurationQuery from 'ee/security_configuration/api_fuzzing/graphql/api_fuzzing_ci_configuration.query.graphql';
import App from 'ee/security_configuration/api_fuzzing/components/app.vue';
import ConfigurationForm from 'ee/security_configuration/api_fuzzing/components/configuration_form.vue';
import { apiFuzzingConfigurationQueryResponse } from '../mock_data';
Vue.use(VueApollo);
describe('EE - ApiFuzzingConfigurationApp', () => {
let wrapper;
const projectFullPath = 'namespace/project';
const pendingHandler = jest.fn(() => new Promise(() => {}));
const successHandler = jest.fn(async () => apiFuzzingConfigurationQueryResponse);
const createMockApolloProvider = (handler) =>
createMockApollo([[apiFuzzingCiConfigurationQuery, handler]]);
const findLoadingSpinner = () => wrapper.find(GlLoadingIcon);
const findConfigurationForm = () => wrapper.find(ConfigurationForm);
const createWrapper = (options) => {
wrapper = shallowMount(
App,
merge(
{
apolloProvider: () => createMockApolloProvider(successHandler),
stubs: {
GlSprintf,
},
provide: {
fullPath: projectFullPath,
apiFuzzingDocumentationPath: '/api_fuzzing/documentation/path',
},
data() {
return {
apiFuzzingCiConfiguration: {},
};
},
},
options,
),
);
};
afterEach(() => {
wrapper.destroy();
});
it('shows a loading spinner while fetching the configuration from the API', () => {
createWrapper({
apolloProvider: createMockApolloProvider(pendingHandler),
});
expect(pendingHandler).toHaveBeenCalledWith({ fullPath: projectFullPath });
expect(findLoadingSpinner().exists()).toBe(true);
expect(findConfigurationForm().exists()).toBe(false);
});
describe('configuration fetched successfully', () => {
beforeEach(() => {
createWrapper();
});
it('shows the form once the configuration has loaded', () => {
expect(findConfigurationForm().exists()).toBe(true);
expect(findLoadingSpinner().exists()).toBe(false);
});
it('passes the configuration to the form', () => {
expect(findConfigurationForm().props('apiFuzzingCiConfiguration')).toEqual(
stripTypenames(apiFuzzingConfigurationQueryResponse.data.project.apiFuzzingCiConfiguration),
);
});
it("shows a notice about the tool's purpose", () => {
const alert = wrapper.find(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(
'Use this tool to generate API fuzzing configuration YAML to copy into your .gitlab-ci.yml file. This tool does not reflect or update your .gitlab-ci.yml file automatically.',
);
});
it('includes a link to API fuzzing documentation ', () => {
const link = wrapper.find(GlLink);
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('/api_fuzzing/documentation/path');
});
});
});
import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { SCAN_MODES } from 'ee/security_configuration/api_fuzzing/constants';
import ConfigurationForm from 'ee/security_configuration/api_fuzzing/components/configuration_form.vue';
import FormInput from 'ee/security_configuration/components/form_input.vue';
import DropdownInput from 'ee/security_configuration/components/dropdown_input.vue';
const makeScanProfile = (name) => ({
name,
description: `${name} description`,
yaml: `
---
:Name: ${name}
`.trim(),
});
describe('EE - ApiFuzzingConfigurationForm', () => {
let wrapper;
const apiFuzzingCiConfiguration = {
scanModes: Object.keys(SCAN_MODES),
scanProfiles: [makeScanProfile('Quick-10'), makeScanProfile('Medium-20')],
};
const findEnableAuthenticationCheckbox = () =>
wrapper.findByTestId('api-fuzzing-enable-authentication-checkbox');
const findScanModeInput = () => wrapper.findAll(DropdownInput).at(0);
const findSpecificationFileInput = () => wrapper.findAll(FormInput).at(1);
const findAuthenticationNotice = () => wrapper.findByTestId('api-fuzzing-authentication-notice');
const findScanProfileDropdownInput = () => wrapper.findAll(DropdownInput).at(1);
const findScanProfileYamlViewer = () =>
wrapper.findByTestId('api-fuzzing-scan-profile-yaml-viewer');
const createWrapper = (options = {}) => {
wrapper = extendedWrapper(
mount(
ConfigurationForm,
merge(
{
provide: {
apiFuzzingAuthenticationDocumentationPath:
'api_fuzzing_authentication/documentation/path',
ciVariablesDocumentationPath: '/ci_cd_variables/documentation/path',
projectCiSettingsPath: '/project/settings/ci_cd',
canSetProjectCiVariables: true,
},
propsData: {
apiFuzzingCiConfiguration,
},
},
options,
),
),
);
};
afterEach(() => {
wrapper.destroy();
});
it('includes a link to API fuzzing authentication documentation', () => {
createWrapper();
expect(wrapper.html()).toContain('api_fuzzing_authentication/documentation/path');
});
describe('scan modes', () => {
beforeEach(() => {
createWrapper();
});
it('displays a dropdown option for each scan mode', () => {
findScanModeInput()
.findAll('li')
.wrappers.forEach((item, index) => {
expect(item.text()).toBe(
SCAN_MODES[apiFuzzingCiConfiguration.scanModes[index]].scanModeLabel,
);
});
});
it('by default, the specification file input is hidden', () => {
expect(wrapper.findAll(FormInput)).toHaveLength(1);
});
describe.each(Object.keys(SCAN_MODES))('when %s scan mode is selected', (scanMode) => {
it('the specificationfile input becomes visible and has the correct labels', async () => {
const selectedScanMode = SCAN_MODES[scanMode];
findScanModeInput().vm.$emit('input', scanMode);
await wrapper.vm.$nextTick();
const specificationFileInput = findSpecificationFileInput();
expect(specificationFileInput.exists()).toBe(true);
expect(specificationFileInput.text()).toContain(selectedScanMode.label);
expect(specificationFileInput.text()).toContain(selectedScanMode.description);
expect(specificationFileInput.find('input').attributes('placeholder')).toBe(
selectedScanMode.placeholder,
);
});
});
});
describe('authentication', () => {
it('authentication section is hidden by default', () => {
createWrapper();
expect(findAuthenticationNotice().exists()).toBe(false);
});
it('authentication section becomes visible once checkbox is checked', async () => {
createWrapper();
await findEnableAuthenticationCheckbox().trigger('click');
expect(findAuthenticationNotice().exists()).toBe(true);
});
it('sees the the proper notice as a maintainer', async () => {
createWrapper();
await findEnableAuthenticationCheckbox().trigger('click');
expect(findAuthenticationNotice().text()).toMatchInterpolatedText(
'Make sure your credentials are secured To prevent a security leak, authentication info must be added as a CI variable. As a user with maintainer access rights, you can manage CI variables in the Settings area.',
);
});
it('sees the the proper notice as a developer', async () => {
createWrapper({
provide: {
canSetProjectCiVariables: false,
},
});
await findEnableAuthenticationCheckbox().trigger('click');
expect(findAuthenticationNotice().text()).toMatchInterpolatedText(
"You may need a maintainer's help to secure your credentials. To prevent a security leak, authentication info must be added as a CI variable. A user with maintainer access rights can manage CI variables in the Settings area. We detected that you are not a maintainer. Commit your changes and assign them to a maintainer to update the credentials before merging.",
);
});
});
describe('scan profiles', () => {
beforeEach(() => {
createWrapper();
});
it('displays a dropdown option for each scan profile', () => {
findScanProfileDropdownInput()
.findAll('li')
.wrappers.forEach((item, index) => {
expect(item.text()).toBe(apiFuzzingCiConfiguration.scanProfiles[index].description);
});
});
it('by default, YAML viewer is not visible', () => {
expect(findScanProfileYamlViewer().exists()).toBe(false);
});
it('when a scan profile is selected, its YAML is visible', async () => {
const selectedScanProfile = apiFuzzingCiConfiguration.scanProfiles[0];
wrapper.findAll(DropdownInput).at(1).vm.$emit('input', selectedScanProfile.name);
await wrapper.vm.$nextTick();
expect(findScanProfileYamlViewer().exists()).toBe(true);
expect(findScanProfileYamlViewer().text()).toBe(selectedScanProfile.yaml);
});
});
});
export const apiFuzzingConfigurationQueryResponse = {
data: {
project: {
apiFuzzingCiConfiguration: {
scanModes: ['HAR', 'OPENAPI'],
scanProfiles: [
{
name: 'Quick-10',
description: 'Fuzzing 10 times per parameter',
yaml:
'---\nName: Quick-10\nDefaultProfile: Empty\nRoutes:\n- Route:\n Order: 0\n Url: "**"\n Mutate: true\n SwaggerUrl:\n Script:\n Headers:\n - Pattern: Host\n Mutate: false\n - Pattern: Connection\n Mutate: false\n - Pattern: Content-Length\n Mutate: false\n ApiTokens:\n - Name: Authorization\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: FormData\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: Query\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: X-API-Key\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n Checks:\n - Name: FormBodyFuzzingCheck\n Configuration:\n FuzzingCount: 10\n UnicodeFuzzing: true\n - Name: GeneralFuzzingCheck\n Configuration:\n FuzzingCount: 10\n UnicodeFuzzing: true\n HeaderFuzzing: false\n Headers:\n - Name: JsonFuzzingCheck\n Configuration:\n FuzzingCount: 10\n UnicodeFuzzing: true\n - Name: XmlFuzzingCheck\n Configuration:\n FuzzingCount: 10\n UnicodeFuzzing: true\n',
__typename: 'ApiFuzzingScanProfile',
},
{
name: 'Medium-20',
description: 'Fuzzing 20 times per parameter',
yaml:
'---\nName: Medium-20\nDefaultProfile: Empty\nRoutes:\n- Route:\n Order: 0\n Url: "**"\n Mutate: true\n SwaggerUrl:\n Script:\n Headers:\n - Pattern: Host\n Mutate: false\n - Pattern: Connection\n Mutate: false\n - Pattern: Content-Length\n Mutate: false\n ApiTokens:\n - Name: Authorization\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: FormData\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: Query\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: X-API-Key\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n Checks:\n - Name: FormBodyFuzzingCheck\n Configuration:\n FuzzingCount: 20\n UnicodeFuzzing: true\n - Name: GeneralFuzzingCheck\n Configuration:\n FuzzingCount: 20\n UnicodeFuzzing: true\n HeaderFuzzing: false\n Headers:\n - Name: JsonFuzzingCheck\n Configuration:\n FuzzingCount: 20\n UnicodeFuzzing: true\n - Name: XmlFuzzingCheck\n Configuration:\n FuzzingCount: 20\n UnicodeFuzzing: true\n',
__typename: 'ApiFuzzingScanProfile',
},
{
name: 'Medium-50',
description: 'Fuzzing 50 times per parameter',
yaml:
'---\nName: Medium-50\nDefaultProfile: Empty\nRoutes:\n- Route:\n Order: 0\n Url: "**"\n Mutate: true\n SwaggerUrl:\n Script:\n Headers:\n - Pattern: Host\n Mutate: false\n - Pattern: Connection\n Mutate: false\n - Pattern: Content-Length\n Mutate: false\n ApiTokens:\n - Name: Authorization\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: FormData\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: Query\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: X-API-Key\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n Checks:\n - Name: FormBodyFuzzingCheck\n Configuration:\n FuzzingCount: 50\n UnicodeFuzzing: true\n - Name: GeneralFuzzingCheck\n Configuration:\n FuzzingCount: 50\n UnicodeFuzzing: true\n HeaderFuzzing: false\n Headers:\n - Name: JsonFuzzingCheck\n Configuration:\n FuzzingCount: 50\n UnicodeFuzzing: true\n - Name: XmlFuzzingCheck\n Configuration:\n FuzzingCount: 50\n UnicodeFuzzing: true\n',
__typename: 'ApiFuzzingScanProfile',
},
{
name: 'Long-100',
description: 'Fuzzing 100 times per parameter',
yaml:
'---\nName: Long-100\nDefaultProfile: Empty\nRoutes:\n- Route:\n Order: 0\n Url: "**"\n Mutate: true\n SwaggerUrl:\n Script:\n Headers:\n - Pattern: Host\n Mutate: false\n - Pattern: Connection\n Mutate: false\n - Pattern: Content-Length\n Mutate: false\n ApiTokens:\n - Name: Authorization\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: FormData\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: access_token\n Where: Query\n Expiration: 120\n IsSignatureOfRequest: false\n - Name: X-API-Key\n Where: Header\n Expiration: 120\n IsSignatureOfRequest: false\n Checks:\n - Name: FormBodyFuzzingCheck\n Configuration:\n FuzzingCount: 100\n UnicodeFuzzing: true\n - Name: GeneralFuzzingCheck\n Configuration:\n FuzzingCount: 100\n UnicodeFuzzing: true\n HeaderFuzzing: false\n Headers:\n - Name: JsonFuzzingCheck\n Configuration:\n FuzzingCount: 100\n UnicodeFuzzing: true\n - Name: XmlFuzzingCheck\n Configuration:\n FuzzingCount: 100\n UnicodeFuzzing: true\n',
__typename: 'ApiFuzzingScanProfile',
},
],
__typename: 'ApiFuzzingCiConfiguration',
},
__typename: 'Project',
},
},
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:full_path) { project.full_path }
let(:api_fuzzing_documentation_path) { help_page_path('user/application_security/api_fuzzing/index') }
let(:api_fuzzing_authentication_documentation_path) { help_page_path('user/application_security/api_fuzzing/index', anchor: 'authentication') }
let(:ci_variables_documentation_path) { help_page_path('ci/variables/README') }
let(:project_ci_settings_path) { project_settings_ci_cd_path(project) }
subject { helper.api_fuzzing_configuration_data(project) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe '#api_fuzzing_configuration_data' do
context 'user with admin_pipeline permissions' do
before do
allow(helper).to receive(:can?).with(user, :admin_pipeline, project).and_return(true)
end
it {
is_expected.to eq(
full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
ci_variables_documentation_path: ci_variables_documentation_path,
project_ci_settings_path: project_ci_settings_path,
can_set_project_ci_variables: 'true'
)
}
end
context 'user without admin_pipeline permissions' do
before do
allow(helper).to receive(:can?).with(user, :admin_pipeline, project).and_return(false)
end
it {
is_expected.to eq(
full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
ci_variables_documentation_path: ci_variables_documentation_path,
project_ci_settings_path: project_ci_settings_path,
can_set_project_ci_variables: 'false'
)
}
end
end
end
......@@ -536,15 +536,14 @@ RSpec.describe API::Issues, :mailer do
include WorkhorseHelpers
using RSpec::Parameterized::TableSyntax
include_context 'workhorse headers'
let(:issue) { create(:incident, project: project) }
let(:file) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:file_name) { 'rails_sample.jpg' }
let(:url) { 'http://gitlab.com' }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:params) { { url: url } }
subject do
......@@ -553,7 +552,7 @@ RSpec.describe API::Issues, :mailer do
method: :post,
file_key: :file,
params: params.merge(file: file),
headers: workhorse_header,
headers: workhorse_headers,
send_rewritten_field: true
)
end
......
......@@ -6,15 +6,14 @@ RSpec.describe API::ProjectImport do
include ExternalAuthorizationServiceHelpers
include WorkhorseHelpers
include_context 'workhorse headers'
let(:user) { create(:user) }
let(:namespace) { create(:group) }
let(:file) { File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:file_name) { 'project_export.tar.gz' }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:file_upload) { fixture_file_upload(file) }
before do
......
......@@ -5,10 +5,10 @@ require 'spec_helper'
RSpec.describe Projects::RequirementsManagement::RequirementsController do
include WorkhorseHelpers
include_context 'workhorse headers'
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
shared_examples 'response with 404 status' do
it 'returns 404' do
......
......@@ -1393,6 +1393,90 @@ msgstr ""
msgid "API Token"
msgstr ""
msgid "APIFuzzing|API Fuzzing Configuration"
msgstr ""
msgid "APIFuzzing|Authentication is handled by providing HTTP basic authentication token as a header or cookie. %{linkStart}More information%{linkEnd}."
msgstr ""
msgid "APIFuzzing|Base URL of API fuzzing target."
msgstr ""
msgid "APIFuzzing|Choose a method"
msgstr ""
msgid "APIFuzzing|Choose a profile"
msgstr ""
msgid "APIFuzzing|Customize common API fuzzing settings to suit your requirements. For details of more advanced configuration options, see the %{docsLinkStart}GitLab API Fuzzing documentation%{docsLinkEnd}."
msgstr ""
msgid "APIFuzzing|Enable authentication"
msgstr ""
msgid "APIFuzzing|Ex: $TestPassword"
msgstr ""
msgid "APIFuzzing|Ex: $TestUsername"
msgstr ""
msgid "APIFuzzing|Ex: Project_Test/File/example_fuzz.har"
msgstr ""
msgid "APIFuzzing|Ex: Project_Test/File/example_fuzz.json"
msgstr ""
msgid "APIFuzzing|Generate code snippet"
msgstr ""
msgid "APIFuzzing|HAR files may contain sensitive information such as authentication tokens, API keys, and session cookies. We recommend that you review the HAR files' contents before adding them to a repository."
msgstr ""
msgid "APIFuzzing|Instead of entering the password directly, enter the key of the CI variable set to the password."
msgstr ""
msgid "APIFuzzing|Instead of entering the username directly, enter the key of the CI variable set to the username."
msgstr ""
msgid "APIFuzzing|Make sure your credentials are secured"
msgstr ""
msgid "APIFuzzing|Password for basic authentication"
msgstr ""
msgid "APIFuzzing|Scan mode"
msgstr ""
msgid "APIFuzzing|Scan profile"
msgstr ""
msgid "APIFuzzing|Show code snippet for the profile"
msgstr ""
msgid "APIFuzzing|Target URL"
msgstr ""
msgid "APIFuzzing|There are two ways to perform scans."
msgstr ""
msgid "APIFuzzing|To prevent a security leak, authentication info must be added as a %{ciVariablesLinkStart}CI variable%{ciVariablesLinkEnd}. A user with maintainer access rights can manage CI variables in the %{ciSettingsLinkStart}Settings%{ciSettingsLinkEnd} area. We detected that you are not a maintainer. Commit your changes and assign them to a maintainer to update the credentials before merging."
msgstr ""
msgid "APIFuzzing|To prevent a security leak, authentication info must be added as a %{ciVariablesLinkStart}CI variable%{ciVariablesLinkEnd}. As a user with maintainer access rights, you can manage CI variables in the %{ciSettingsLinkStart}Settings%{ciSettingsLinkEnd} area."
msgstr ""
msgid "APIFuzzing|Use this tool to generate API fuzzing configuration YAML to copy into your .gitlab-ci.yml file. This tool does not reflect or update your .gitlab-ci.yml file automatically."
msgstr ""
msgid "APIFuzzing|Username for basic authentication"
msgstr ""
msgid "APIFuzzing|We recommend that you review the JSON specifications file before adding it to a repository."
msgstr ""
msgid "APIFuzzing|You may need a maintainer's help to secure your credentials."
msgstr ""
msgid "AWS Access Key"
msgstr ""
......@@ -4141,6 +4225,9 @@ msgstr ""
msgid "Authenticating"
msgstr ""
msgid "Authentication"
msgstr ""
msgid "Authentication Failure"
msgstr ""
......@@ -11853,6 +11940,9 @@ msgstr ""
msgid "Evidence collection"
msgstr ""
msgid "Ex: Example.com"
msgstr ""
msgid "Exactly one of %{attributes} is required"
msgstr ""
......@@ -12300,9 +12390,6 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
msgid "Failed to update tag!"
msgstr ""
msgid "Failed to update the Canary Ingress."
msgstr ""
......@@ -14513,6 +14600,12 @@ msgstr ""
msgid "Guideline"
msgstr ""
msgid "HAR (HTTP Archive)"
msgstr ""
msgid "HAR file path"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
msgstr ""
......@@ -20670,6 +20763,12 @@ msgstr ""
msgid "Open"
msgstr ""
msgid "Open API"
msgstr ""
msgid "Open API specification file path"
msgstr ""
msgid "Open Selection"
msgstr ""
......
......@@ -21,14 +21,11 @@ describe('Batch comments draft note component', () => {
const getList = () => getByRole(wrapper.element, 'list');
const createComponent = (propsData = { draft }, features = {}) => {
const createComponent = (propsData = { draft }) => {
wrapper = shallowMount(localVue.extend(DraftNote), {
store,
propsData,
localVue,
provide: {
glFeatures: { multilineComments: true, ...features },
},
});
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
......@@ -145,16 +142,14 @@ describe('Batch comments draft note component', () => {
describe('multiline comments', () => {
describe.each`
desc | props | features | event | expectedCalls
${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
`('$desc and features $features', ({ props, event, features, expectedCalls }) => {
desc | props | event | expectedCalls
${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
${'without `draft.position`'} | ${{}} | ${'mouseenter'} | ${[]}
${'without `draft.position`'} | ${{}} | ${'mouseleave'} | ${[]}
`('$desc', ({ props, event, expectedCalls }) => {
beforeEach(() => {
createComponent({ draft: { ...draft, ...props } }, features);
createComponent({ draft: { ...draft, ...props } });
jest.spyOn(store, 'dispatch');
});
......
......@@ -56,17 +56,30 @@ describe('Batch comments draft preview item component', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
position: { new_line: 1 },
position: {
line_range: {
start: {
new_line: 1,
type: 'new',
},
},
},
});
expect(vm.$el.querySelector('.bold').textContent).toContain(':1');
expect(vm.$el.querySelector('.bold').textContent).toContain(':+1');
});
it('renders old line position', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
position: { old_line: 2 },
position: {
line_range: {
start: {
old_line: 2,
},
},
},
});
expect(vm.$el.querySelector('.bold').textContent).toContain(':2');
......
......@@ -17,6 +17,7 @@ describe('DiffLineNoteForm', () => {
const store = createStore();
store.state.notes.userData.id = 1;
store.state.notes.noteableData = noteableDataMock;
store.state.diffs.diffFiles = [diffFile];
store.replaceState({ ...store.state, ...args.state });
......
......@@ -23,7 +23,7 @@ describe('DiscussionNotes', () => {
let wrapper;
const getList = () => getByRole(wrapper.element, 'list');
const createComponent = (props, features = {}) => {
const createComponent = (props) => {
wrapper = shallowMount(DiscussionNotes, {
store,
propsData: {
......@@ -38,9 +38,6 @@ describe('DiscussionNotes', () => {
slots: {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
},
provide: {
glFeatures: { multilineComments: true, ...features },
},
});
};
......@@ -177,16 +174,14 @@ describe('DiscussionNotes', () => {
});
describe.each`
desc | props | features | event | expectedCalls
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
`('$desc and features $features', ({ props, event, features, expectedCalls }) => {
desc | props | event | expectedCalls
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
${'without `discussion.position`'} | ${{}} | ${'mouseenter'} | ${[]}
${'without `discussion.position`'} | ${{}} | ${'mouseleave'} | ${[]}
`('$desc', ({ props, event, expectedCalls }) => {
beforeEach(() => {
createComponent(props, features);
createComponent(props);
jest.spyOn(store, 'dispatch');
});
......
......@@ -8,15 +8,6 @@ import NoteActions from '~/notes/components/note_actions.vue';
import NoteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({
inject: {
glFeatures: {
from: 'glFeatures',
default: () => ({ multilineComments: true }),
},
},
}));
describe('issue_note', () => {
let store;
let wrapper;
......
......@@ -6,6 +6,8 @@ RSpec.describe API::GenericPackages do
include HttpBasicAuthHelpers
using RSpec::Parameterized::TableSyntax
include_context 'workhorse headers'
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:deploy_token_rw) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
......@@ -14,8 +16,6 @@ RSpec.describe API::GenericPackages do
let_it_be(:project_deploy_token_ro) { create(:project_deploy_token, deploy_token: deploy_token_ro, project: project) }
let_it_be(:deploy_token_wo) { create(:deploy_token, read_package_registry: false, write_package_registry: true) }
let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:user) { personal_access_token.user }
let(:ci_build) { create(:ci_build, :running, user: user) }
......@@ -129,7 +129,7 @@ RSpec.describe API::GenericPackages do
end
it "responds with #{params[:expected_status]}" do
authorize_upload_file(workhorse_header.merge(auth_header))
authorize_upload_file(workhorse_headers.merge(auth_header))
expect(response).to have_gitlab_http_status(expected_status)
end
......@@ -144,7 +144,7 @@ RSpec.describe API::GenericPackages do
with_them do
it "responds with #{params[:expected_status]}" do
authorize_upload_file(workhorse_header.merge(deploy_token_auth_header))
authorize_upload_file(workhorse_headers.merge(deploy_token_auth_header))
expect(response).to have_gitlab_http_status(expected_status)
end
......@@ -162,7 +162,7 @@ RSpec.describe API::GenericPackages do
end
with_them do
subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) }
subject { authorize_upload_file(workhorse_headers.merge(personal_access_token_header), param_name => param_value) }
it_behaves_like 'secure endpoint'
end
......@@ -173,7 +173,7 @@ RSpec.describe API::GenericPackages do
stub_feature_flags(generic_packages: false)
project.add_developer(user)
authorize_upload_file(workhorse_header.merge(personal_access_token_header))
authorize_upload_file(workhorse_headers.merge(personal_access_token_header))
expect(response).to have_gitlab_http_status(:not_found)
end
......@@ -239,7 +239,7 @@ RSpec.describe API::GenericPackages do
end
it "responds with #{params[:expected_status]}" do
headers = workhorse_header.merge(auth_header)
headers = workhorse_headers.merge(auth_header)
upload_file(params, headers)
......@@ -254,7 +254,7 @@ RSpec.describe API::GenericPackages do
with_them do
it "responds with #{params[:expected_status]}" do
headers = workhorse_header.merge(deploy_token_auth_header)
headers = workhorse_headers.merge(deploy_token_auth_header)
upload_file(params, headers)
......@@ -270,7 +270,7 @@ RSpec.describe API::GenericPackages do
shared_examples 'creates a package and package file' do
it 'creates a package and package file' do
headers = workhorse_header.merge(auth_header)
headers = workhorse_headers.merge(auth_header)
expect { upload_file(params, headers) }
.to change { project.packages.generic.count }.by(1)
......@@ -324,26 +324,26 @@ RSpec.describe API::GenericPackages do
end
context 'event tracking' do
subject { upload_file(params, workhorse_header.merge(personal_access_token_header)) }
subject { upload_file(params, workhorse_headers.merge(personal_access_token_header)) }
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end
it 'rejects request without a file from workhorse' do
headers = workhorse_header.merge(personal_access_token_header)
headers = workhorse_headers.merge(personal_access_token_header)
upload_file({}, headers)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'rejects request without an auth token' do
upload_file(params, workhorse_header)
upload_file(params, workhorse_headers)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request without workhorse rewritten fields' do
headers = workhorse_header.merge(personal_access_token_header)
headers = workhorse_headers.merge(personal_access_token_header)
upload_file(params, headers, send_rewritten_field: false)
expect(response).to have_gitlab_http_status(:bad_request)
......@@ -354,7 +354,7 @@ RSpec.describe API::GenericPackages do
allow(uploaded_file).to receive(:size).and_return(project.actual_limits.generic_packages_max_file_size + 1)
end
headers = workhorse_header.merge(personal_access_token_header)
headers = workhorse_headers.merge(personal_access_token_header)
upload_file(params, headers)
expect(response).to have_gitlab_http_status(:bad_request)
......@@ -378,7 +378,7 @@ RSpec.describe API::GenericPackages do
end
with_them do
subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) }
subject { upload_file(params, workhorse_headers.merge(personal_access_token_header), param_name => param_value) }
it_behaves_like 'secure endpoint'
end
......
......@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe API::GroupImport do
include WorkhorseHelpers
include_context 'workhorse headers'
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let(:path) { '/groups/import' }
let(:file) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
before do
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
......
......@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe API::MavenPackages do
include WorkhorseHelpers
include_context 'workhorse headers'
let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group) }
let_it_be(:group) { package_settings.namespace }
let_it_be(:user) { create(:user) }
......@@ -20,8 +22,7 @@ RSpec.describe API::MavenPackages do
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
let(:package_name) { 'com/example/my-app' }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers) { workhorse_headers }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
let(:group_deploy_token_headers) { { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token_for_group.token } }
......@@ -548,8 +549,8 @@ RSpec.describe API::MavenPackages do
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let(:send_rewritten_field) { true }
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar') }
......@@ -602,7 +603,7 @@ RSpec.describe API::MavenPackages do
end
context 'without workhorse header' do
let(:workhorse_header) { {} }
let(:workhorse_headers) { {} }
subject { upload_file_with_token(params: params) }
......
......@@ -144,8 +144,8 @@ RSpec.describe API::NugetProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
let(:headers) { {} }
......@@ -176,7 +176,7 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
......@@ -194,8 +194,8 @@ RSpec.describe API::NugetProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{target.id}/packages/nuget" }
let(:headers) { {} }
......@@ -239,7 +239,7 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
......@@ -256,7 +256,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'rejects nuget access with invalid target id'
context 'file size above maximum limit' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(UploadedFile) do |uploaded_file|
......
......@@ -5,13 +5,12 @@ require 'spec_helper'
RSpec.describe API::ProjectImport do
include WorkhorseHelpers
include_context 'workhorse headers'
let(:user) { create(:user) }
let(:file) { File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:namespace) { create(:group) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
before do
namespace.add_owner(user)
end
......
......@@ -74,8 +74,8 @@ RSpec.describe API::PypiPackages do
end
describe 'POST /api/v4/projects/:id/packages/pypi/authorize' do
let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let(:url) { "/projects/#{project.id}/packages/pypi/authorize" }
let(:headers) { {} }
......@@ -106,7 +106,7 @@ RSpec.describe API::PypiPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
......@@ -124,8 +124,8 @@ RSpec.describe API::PypiPackages do
end
describe 'POST /api/v4/projects/:id/packages/pypi' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let_it_be(:file_name) { 'package.whl' }
let(:url) { "/projects/#{project.id}/packages/pypi" }
let(:headers) { {} }
......@@ -170,7 +170,7 @@ RSpec.describe API::PypiPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
......@@ -184,7 +184,7 @@ RSpec.describe API::PypiPackages do
let(:requires_python) { 'x' * 256 }
let(:token) { personal_access_token.token }
let(:user_headers) { basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
......@@ -196,7 +196,7 @@ RSpec.describe API::PypiPackages do
context 'with an invalid package' do
let(:token) { personal_access_token.token }
let(:user_headers) { basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
let(:headers) { user_headers.merge(workhorse_headers) }
before do
params[:name] = '.$/@!^*'
......@@ -213,7 +213,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'rejects PyPI access with unknown project id'
context 'file size above maximum limit' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(UploadedFile) do |uploaded_file|
......
......@@ -5,12 +5,10 @@ require 'spec_helper'
RSpec.describe Import::GitlabGroupsController do
include WorkhorseHelpers
include_context 'workhorse headers'
let_it_be(:user) { create(:user) }
let(:import_path) { "#{Dir.tmpdir}/gitlab_groups_controller_spec" }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) do
{ 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
end
before do
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
......
......@@ -5,8 +5,7 @@ require 'spec_helper'
RSpec.describe Import::GitlabProjectsController do
include WorkhorseHelpers
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
include_context 'workhorse headers'
let_it_be(:namespace) { create(:namespace) }
let_it_be(:user) { namespace.owner }
......
......@@ -67,9 +67,9 @@ RSpec.shared_context 'conan file upload endpoints' do
include WorkhorseHelpers
include HttpBasicAuthHelpers
include_context 'workhorse headers'
let(:jwt) { build_jwt(personal_access_token) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_headers) }
let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
end
# frozen_string_literal: true
RSpec.shared_context 'workhorse headers' do
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
end
# frozen_string_literal: true
RSpec.shared_context 'Debian repository shared context' do |object_type|
include_context 'workhorse headers'
before do
stub_feature_flags(debian_packages: true)
end
......@@ -37,16 +39,15 @@ RSpec.shared_context 'Debian repository shared context' do |object_type|
let(:params) { workhorse_params }
let(:auth_headers) { {} }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_headers) do
let(:wh_headers) do
if method == :put
{ 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
workhorse_headers
else
{}
end
end
let(:headers) { auth_headers.merge(workhorse_headers) }
let(:headers) { auth_headers.merge(wh_headers) }
let(:send_rewritten_field) { true }
......
......@@ -123,7 +123,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
context 'with a request that bypassed gitlab-workhorse' do
let(:headers) do
basic_auth_header(user.username, personal_access_token.token)
.merge(workhorse_header)
.merge(workhorse_headers)
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
end
......
......@@ -24,7 +24,7 @@ end
RSpec.shared_examples 'deploy token for package uploads' do
context 'with deploy token headers' do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
......@@ -35,7 +35,7 @@ RSpec.shared_examples 'deploy token for package uploads' do
end
context 'invalid token' do
let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) }
let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
......@@ -102,7 +102,7 @@ end
RSpec.shared_examples 'job token for package uploads' do
context 'with job token headers' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
......@@ -114,13 +114,13 @@ RSpec.shared_examples 'job token for package uploads' do
end
context 'invalid token' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
context 'invalid user' do
let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment