Commit e5fc3be1 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents ea338a0b e8867a28
......@@ -109,7 +109,6 @@ linters:
- 'app/views/groups/runners/edit.html.haml'
- 'app/views/groups/settings/_advanced.html.haml'
- 'app/views/groups/settings/_lfs.html.haml'
- 'app/views/help/_shortcuts.html.haml'
- 'app/views/help/index.html.haml'
- 'app/views/help/instance_configuration.html.haml'
- 'app/views/help/instance_configuration/_gitlab_ci.html.haml'
......
......@@ -3,12 +3,11 @@ import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
import { flatten } from 'lodash';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
import findAndFollowLink from '~/lib/utils/navigation_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
import ShortcutsToggle from './shortcuts_toggle.vue';
import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
......@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo
return defaultStopCallback.call(this, e, element, combo);
};
function initToggleButton() {
return new Vue({
el: document.querySelector('.js-toggle-shortcuts'),
render(createElement) {
return createElement(ShortcutsToggle);
},
});
}
/**
* The key used to save and fetch the local Mousetrap instance
* attached to a `<textarea>` element using `jQuery.data`
......@@ -65,7 +55,8 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
this.helpModalElement = null;
this.helpModalVueInstance = null;
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
......@@ -107,11 +98,33 @@ export default class Shortcuts {
}
onToggleHelp(e) {
if (e.preventDefault) {
if (e?.preventDefault) {
e.preventDefault();
}
Shortcuts.toggleHelp(this.enabledHelp);
if (this.helpModalElement && this.helpModalVueInstance) {
this.helpModalVueInstance.$destroy();
this.helpModalElement.remove();
this.helpModalElement = null;
this.helpModalVueInstance = null;
} else {
this.helpModalElement = document.createElement('div');
document.body.append(this.helpModalElement);
this.helpModalVueInstance = new Vue({
el: this.helpModalElement,
components: {
ShortcutsHelp: () => import('./shortcuts_help.vue'),
},
render: (createElement) => {
return createElement('shortcuts-help', {
on: {
hidden: this.onToggleHelp,
},
});
},
});
}
}
static onTogglePerfBar(e) {
......@@ -144,34 +157,6 @@ export default class Shortcuts {
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
static toggleHelp(location) {
const $modal = $('#modal-shortcuts');
if ($modal.length) {
$modal.modal('toggle');
return null;
}
return axios
.get(gon.shortcuts_path, {
responseType: 'text',
})
.then(({ data }) => {
$.globalEval(data, { nonce: getCspNonceValue() });
if (location && location.length > 0) {
const results = [];
for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show());
}
return results;
}
return $('.js-more-help-button').remove();
})
.then(initToggleButton);
}
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
......
This diff is collapsed.
import { initStaticSecurityConfiguration } from '~/security_configuration';
initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
<script>
import ConfigurationTable from './configuration_table.vue';
export default {
components: {
ConfigurationTable,
},
};
</script>
<template>
<article>
<header>
<h4 class="gl-my-5">
{{ __('Security Configuration') }}
</h4>
<h5 class="gl-font-lg gl-mt-7">
{{ s__('SecurityConfiguration|Testing & Compliance') }}
</h5>
</header>
<configuration-table />
</article>
</template>
<script>
import { GlLink, GlSprintf, GlTable, GlAlert } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
import ManageSast from './manage_sast.vue';
import Upgrade from './upgrade.vue';
import { features } from './features_constants';
const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
export default {
components: {
GlLink,
GlSprintf,
GlTable,
GlAlert,
},
data: () => ({
features,
errorMessage: '',
}),
methods: {
getFeatureDocumentationLinkLabel(item) {
return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
featureName: item.name,
});
},
onError(value) {
this.errorMessage = value;
},
getComponentForItem(item) {
const COMPONENTS = {
[REPORT_TYPE_SAST]: ManageSast,
[REPORT_TYPE_DAST]: Upgrade,
[REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
[REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
[REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
[REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
};
return COMPONENTS[item.type];
},
},
table: {
fields: [
{
key: 'feature',
label: s__('SecurityConfiguration|Security Control'),
thClass,
},
{
key: 'manage',
label: s__('SecurityConfiguration|Manage'),
thClass,
},
],
items: features,
},
};
</script>
<template>
<div>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
<template #cell(feature)="{ item }">
<div class="gl-text-gray-900">
{{ item.name }}
</div>
<div>
{{ item.description }}
<gl-link
target="_blank"
:href="item.link"
:aria-label="getFeatureDocumentationLinkLabel(item)"
>
{{ s__('SecurityConfiguration|More information') }}
</gl-link>
</div>
</template>
<template #cell(manage)="{ item }">
<component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
</template>
</gl-table>
</div>
</template>
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_SECRET_DETECTION,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
/**
* Translations & helpPagePaths for Static Security Configuration Page
*/
export const SAST_NAME = s__('Static Application Security Testing (SAST)');
export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.');
export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)');
export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.');
export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const SECRET_DETECTION_NAME = s__('Secret Detection');
export const SECRET_DETECTION_DESCRIPTION = s__(
'Analyze your source code and git history for secrets.',
);
export const SECRET_DETECTION_HELP_PATH = helpPagePath(
'user/application_security/secret_detection/index',
);
export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning');
export const DEPENDENCY_SCANNING_DESCRIPTION = s__(
'Analyze your dependencies for known vulnerabilities.',
);
export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/dependency_scanning/index',
);
export const CONTAINER_SCANNING_NAME = s__('Container Scanning');
export const CONTAINER_SCANNING_DESCRIPTION = s__(
'Check your Docker images for known vulnerabilities.',
);
export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/container_scanning/index',
);
export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing');
export const COVERAGE_FUZZING_DESCRIPTION = s__(
'Find bugs in your code with coverage-guided fuzzing.',
);
export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
'user/application_security/coverage_fuzzing/index',
);
export const LICENSE_COMPLIANCE_NAME = s__('License Compliance');
export const LICENSE_COMPLIANCE_DESCRIPTION = s__(
'Search your project dependencies for their licenses and apply policies.',
);
export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index',
);
export const UPGRADE_CTA = s__(
'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
);
export const features = [
{
name: SAST_NAME,
description: SAST_DESCRIPTION,
helpPath: SAST_HELP_PATH,
type: REPORT_TYPE_SAST,
},
{
name: DAST_NAME,
description: DAST_DESCRIPTION,
helpPath: DAST_HELP_PATH,
type: REPORT_TYPE_DAST,
},
{
name: SECRET_DETECTION_NAME,
description: SECRET_DETECTION_DESCRIPTION,
helpPath: SECRET_DETECTION_HELP_PATH,
type: REPORT_TYPE_SECRET_DETECTION,
},
{
name: DEPENDENCY_SCANNING_NAME,
description: DEPENDENCY_SCANNING_DESCRIPTION,
helpPath: DEPENDENCY_SCANNING_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
},
{
name: CONTAINER_SCANNING_NAME,
description: CONTAINER_SCANNING_DESCRIPTION,
helpPath: CONTAINER_SCANNING_HELP_PATH,
type: REPORT_TYPE_CONTAINER_SCANNING,
},
{
name: COVERAGE_FUZZING_NAME,
description: COVERAGE_FUZZING_DESCRIPTION,
helpPath: COVERAGE_FUZZING_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
},
{
name: LICENSE_COMPLIANCE_NAME,
description: LICENSE_COMPLIANCE_DESCRIPTION,
helpPath: LICENSE_COMPLIANCE_HELP_PATH,
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
<script>
import { GlButton } from '@gitlab/ui';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
},
inject: {
projectPath: {
from: 'projectPath',
default: '',
},
},
data: () => ({
isLoading: false,
}),
methods: {
async mutate() {
this.isLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: configureSastMutation,
variables: {
input: {
projectPath: this.projectPath,
configuration: { global: [], pipeline: [], analyzers: [] },
},
},
});
const { errors, successPath } = data.configureSast;
if (errors.length > 0) {
throw new Error(errors[0]);
}
if (!successPath) {
throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
}
redirectTo(successPath);
} catch (e) {
this.$emit('error', e.message);
this.isLoading = false;
}
},
},
};
</script>
<template>
<gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
s__('SecurityConfiguration|Configure via Merge Request')
}}</gl-button>
</template>
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { UPGRADE_CTA } from './features_constants';
export default {
components: {
GlLink,
GlSprintf,
},
i18n: {
UPGRADE_CTA,
},
};
</script>
<template>
<span>
<gl-sprintf :message="$options.i18n.UPGRADE_CTA">
<template #link="{ content }">
<gl-link target="_blank" href="https://about.gitlab.com/pricing/">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</span>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import SecurityConfigurationApp from './components/app.vue';
export const initStaticSecurityConfiguration = (el) => {
if (!el) {
return null;
}
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { projectPath } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
projectPath,
},
render(createElement) {
return createElement(SecurityConfigurationApp);
},
});
};
......@@ -56,7 +56,10 @@ export default {
mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
this.removeSourceBranch =
data.project.mergeRequest.shouldRemoveSourceBranch ||
data.project.mergeRequest.forceRemoveSourceBranch ||
false;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly;
......
......@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project {
mergeRequest(iid: $iid) {
autoMergeEnabled
shouldRemoveSourceBranch
forceRemoveSourceBranch
defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
......
......@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
......
This diff is collapsed.
:plain
$("body").append("#{escape_javascript(render('shortcuts'))}");
$("#modal-shortcuts").modal();
- breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration")
#js-security-configuration-static
#js-security-configuration-static{ data: {project_path: @project.full_path} }
---
title: Remove namespace_onboarding_actions table
merge_request: 53488
author:
type: other
---
title: Fix charts sometimes being hidden on milestone page
merge_request: 52552
author:
type: fixed
......@@ -3,3 +3,4 @@ filenames:
- 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
- ee/app/assets/javascripts/on_demand_scans/graphql/dast_profile_update.mutation.graphql
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/create_api_fuzzing_configuration.mutation.graphql
# frozen_string_literal: true
class RemoveNamespaceIdForeignKeyOnNamespaceOnboardingActions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
remove_foreign_key :namespace_onboarding_actions, :namespaces
end
end
def down
with_lock_retries do
add_foreign_key :namespace_onboarding_actions, :namespaces, on_delete: :cascade
end
end
end
# frozen_string_literal: true
class RemoveNamespaceOnboardingActionsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
drop_table :namespace_onboarding_actions
end
end
def down
with_lock_retries do
create_table :namespace_onboarding_actions do |t|
t.references :namespace, index: true, null: false
t.datetime_with_timezone :created_at, null: false
t.integer :action, limit: 2, null: false
end
end
end
end
cdf55e9f2b1b9c375920198a438d29fe3c9ab7147f3c670b0d66b11d499573d9
\ No newline at end of file
d9cfb7515805e642c562b8be58b6cd482c24e62e76245db35a7d91b25c327d8d
\ No newline at end of file
......@@ -14289,22 +14289,6 @@ CREATE TABLE namespace_limits (
temporary_storage_increase_ends_on date
);
CREATE TABLE namespace_onboarding_actions (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
action smallint NOT NULL
);
CREATE SEQUENCE namespace_onboarding_actions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE namespace_onboarding_actions_id_seq OWNED BY namespace_onboarding_actions.id;
CREATE TABLE namespace_package_settings (
namespace_id bigint NOT NULL,
maven_duplicates_allowed boolean DEFAULT true NOT NULL,
......@@ -19097,8 +19081,6 @@ ALTER TABLE ONLY metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_seq'::regclass);
ALTER TABLE ONLY namespace_onboarding_actions ALTER COLUMN id SET DEFAULT nextval('namespace_onboarding_actions_id_seq'::regclass);
ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass);
ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
......@@ -20443,9 +20425,6 @@ ALTER TABLE ONLY namespace_aggregation_schedules
ALTER TABLE ONLY namespace_limits
ADD CONSTRAINT namespace_limits_pkey PRIMARY KEY (namespace_id);
ALTER TABLE ONLY namespace_onboarding_actions
ADD CONSTRAINT namespace_onboarding_actions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT namespace_package_settings_pkey PRIMARY KEY (namespace_id);
......@@ -22620,8 +22599,6 @@ CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_time_to_merge ON me
CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id);
CREATE INDEX index_namespace_onboarding_actions_on_namespace_id ON namespace_onboarding_actions USING btree (namespace_id);
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
......@@ -25160,9 +25137,6 @@ ALTER TABLE ONLY merge_request_assignees
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_4437bf4070 FOREIGN KEY (dependency_id) REFERENCES packages_dependencies(id) ON DELETE CASCADE;
ALTER TABLE ONLY namespace_onboarding_actions
ADD CONSTRAINT fk_rails_4504f6875a FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_auto_devops
ADD CONSTRAINT fk_rails_45436b12b2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
......
......@@ -6,6 +6,38 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# HTML style guide
## Semantic elements
[Semantic elements](https://developer.mozilla.org/en-US/docs/Glossary/Semantics) are HTML tags that
give semantic (rather than presentational) meaning to the data they contain. For example:
- [`<article>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article)
- [`<nav>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav)
- [`<strong>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong)
Prefer using semantic tags, but only if the intention is truly accurate with the semantic meaning
of the tag itself. View the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
for a description on what each tag semantically means.
```html
<!-- bad - could use semantic tags instead of div's. -->
<div class="...">
<p>
<!-- bad - this isn't what "strong" is meant for. -->
Simply visit your <strong>Settings</strong> to say hello to the world.
</p>
<div class="...">...</div>
</div>
<!-- good - prefer semantic classes used accurately -->
<section class="...">
<p>
Simply visit your <span class="gl-font-weight-bold">Settings</span> to say hello to the world.
</p>
<footer class="...">...</footer>
</section>
```
## Buttons
### Button type
......
......@@ -369,6 +369,7 @@ export default {
:open-issues-count="issuesCount"
:open-issues-weight="issuesWeight"
:issues-selected="issuesSelected"
:loading="loading"
class="col-md-6"
/>
<burnup-chart
......@@ -376,6 +377,7 @@ export default {
:due-date="dueDate"
:burnup-data="burnupData"
:issues-selected="issuesSelected"
:loading="loading"
class="col-md-6"
/>
</div>
......
......@@ -40,6 +40,11 @@ export default {
required: false,
default: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -126,7 +131,7 @@ export default {
<div v-if="showTitle" class="burndown-header d-flex align-items-center">
<h3>{{ __('Burndown chart') }}</h3>
</div>
<resizable-chart-container class="burndown-chart js-burndown-chart">
<resizable-chart-container v-if="!loading" class="burndown-chart js-burndown-chart">
<gl-line-chart
slot-scope="{ width }"
:width="width"
......
......@@ -30,6 +30,11 @@ export default {
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -114,8 +119,10 @@ export default {
<div class="burndown-header d-flex align-items-center">
<h3>{{ __('Burnup chart') }}</h3>
</div>
<resizable-chart-container class="js-burnup-chart">
<resizable-chart-container v-if="!loading" class="js-burnup-chart">
<gl-line-chart
slot-scope="{ width }"
:width="width"
:data="dataSeries"
:option="options"
:format-tooltip-text="formatTooltipText"
......
import initSecurityConfiguration from 'ee/security_configuration';
import { initSecurityConfiguration } from 'ee/security_configuration';
import { initStaticSecurityConfiguration } from '~/security_configuration';
initSecurityConfiguration();
const el = document.querySelector('#js-security-configuration');
if (el) {
initSecurityConfiguration(el);
} else {
initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
}
......@@ -11,6 +11,7 @@ export const initApiFuzzingConfiguration = () => {
}
const {
securityConfigurationPath,
fullPath,
apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath,
......@@ -23,6 +24,7 @@ export const initApiFuzzingConfiguration = () => {
el,
apolloProvider,
provide: {
securityConfigurationPath,
fullPath,
apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath,
......
......@@ -10,13 +10,18 @@ import {
GlLink,
GlSprintf,
} from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale';
import { SCAN_MODES } from '../constants';
import { isEmptyValue } from '~/lib/utils/forms';
import { SCAN_MODES, CONFIGURATION_SNIPPET_MODAL_ID } from '../constants';
import createApiFuzzingConfigurationMutation from '../graphql/create_api_fuzzing_configuration.mutation.graphql';
import DropdownInput from '../../components/dropdown_input.vue';
import DynamicFields from '../../components/dynamic_fields.vue';
import FormInput from '../../components/form_input.vue';
import ConfigurationSnippetModal from './configuration_snippet_modal.vue';
export default {
CONFIGURATION_SNIPPET_MODAL_ID,
components: {
GlAccordion,
GlAccordionItem,
......@@ -27,24 +32,19 @@ export default {
GlFormCheckbox,
GlLink,
GlSprintf,
ConfigurationSnippetModal,
DropdownInput,
DynamicFields,
FormInput,
},
inject: {
apiFuzzingAuthenticationDocumentationPath: {
from: 'apiFuzzingAuthenticationDocumentationPath',
},
ciVariablesDocumentationPath: {
from: 'ciVariablesDocumentationPath',
},
projectCiSettingsPath: {
from: 'projectCiSettingsPath',
},
canSetProjectCiVariables: {
from: 'canSetProjectCiVariables',
},
},
inject: [
'securityConfigurationPath',
'fullPath',
'apiFuzzingAuthenticationDocumentationPath',
'ciVariablesDocumentationPath',
'projectCiSettingsPath',
'canSetProjectCiVariables',
],
props: {
apiFuzzingCiConfiguration: {
type: Object,
......@@ -53,6 +53,8 @@ export default {
},
data() {
return {
isLoading: false,
showError: false,
targetUrl: {
field: 'targetUrl',
label: s__('APIFuzzing|Target URL'),
......@@ -111,6 +113,8 @@ export default {
}),
),
},
ciYamlEditUrl: '',
configurationYaml: '',
};
},
computed: {
......@@ -142,9 +146,60 @@ export default {
({ name }) => name === this.scanProfile.value,
)?.yaml;
},
someFieldEmpty() {
const fields = [this.targetUrl, this.scanMode, this.apiSpecificationFile, this.scanProfile];
if (this.authenticationEnabled) {
fields.push(...this.authenticationSettings);
}
return fields.some(({ value }) => isEmptyValue(value));
},
},
methods: {
onSubmit() {},
async onSubmit() {
this.isLoading = true;
this.showError = false;
try {
const input = {
projectPath: this.fullPath,
target: this.targetUrl.value,
scanMode: this.scanMode.value,
apiSpecificationFile: this.apiSpecificationFile.value,
scanProfile: this.scanProfile.value,
};
if (this.authenticationEnabled) {
const [authUsername, authPassword] = this.authenticationSettings;
input.authUsername = authUsername.value;
input.authPassword = authPassword.value;
}
const {
data: {
createApiFuzzingCiConfiguration: {
gitlabCiYamlEditUrl,
configurationYaml,
errors = [],
},
},
} = await this.$apollo.mutate({
mutation: createApiFuzzingConfigurationMutation,
variables: { input },
});
if (errors.length) {
this.showError = true;
} else {
this.ciYamlEditUrl = gitlabCiYamlEditUrl;
this.configurationYaml = configurationYaml;
this.$refs[CONFIGURATION_SNIPPET_MODAL_ID].show();
}
} catch (e) {
this.showError = true;
Sentry.captureException(e);
} finally {
this.isLoading = false;
}
},
dismissError() {
this.showError = false;
},
},
SCAN_MODES,
};
......@@ -152,6 +207,10 @@ export default {
<template>
<form @submit.prevent="onSubmit">
<gl-alert v-if="showError" variant="danger" class="gl-mb-5" @dismiss="dismissError">
{{ s__('APIFuzzing|The configuration could not be saved, please try again later.') }}
</gl-alert>
<form-input v-model="targetUrl.value" v-bind="targetUrl" class="gl-mb-7" />
<dropdown-input v-model="scanMode.value" v-bind="scanMode" />
......@@ -223,9 +282,26 @@ export default {
<hr />
<gl-button type="submit" variant="confirm">{{
s__('APIFuzzing|Generate code snippet')
}}</gl-button>
<gl-button>{{ __('Cancel') }}</gl-button>
<gl-button
:disabled="someFieldEmpty"
:loading="isLoading"
type="submit"
variant="confirm"
class="js-no-auto-disable"
data-testid="api-fuzzing-configuration-submit-button"
>{{ s__('APIFuzzing|Generate code snippet') }}</gl-button
>
<gl-button
:disabled="isLoading"
:href="securityConfigurationPath"
data-testid="api-fuzzing-configuration-cancel-button"
>{{ __('Cancel') }}</gl-button
>
<configuration-snippet-modal
:ref="$options.CONFIGURATION_SNIPPET_MODAL_ID"
:ci-yaml-edit-url="ciYamlEditUrl"
:yaml="configurationYaml"
/>
</form>
</template>
<script>
import { GlModal } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { redirectTo } from '~/lib/utils/url_utility';
import { CONFIGURATION_SNIPPET_MODAL_ID } from '../constants';
export default {
CONFIGURATION_SNIPPET_MODAL_ID,
components: {
GlModal,
},
props: {
ciYamlEditUrl: {
type: String,
required: true,
},
yaml: {
type: String,
required: true,
},
},
methods: {
show() {
this.$refs.modal.show();
},
onHide() {
this.clipboard?.destroy();
},
copySnippet(andRedirect = true) {
const id = andRedirect ? 'copy-yaml-snippet-and-edit-button' : 'copy-yaml-snippet-button';
const clipboard = new Clipboard(`#${id}`, {
text: () => this.yaml,
});
clipboard.on('success', () => {
if (andRedirect) {
redirectTo(this.ciYamlEditUrl);
}
});
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:action-primary="{
text: s__('APIFuzzing|Copy code and open .gitlab-ci.yml file'),
attributes: [{ variant: 'confirm' }, { id: 'copy-yaml-snippet-and-edit-button' }],
}"
:action-secondary="{
text: s__('APIFuzzing|Copy code only'),
attributes: [{ variant: 'default' }, { id: 'copy-yaml-snippet-button' }],
}"
:action-cancel="{
text: __('Cancel'),
}"
:modal-id="$options.CONFIGURATION_SNIPPET_MODAL_ID"
:title="s__('APIFuzzing|Code snippet for the API Fuzzing configuration')"
@hide="onHide"
@primary="copySnippet"
@secondary="copySnippet(false)"
>
<pre><code data-testid="api-fuzzing-modal-yaml-snippet" v-text="yaml"></code></pre>
</gl-modal>
</template>
......@@ -18,3 +18,5 @@ export const SCAN_MODES = {
),
},
};
export const CONFIGURATION_SNIPPET_MODAL_ID = 'CONFIGURATION_SNIPPET_MODAL_ID';
mutation($input: CreateApiFuzzingCiConfigurationInput!) {
createApiFuzzingCiConfiguration(input: $input) {
configurationYaml
gitlabCiYamlEditUrl
errors
}
}
......@@ -2,9 +2,7 @@ import Vue from 'vue';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
export default function init() {
const el = document.getElementById('js-security-configuration');
export const initSecurityConfiguration = (el) => {
if (!el) {
return null;
}
......@@ -58,4 +56,4 @@ export default function init() {
});
},
});
}
};
<script>
import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
import DynamicFields from '../../components/dynamic_fields.vue';
import ExpandableSection from '../../components/expandable_section.vue';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import AnalyzerConfiguration from './analyzer_configuration.vue';
import {
toSastCiConfigurationEntityInput,
......
/* eslint-disable import/export */
import { invert } from 'lodash';
import { reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE } from '~/vue_shared/security_reports/constants';
import {
reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE,
REPORT_TYPE_API_FUZZING,
} from '~/vue_shared/security_reports/constants';
export * from '~/vue_shared/security_reports/constants';
/**
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
*
......
......@@ -3,6 +3,7 @@
module Projects::Security::ApiFuzzingConfigurationHelper
def api_fuzzing_configuration_data(project)
{
security_configuration_path: project_security_configuration_path(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'),
......
......@@ -39,7 +39,7 @@ describe('burndown_chart', () => {
burndownEventsPath: '/api/v4/projects/1234/milestones/1/burndown_events',
};
const createComponent = ({ props = {}, data = {} } = {}) => {
const createComponent = ({ props = {}, data = {}, loading = false } = {}) => {
wrapper = shallowMount(BurnCharts, {
propsData: {
...defaultProps,
......@@ -49,7 +49,7 @@ describe('burndown_chart', () => {
$apollo: {
queries: {
report: {
loading: false,
loading,
},
},
},
......@@ -69,6 +69,20 @@ describe('burndown_chart', () => {
wrapper.destroy();
});
it('passes loading=true through to charts', () => {
createComponent({ loading: true });
expect(findBurndownChart().props('loading')).toBe(true);
expect(findBurnupChart().props('loading')).toBe(true);
});
it('passes loading=false through to charts', () => {
createComponent({ loading: false });
expect(findBurndownChart().props('loading')).toBe(false);
expect(findBurnupChart().props('loading')).toBe(false);
});
it('includes Issues and Issue weight buttons', () => {
createComponent();
......
......@@ -27,6 +27,18 @@ describe('burndown_chart', () => {
});
};
it('hides chart while loading', () => {
createComponent({ loading: true });
expect(findChart().exists()).toBe(false);
});
it('shows chart when not loading', () => {
createComponent({ loading: false });
expect(findChart().exists()).toBe(true);
});
describe('with single point', () => {
it('does not show guideline', () => {
createComponent({
......
......@@ -26,6 +26,18 @@ describe('Burnup chart', () => {
});
};
it('hides chart while loading', () => {
createComponent({ loading: true });
expect(findChart().exists()).toBe(false);
});
it('shows chart when not loading', () => {
createComponent({ loading: false });
expect(findChart().exists()).toBe(true);
});
it('renders the lineChart correctly', () => {
const burnupData = [day1, day2, day3];
......
import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import { GlAlert } from '@gitlab/ui';
import { stripTypenames } from 'helpers/graphql_helpers';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { SCAN_MODES } from 'ee/security_configuration/api_fuzzing/constants';
import waitForPromises from 'helpers/wait_for_promises';
import {
SCAN_MODES,
CONFIGURATION_SNIPPET_MODAL_ID,
} from 'ee/security_configuration/api_fuzzing/constants';
import ConfigurationForm from 'ee/security_configuration/api_fuzzing/components/configuration_form.vue';
import ConfigurationSnippetModal from 'ee/security_configuration/api_fuzzing/components/configuration_snippet_modal.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.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(),
});
import {
apiFuzzingConfigurationQueryResponse,
createApiFuzzingConfigurationMutationResponse,
} from '../mock_data';
describe('EE - ApiFuzzingConfigurationForm', () => {
let wrapper;
const apiFuzzingCiConfiguration = {
scanModes: Object.keys(SCAN_MODES),
scanProfiles: [makeScanProfile('Quick-10'), makeScanProfile('Medium-20')],
};
const apiFuzzingCiConfiguration = stripTypenames(
apiFuzzingConfigurationQueryResponse.data.project.apiFuzzingCiConfiguration,
);
const findAlert = () => wrapper.find(GlAlert);
const findEnableAuthenticationCheckbox = () =>
wrapper.findByTestId('api-fuzzing-enable-authentication-checkbox');
const findTargetUrlInput = () => wrapper.findAll(FormInput).at(0);
const findScanModeInput = () => wrapper.findAll(DropdownInput).at(0);
const findSpecificationFileInput = () => wrapper.findAll(FormInput).at(1);
const findAuthenticationNotice = () => wrapper.findByTestId('api-fuzzing-authentication-notice');
const findAuthenticationFields = () => wrapper.find(DynamicFields);
const findScanProfileDropdownInput = () => wrapper.findAll(DropdownInput).at(1);
const findScanProfileYamlViewer = () =>
wrapper.findByTestId('api-fuzzing-scan-profile-yaml-viewer');
const findSubmitButton = () => wrapper.findByTestId('api-fuzzing-configuration-submit-button');
const findCancelButton = () => wrapper.findByTestId('api-fuzzing-configuration-cancel-button');
const findConfigurationSnippetModal = () => wrapper.find(ConfigurationSnippetModal);
const setFormData = async () => {
findTargetUrlInput().vm.$emit('input', 'https://gitlab.com');
await findScanModeInput().vm.$emit('input', 'HAR');
findSpecificationFileInput().vm.$emit('input', '/specification/file/path');
return findScanProfileDropdownInput().vm.$emit(
'input',
apiFuzzingCiConfiguration.scanProfiles[0].name,
);
};
const createWrapper = (options = {}) => {
wrapper = extendedWrapper(
......@@ -39,6 +57,8 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
merge(
{
provide: {
fullPath: 'namespace/project',
securityConfigurationPath: '/security/configuration',
apiFuzzingAuthenticationDocumentationPath:
'api_fuzzing_authentication/documentation/path',
ciVariablesDocumentationPath: '/ci_cd_variables/documentation/path',
......@@ -48,6 +68,11 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
propsData: {
apiFuzzingCiConfiguration,
},
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
},
options,
),
......@@ -156,12 +181,112 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
});
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);
const [selectedScanProfile] = apiFuzzingCiConfiguration.scanProfiles;
findScanProfileDropdownInput().vm.$emit('input', selectedScanProfile.name);
await wrapper.vm.$nextTick();
expect(findScanProfileYamlViewer().exists()).toBe(true);
expect(findScanProfileYamlViewer().text()).toBe(selectedScanProfile.yaml);
expect(findScanProfileYamlViewer().text()).toBe(selectedScanProfile.yaml.trim());
});
});
describe('form submission', () => {
it('cancel button points to Security Configuration page', () => {
createWrapper();
expect(findCancelButton().attributes('href')).toBe('/security/configuration');
});
it('submit button is disabled until all fields are filled', async () => {
createWrapper();
expect(findSubmitButton().props('disabled')).toBe(true);
await setFormData();
expect(findSubmitButton().props('disabled')).toBe(false);
await findEnableAuthenticationCheckbox().trigger('click');
expect(findSubmitButton().props('disabled')).toBe(true);
await findAuthenticationFields().vm.$emit('input', [
{
...wrapper.vm.authenticationSettings[0],
value: '$UsernameVariable',
},
{
...wrapper.vm.authenticationSettings[1],
value: '$PasswordVariable',
},
]);
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('triggers the createApiFuzzingConfiguration mutation on submit and opens the modal with the correct props', async () => {
createWrapper();
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue(createApiFuzzingConfigurationMutationResponse);
jest.spyOn(wrapper.vm.$refs[CONFIGURATION_SNIPPET_MODAL_ID], 'show');
await setFormData();
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().exists()).toBe(false);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
expect(wrapper.vm.$refs[CONFIGURATION_SNIPPET_MODAL_ID].show).toHaveBeenCalled();
expect(findConfigurationSnippetModal().props()).toEqual({
ciYamlEditUrl:
createApiFuzzingConfigurationMutationResponse.data.createApiFuzzingCiConfiguration
.gitlabCiYamlEditUrl,
yaml:
createApiFuzzingConfigurationMutationResponse.data.createApiFuzzingCiConfiguration
.configurationYaml,
});
});
it('shows an error on top-level error', async () => {
createWrapper({
mocks: {
$apollo: {
mutate: jest.fn().mockRejectedValue(),
},
},
});
await setFormData();
expect(findAlert().exists()).toBe(false);
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().exists()).toBe(true);
});
it('shows an error on error-as-data', async () => {
createWrapper({
mocks: {
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
createApiFuzzingCiConfiguration: {
errors: ['error#1'],
},
},
}),
},
},
});
await setFormData();
expect(findAlert().exists()).toBe(false);
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import Clipboard from 'clipboard';
import { GlModal } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ConfigurationSnippetModal from 'ee/security_configuration/api_fuzzing/components/configuration_snippet_modal.vue';
import { createApiFuzzingConfigurationMutationResponse } from '../mock_data';
jest.mock('clipboard', () =>
jest.fn().mockImplementation(() => ({
on: jest.fn().mockImplementation((_event, cb) => cb()),
})),
);
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const {
gitlabCiYamlEditUrl,
configurationYaml,
} = createApiFuzzingConfigurationMutationResponse.data.createApiFuzzingCiConfiguration;
describe('EE - ApiFuzzingConfigurationSnippetModal', () => {
let wrapper;
const findModal = () => wrapper.find(GlModal);
const findYamlSnippet = () => wrapper.findByTestId('api-fuzzing-modal-yaml-snippet');
const createWrapper = (options) => {
wrapper = extendedWrapper(
shallowMount(
ConfigurationSnippetModal,
merge(
{
propsData: {
ciYamlEditUrl: gitlabCiYamlEditUrl,
yaml: configurationYaml,
},
attrs: {
static: true,
visible: true,
},
},
options,
),
),
);
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the YAML snippet', () => {
expect(findYamlSnippet().text()).toBe(configurationYaml);
});
it('on primary event, text is copied to the clipbard and user is redirected to CI editor', async () => {
findModal().vm.$emit('primary');
expect(Clipboard).toHaveBeenCalledWith('#copy-yaml-snippet-and-edit-button', {
text: expect.any(Function),
});
expect(redirectTo).toHaveBeenCalledWith(gitlabCiYamlEditUrl);
});
it('on secondary event, text is copied to the clipbard', async () => {
findModal().vm.$emit('secondary');
expect(Clipboard).toHaveBeenCalledWith('#copy-yaml-snippet-button', {
text: expect.any(Function),
});
});
});
......@@ -39,3 +39,14 @@ export const apiFuzzingConfigurationQueryResponse = {
},
},
};
export const createApiFuzzingConfigurationMutationResponse = {
data: {
createApiFuzzingCiConfiguration: {
configurationYaml: 'yaml snippet',
gitlabCiYamlEditUrl: '/ci/editor',
errors: [],
__typename: 'ApiFuzzingCiConfiguration',
},
},
};
......@@ -5,7 +5,7 @@ import AnalyzerConfiguration from 'ee/security_configuration/sast/components/ana
import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
import configureSastMutation from 'ee/security_configuration/sast/graphql/configure_sast.mutation.graphql';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper';
import { makeEntities, makeSastCiConfiguration } from '../../helpers';
......
......@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:security_configuration_path) { project_security_configuration_path(project) }
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') }
......@@ -25,6 +26,7 @@ RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
it {
is_expected.to eq(
security_configuration_path: security_configuration_path,
full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
......@@ -42,6 +44,7 @@ RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
it {
is_expected.to eq(
security_configuration_path: security_configuration_path,
full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
......
......@@ -13,7 +13,6 @@ module Gitlab
gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
if Gitlab.config.sentry.enabled
......
......@@ -148,7 +148,7 @@ module Gitlab
end
def load_yaml_from_path(path)
YAML.safe_load(File.read(path))&.map(&:with_indifferent_access)
YAML.safe_load(File.read(path), aliases: true)&.map(&:with_indifferent_access)
end
end
end
......
......@@ -1405,6 +1405,15 @@ msgstr ""
msgid "APIFuzzing|Choose a profile"
msgstr ""
msgid "APIFuzzing|Code snippet for the API Fuzzing configuration"
msgstr ""
msgid "APIFuzzing|Copy code and open .gitlab-ci.yml file"
msgstr ""
msgid "APIFuzzing|Copy code only"
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 ""
......@@ -1453,6 +1462,9 @@ msgstr ""
msgid "APIFuzzing|Target URL"
msgstr ""
msgid "APIFuzzing|The configuration could not be saved, please try again later."
msgstr ""
msgid "APIFuzzing|There are two ways to perform scans."
msgstr ""
......@@ -25905,12 +25917,18 @@ msgstr ""
msgid "SecurityConfiguration|Available for on-demand DAST"
msgstr ""
msgid "SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}"
msgstr ""
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request."
msgstr ""
msgid "SecurityConfiguration|Configure"
msgstr ""
msgid "SecurityConfiguration|Configure via Merge Request"
msgstr ""
msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later."
msgstr ""
......@@ -25950,6 +25968,9 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration"
msgstr ""
msgid "SecurityConfiguration|SAST merge request creation mutation failed"
msgstr ""
msgid "SecurityConfiguration|Security Control"
msgstr ""
......
......@@ -23,7 +23,7 @@ RSpec.describe 'Help Pages' do
it 'opens shortcuts help dialog' do
find('.js-trigger-shortcut').click
expect(page).to have_selector('#modal-shortcuts')
expect(page).to have_selector('[data-testid="modal-shortcuts"]')
end
end
end
......
......@@ -27,14 +27,13 @@ RSpec.describe 'User uses shortcuts', :js do
open_modal_shortcut_keys
# modal-shortcuts still in the DOM, but hidden
expect(find('#modal-shortcuts', visible: false)).not_to be_visible
expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
page.refresh
open_modal_shortcut_keys
# after reload, shortcuts modal doesn't exist at all until we add it
expect(page).not_to have_selector('#modal-shortcuts')
expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
end
it 're-enables shortcuts' do
......@@ -47,7 +46,7 @@ RSpec.describe 'User uses shortcuts', :js do
close_modal
open_modal_shortcut_keys
expect(find('#modal-shortcuts')).to be_visible
expect(find('[data-testid="modal-shortcuts"]')).to be_visible
end
def open_modal_shortcut_keys
......
import { shallowMount } from '@vue/test-utils';
import App from '~/security_configuration/components/app.vue';
import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
describe('App Component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(App, {});
};
const findConfigurationTable = () => wrapper.findComponent(ConfigurationTable);
afterEach(() => {
wrapper.destroy();
});
it('renders correct primary & Secondary Heading', () => {
createComponent();
expect(wrapper.text()).toContain('Security Configuration');
expect(wrapper.text()).toContain('Testing & Compliance');
});
it('renders ConfigurationTable Component', () => {
createComponent();
expect(findConfigurationTable().exists()).toBe(true);
});
});
import { mount } from '@vue/test-utils';
import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
import { features, UPGRADE_CTA } from '~/security_configuration/components/features_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
describe('Configuration Table Component', () => {
let wrapper;
const createComponent = () => {
wrapper = extendedWrapper(mount(ConfigurationTable, {}));
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent();
});
it.each(features)('should match strings', (feature) => {
expect(wrapper.text()).toContain(feature.name);
expect(wrapper.text()).toContain(feature.description);
if (feature.type === REPORT_TYPE_SAST) {
expect(wrapper.findByTestId(feature.type).text()).toBe('Configure via Merge Request');
} else if (
[
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
].includes(feature.type)
) {
expect(wrapper.findByTestId(feature.type).text()).toMatchInterpolatedText(UPGRADE_CTA);
}
});
});
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import ManageSast from '~/security_configuration/components/manage_sast.vue';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
Vue.use(VueApollo);
describe('Manage Sast Component', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
const successHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const noSuccessPathHandler = async () => {
return {
data: {
configureSast: {
successPath: '',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const errorHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: ['foo'],
__typename: 'ConfigureSastPayload',
},
},
};
};
const pendingHandler = () => new Promise(() => {});
function createMockApolloProvider(handler) {
const requestHandlers = [[configureSastMutation, handler]];
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
wrapper = extendedWrapper(
mount(ManageSast, {
apolloProvider: mockApollo,
}),
);
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render Button with correct text', () => {
createComponent();
expect(findButton().text()).toContain('Configure via Merge Request');
});
describe('given a successful response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo });
});
it('should call redirect helper with correct value', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
// This is done for UX reasons. If the loading prop is set to false
// on success, then there's a period where the button is clickable
// again. Instead, we want the button to display a loading indicator
// for the remainder of the lifetime of the page (i.e., until the
// browser can start painting the new page it's been redirected to).
expect(findButton().props().loading).toBe(true);
});
});
describe('given a pending response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(pendingHandler);
createComponent({ mockApollo });
});
it('renders spinner correctly', async () => {
expect(findButton().props('loading')).toBe(false);
await wrapper.trigger('click');
await waitForPromises();
expect(findButton().props('loading')).toBe(true);
});
});
describe.each`
handler | message
${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'}
${errorHandler} | ${'foo'}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(handler);
createComponent({ mockApollo });
});
it('should catch and emit error', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[message]]);
expect(findButton().props('loading')).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import Upgrade from '~/security_configuration/components/upgrade.vue';
import { UPGRADE_CTA } from '~/security_configuration/components/features_constants';
let wrapper;
const createComponent = () => {
wrapper = mount(Upgrade, {});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('Upgrade component', () => {
it('renders correct text in link', () => {
expect(wrapper.text()).toMatchInterpolatedText(UPGRADE_CTA);
});
it('renders link with correct attributes', () => {
expect(wrapper.find('a').attributes()).toMatchObject({
href: 'https://about.gitlab.com/pricing/',
target: '_blank',
});
});
});
......@@ -222,6 +222,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
end
end
it 'allows for YAML aliases in aggregated metrics configs' do
expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true)
described_class.new(recorded_at)
end
describe '.aggregated_metrics_weekly_data' do
subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data }
......
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