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: ...@@ -109,7 +109,6 @@ linters:
- 'app/views/groups/runners/edit.html.haml' - 'app/views/groups/runners/edit.html.haml'
- 'app/views/groups/settings/_advanced.html.haml' - 'app/views/groups/settings/_advanced.html.haml'
- 'app/views/groups/settings/_lfs.html.haml' - 'app/views/groups/settings/_lfs.html.haml'
- 'app/views/help/_shortcuts.html.haml'
- 'app/views/help/index.html.haml' - 'app/views/help/index.html.haml'
- 'app/views/help/instance_configuration.html.haml' - 'app/views/help/instance_configuration.html.haml'
- 'app/views/help/instance_configuration/_gitlab_ci.html.haml' - 'app/views/help/instance_configuration/_gitlab_ci.html.haml'
......
...@@ -3,12 +3,11 @@ import Cookies from 'js-cookie'; ...@@ -3,12 +3,11 @@ import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import Vue from 'vue'; import Vue from 'vue';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils'; import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
import axios from '../../lib/utils/axios_utils'; import findAndFollowLink from '~/lib/utils/navigation_utility';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
import ShortcutsToggle from './shortcuts_toggle.vue';
import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings'; import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback; const defaultStopCallback = Mousetrap.prototype.stopCallback;
...@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo ...@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo
return defaultStopCallback.call(this, 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 * The key used to save and fetch the local Mousetrap instance
* attached to a `<textarea>` element using `jQuery.data` * attached to a `<textarea>` element using `jQuery.data`
...@@ -65,7 +55,8 @@ function getToolbarBtnToShortcutsMap($textarea) { ...@@ -65,7 +55,8 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts { export default class Shortcuts {
constructor() { constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this); this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = []; this.helpModalElement = null;
this.helpModalVueInstance = null;
Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('s', Shortcuts.focusSearch);
...@@ -107,11 +98,33 @@ export default class Shortcuts { ...@@ -107,11 +98,33 @@ export default class Shortcuts {
} }
onToggleHelp(e) { onToggleHelp(e) {
if (e.preventDefault) { if (e?.preventDefault) {
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) { static onTogglePerfBar(e) {
...@@ -144,34 +157,6 @@ export default class Shortcuts { ...@@ -144,34 +157,6 @@ export default class Shortcuts {
$(document).triggerHandler('markdown-preview:toggle', [e]); $(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) { focusFilter(e) {
if (!this.filterInput) { if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls'); 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 { ...@@ -56,7 +56,10 @@ export default {
mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled, mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds, 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.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge; this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly; this.isSquashReadOnly = data.project.squashReadOnly;
......
...@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project { ...@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project {
mergeRequest(iid: $iid) { mergeRequest(iid: $iid) {
autoMergeEnabled autoMergeEnabled
shouldRemoveSourceBranch shouldRemoveSourceBranch
forceRemoveSourceBranch
defaultMergeCommitMessage defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage defaultSquashCommitMessage
......
...@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = { ...@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend. * Security scan report types, as provided by the backend.
*/ */
export const REPORT_TYPE_SAST = 'sast'; export const REPORT_TYPE_SAST = 'sast';
export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection'; 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. * SecurityReportTypeEnum values for use with GraphQL.
......
This diff is collapsed.
:plain
$("body").append("#{escape_javascript(render('shortcuts'))}");
$("#modal-shortcuts").modal();
- breadcrumb_title _("Security Configuration") - breadcrumb_title _("Security Configuration")
- page_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: ...@@ -3,3 +3,4 @@ filenames:
- ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.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 - 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/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 ( ...@@ -14289,22 +14289,6 @@ CREATE TABLE namespace_limits (
temporary_storage_increase_ends_on date 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 ( CREATE TABLE namespace_package_settings (
namespace_id bigint NOT NULL, namespace_id bigint NOT NULL,
maven_duplicates_allowed boolean DEFAULT true 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 ...@@ -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 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 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); ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
...@@ -20443,9 +20425,6 @@ ALTER TABLE ONLY namespace_aggregation_schedules ...@@ -20443,9 +20425,6 @@ ALTER TABLE ONLY namespace_aggregation_schedules
ALTER TABLE ONLY namespace_limits ALTER TABLE ONLY namespace_limits
ADD CONSTRAINT namespace_limits_pkey PRIMARY KEY (namespace_id); 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 ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT namespace_package_settings_pkey PRIMARY KEY (namespace_id); 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 ...@@ -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 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_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); 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 ...@@ -25160,9 +25137,6 @@ ALTER TABLE ONLY merge_request_assignees
ALTER TABLE ONLY packages_dependency_links ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_4437bf4070 FOREIGN KEY (dependency_id) REFERENCES packages_dependencies(id) ON DELETE CASCADE; 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 ALTER TABLE ONLY project_auto_devops
ADD CONSTRAINT fk_rails_45436b12b2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; 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 ...@@ -6,6 +6,38 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# HTML style guide # 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 ## Buttons
### Button type ### Button type
......
...@@ -369,6 +369,7 @@ export default { ...@@ -369,6 +369,7 @@ export default {
:open-issues-count="issuesCount" :open-issues-count="issuesCount"
:open-issues-weight="issuesWeight" :open-issues-weight="issuesWeight"
:issues-selected="issuesSelected" :issues-selected="issuesSelected"
:loading="loading"
class="col-md-6" class="col-md-6"
/> />
<burnup-chart <burnup-chart
...@@ -376,6 +377,7 @@ export default { ...@@ -376,6 +377,7 @@ export default {
:due-date="dueDate" :due-date="dueDate"
:burnup-data="burnupData" :burnup-data="burnupData"
:issues-selected="issuesSelected" :issues-selected="issuesSelected"
:loading="loading"
class="col-md-6" class="col-md-6"
/> />
</div> </div>
......
...@@ -40,6 +40,11 @@ export default { ...@@ -40,6 +40,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -126,7 +131,7 @@ export default { ...@@ -126,7 +131,7 @@ export default {
<div v-if="showTitle" class="burndown-header d-flex align-items-center"> <div v-if="showTitle" class="burndown-header d-flex align-items-center">
<h3>{{ __('Burndown chart') }}</h3> <h3>{{ __('Burndown chart') }}</h3>
</div> </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 <gl-line-chart
slot-scope="{ width }" slot-scope="{ width }"
:width="width" :width="width"
......
...@@ -30,6 +30,11 @@ export default { ...@@ -30,6 +30,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -114,8 +119,10 @@ export default { ...@@ -114,8 +119,10 @@ export default {
<div class="burndown-header d-flex align-items-center"> <div class="burndown-header d-flex align-items-center">
<h3>{{ __('Burnup chart') }}</h3> <h3>{{ __('Burnup chart') }}</h3>
</div> </div>
<resizable-chart-container class="js-burnup-chart"> <resizable-chart-container v-if="!loading" class="js-burnup-chart">
<gl-line-chart <gl-line-chart
slot-scope="{ width }"
:width="width"
:data="dataSeries" :data="dataSeries"
:option="options" :option="options"
:format-tooltip-text="formatTooltipText" :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 = () => { ...@@ -11,6 +11,7 @@ export const initApiFuzzingConfiguration = () => {
} }
const { const {
securityConfigurationPath,
fullPath, fullPath,
apiFuzzingDocumentationPath, apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath, apiFuzzingAuthenticationDocumentationPath,
...@@ -23,6 +24,7 @@ export const initApiFuzzingConfiguration = () => { ...@@ -23,6 +24,7 @@ export const initApiFuzzingConfiguration = () => {
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
securityConfigurationPath,
fullPath, fullPath,
apiFuzzingDocumentationPath, apiFuzzingDocumentationPath,
apiFuzzingAuthenticationDocumentationPath, apiFuzzingAuthenticationDocumentationPath,
......
...@@ -10,13 +10,18 @@ import { ...@@ -10,13 +10,18 @@ import {
GlLink, GlLink,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale'; 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 DropdownInput from '../../components/dropdown_input.vue';
import DynamicFields from '../../components/dynamic_fields.vue'; import DynamicFields from '../../components/dynamic_fields.vue';
import FormInput from '../../components/form_input.vue'; import FormInput from '../../components/form_input.vue';
import ConfigurationSnippetModal from './configuration_snippet_modal.vue';
export default { export default {
CONFIGURATION_SNIPPET_MODAL_ID,
components: { components: {
GlAccordion, GlAccordion,
GlAccordionItem, GlAccordionItem,
...@@ -27,24 +32,19 @@ export default { ...@@ -27,24 +32,19 @@ export default {
GlFormCheckbox, GlFormCheckbox,
GlLink, GlLink,
GlSprintf, GlSprintf,
ConfigurationSnippetModal,
DropdownInput, DropdownInput,
DynamicFields, DynamicFields,
FormInput, FormInput,
}, },
inject: { inject: [
apiFuzzingAuthenticationDocumentationPath: { 'securityConfigurationPath',
from: 'apiFuzzingAuthenticationDocumentationPath', 'fullPath',
}, 'apiFuzzingAuthenticationDocumentationPath',
ciVariablesDocumentationPath: { 'ciVariablesDocumentationPath',
from: 'ciVariablesDocumentationPath', 'projectCiSettingsPath',
}, 'canSetProjectCiVariables',
projectCiSettingsPath: { ],
from: 'projectCiSettingsPath',
},
canSetProjectCiVariables: {
from: 'canSetProjectCiVariables',
},
},
props: { props: {
apiFuzzingCiConfiguration: { apiFuzzingCiConfiguration: {
type: Object, type: Object,
...@@ -53,6 +53,8 @@ export default { ...@@ -53,6 +53,8 @@ export default {
}, },
data() { data() {
return { return {
isLoading: false,
showError: false,
targetUrl: { targetUrl: {
field: 'targetUrl', field: 'targetUrl',
label: s__('APIFuzzing|Target URL'), label: s__('APIFuzzing|Target URL'),
...@@ -111,6 +113,8 @@ export default { ...@@ -111,6 +113,8 @@ export default {
}), }),
), ),
}, },
ciYamlEditUrl: '',
configurationYaml: '',
}; };
}, },
computed: { computed: {
...@@ -142,9 +146,60 @@ export default { ...@@ -142,9 +146,60 @@ export default {
({ name }) => name === this.scanProfile.value, ({ name }) => name === this.scanProfile.value,
)?.yaml; )?.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: { 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, SCAN_MODES,
}; };
...@@ -152,6 +207,10 @@ export default { ...@@ -152,6 +207,10 @@ export default {
<template> <template>
<form @submit.prevent="onSubmit"> <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" /> <form-input v-model="targetUrl.value" v-bind="targetUrl" class="gl-mb-7" />
<dropdown-input v-model="scanMode.value" v-bind="scanMode" /> <dropdown-input v-model="scanMode.value" v-bind="scanMode" />
...@@ -223,9 +282,26 @@ export default { ...@@ -223,9 +282,26 @@ export default {
<hr /> <hr />
<gl-button type="submit" variant="confirm">{{ <gl-button
s__('APIFuzzing|Generate code snippet') :disabled="someFieldEmpty"
}}</gl-button> :loading="isLoading"
<gl-button>{{ __('Cancel') }}</gl-button> 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> </form>
</template> </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 = { ...@@ -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'; ...@@ -2,9 +2,7 @@ import Vue from 'vue';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue'; import SecurityConfigurationApp from './components/app.vue';
export default function init() { export const initSecurityConfiguration = (el) => {
const el = document.getElementById('js-security-configuration');
if (!el) { if (!el) {
return null; return null;
} }
...@@ -58,4 +56,4 @@ export default function init() { ...@@ -58,4 +56,4 @@ export default function init() {
}); });
}, },
}); });
} };
<script> <script>
import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui'; import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { cloneDeep } from 'lodash'; 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 * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql'; import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import DynamicFields from '../../components/dynamic_fields.vue';
import ExpandableSection from '../../components/expandable_section.vue';
import AnalyzerConfiguration from './analyzer_configuration.vue'; import AnalyzerConfiguration from './analyzer_configuration.vue';
import { import {
toSastCiConfigurationEntityInput, toSastCiConfigurationEntityInput,
......
/* eslint-disable import/export */ /* eslint-disable import/export */
import { invert } from 'lodash'; 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'; 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. * SecurityReportTypeEnum values for use with GraphQL.
* *
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Projects::Security::ApiFuzzingConfigurationHelper module Projects::Security::ApiFuzzingConfigurationHelper
def api_fuzzing_configuration_data(project) def api_fuzzing_configuration_data(project)
{ {
security_configuration_path: project_security_configuration_path(project),
full_path: project.full_path, full_path: project.full_path,
api_fuzzing_documentation_path: help_page_path('user/application_security/api_fuzzing/index'), 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'), api_fuzzing_authentication_documentation_path: help_page_path('user/application_security/api_fuzzing/index', anchor: 'authentication'),
......
...@@ -39,7 +39,7 @@ describe('burndown_chart', () => { ...@@ -39,7 +39,7 @@ describe('burndown_chart', () => {
burndownEventsPath: '/api/v4/projects/1234/milestones/1/burndown_events', burndownEventsPath: '/api/v4/projects/1234/milestones/1/burndown_events',
}; };
const createComponent = ({ props = {}, data = {} } = {}) => { const createComponent = ({ props = {}, data = {}, loading = false } = {}) => {
wrapper = shallowMount(BurnCharts, { wrapper = shallowMount(BurnCharts, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...@@ -49,7 +49,7 @@ describe('burndown_chart', () => { ...@@ -49,7 +49,7 @@ describe('burndown_chart', () => {
$apollo: { $apollo: {
queries: { queries: {
report: { report: {
loading: false, loading,
}, },
}, },
}, },
...@@ -69,6 +69,20 @@ describe('burndown_chart', () => { ...@@ -69,6 +69,20 @@ describe('burndown_chart', () => {
wrapper.destroy(); 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', () => { it('includes Issues and Issue weight buttons', () => {
createComponent(); createComponent();
......
...@@ -27,6 +27,18 @@ describe('burndown_chart', () => { ...@@ -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', () => { describe('with single point', () => {
it('does not show guideline', () => { it('does not show guideline', () => {
createComponent({ createComponent({
......
...@@ -26,6 +26,18 @@ describe('Burnup chart', () => { ...@@ -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', () => { it('renders the lineChart correctly', () => {
const burnupData = [day1, day2, day3]; const burnupData = [day1, day2, day3];
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { GlAlert } from '@gitlab/ui';
import { stripTypenames } from 'helpers/graphql_helpers';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; 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 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 FormInput from 'ee/security_configuration/components/form_input.vue';
import DropdownInput from 'ee/security_configuration/components/dropdown_input.vue'; import DropdownInput from 'ee/security_configuration/components/dropdown_input.vue';
import {
const makeScanProfile = (name) => ({ apiFuzzingConfigurationQueryResponse,
name, createApiFuzzingConfigurationMutationResponse,
description: `${name} description`, } from '../mock_data';
yaml: `
---
:Name: ${name}
`.trim(),
});
describe('EE - ApiFuzzingConfigurationForm', () => { describe('EE - ApiFuzzingConfigurationForm', () => {
let wrapper; let wrapper;
const apiFuzzingCiConfiguration = { const apiFuzzingCiConfiguration = stripTypenames(
scanModes: Object.keys(SCAN_MODES), apiFuzzingConfigurationQueryResponse.data.project.apiFuzzingCiConfiguration,
scanProfiles: [makeScanProfile('Quick-10'), makeScanProfile('Medium-20')], );
};
const findAlert = () => wrapper.find(GlAlert);
const findEnableAuthenticationCheckbox = () => const findEnableAuthenticationCheckbox = () =>
wrapper.findByTestId('api-fuzzing-enable-authentication-checkbox'); wrapper.findByTestId('api-fuzzing-enable-authentication-checkbox');
const findTargetUrlInput = () => wrapper.findAll(FormInput).at(0);
const findScanModeInput = () => wrapper.findAll(DropdownInput).at(0); const findScanModeInput = () => wrapper.findAll(DropdownInput).at(0);
const findSpecificationFileInput = () => wrapper.findAll(FormInput).at(1); const findSpecificationFileInput = () => wrapper.findAll(FormInput).at(1);
const findAuthenticationNotice = () => wrapper.findByTestId('api-fuzzing-authentication-notice'); const findAuthenticationNotice = () => wrapper.findByTestId('api-fuzzing-authentication-notice');
const findAuthenticationFields = () => wrapper.find(DynamicFields);
const findScanProfileDropdownInput = () => wrapper.findAll(DropdownInput).at(1); const findScanProfileDropdownInput = () => wrapper.findAll(DropdownInput).at(1);
const findScanProfileYamlViewer = () => const findScanProfileYamlViewer = () =>
wrapper.findByTestId('api-fuzzing-scan-profile-yaml-viewer'); 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 = {}) => { const createWrapper = (options = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
...@@ -39,6 +57,8 @@ describe('EE - ApiFuzzingConfigurationForm', () => { ...@@ -39,6 +57,8 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
merge( merge(
{ {
provide: { provide: {
fullPath: 'namespace/project',
securityConfigurationPath: '/security/configuration',
apiFuzzingAuthenticationDocumentationPath: apiFuzzingAuthenticationDocumentationPath:
'api_fuzzing_authentication/documentation/path', 'api_fuzzing_authentication/documentation/path',
ciVariablesDocumentationPath: '/ci_cd_variables/documentation/path', ciVariablesDocumentationPath: '/ci_cd_variables/documentation/path',
...@@ -48,6 +68,11 @@ describe('EE - ApiFuzzingConfigurationForm', () => { ...@@ -48,6 +68,11 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
propsData: { propsData: {
apiFuzzingCiConfiguration, apiFuzzingCiConfiguration,
}, },
mocks: {
$apollo: {
mutate: jest.fn(),
},
},
}, },
options, options,
), ),
...@@ -156,12 +181,112 @@ describe('EE - ApiFuzzingConfigurationForm', () => { ...@@ -156,12 +181,112 @@ describe('EE - ApiFuzzingConfigurationForm', () => {
}); });
it('when a scan profile is selected, its YAML is visible', async () => { it('when a scan profile is selected, its YAML is visible', async () => {
const selectedScanProfile = apiFuzzingCiConfiguration.scanProfiles[0]; const [selectedScanProfile] = apiFuzzingCiConfiguration.scanProfiles;
wrapper.findAll(DropdownInput).at(1).vm.$emit('input', selectedScanProfile.name); findScanProfileDropdownInput().vm.$emit('input', selectedScanProfile.name);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findScanProfileYamlViewer().exists()).toBe(true); 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 = { ...@@ -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 ...@@ -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 ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue'; import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.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 { redirectTo } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { makeEntities, makeSastCiConfiguration } from '../../helpers'; import { makeEntities, makeSastCiConfiguration } from '../../helpers';
......
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:security_configuration_path) { project_security_configuration_path(project) }
let(:full_path) { project.full_path } let(:full_path) { project.full_path }
let(:api_fuzzing_documentation_path) { help_page_path('user/application_security/api_fuzzing/index') } 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(: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 ...@@ -25,6 +26,7 @@ RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
it { it {
is_expected.to eq( is_expected.to eq(
security_configuration_path: security_configuration_path,
full_path: full_path, full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path, api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path, api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
...@@ -42,6 +44,7 @@ RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do ...@@ -42,6 +44,7 @@ RSpec.describe Projects::Security::ApiFuzzingConfigurationHelper do
it { it {
is_expected.to eq( is_expected.to eq(
security_configuration_path: security_configuration_path,
full_path: full_path, full_path: full_path,
api_fuzzing_documentation_path: api_fuzzing_documentation_path, api_fuzzing_documentation_path: api_fuzzing_documentation_path,
api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path, api_fuzzing_authentication_documentation_path: api_fuzzing_authentication_documentation_path,
......
...@@ -13,7 +13,6 @@ module Gitlab ...@@ -13,7 +13,6 @@ module Gitlab
gon.asset_host = ActionController::Base.asset_host gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root 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 gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
if Gitlab.config.sentry.enabled if Gitlab.config.sentry.enabled
......
...@@ -148,7 +148,7 @@ module Gitlab ...@@ -148,7 +148,7 @@ module Gitlab
end end
def load_yaml_from_path(path) 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 end
end end
......
...@@ -1405,6 +1405,15 @@ msgstr "" ...@@ -1405,6 +1405,15 @@ msgstr ""
msgid "APIFuzzing|Choose a profile" msgid "APIFuzzing|Choose a profile"
msgstr "" 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}." 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 "" msgstr ""
...@@ -1453,6 +1462,9 @@ msgstr "" ...@@ -1453,6 +1462,9 @@ msgstr ""
msgid "APIFuzzing|Target URL" msgid "APIFuzzing|Target URL"
msgstr "" msgstr ""
msgid "APIFuzzing|The configuration could not be saved, please try again later."
msgstr ""
msgid "APIFuzzing|There are two ways to perform scans." msgid "APIFuzzing|There are two ways to perform scans."
msgstr "" msgstr ""
...@@ -25905,12 +25917,18 @@ msgstr "" ...@@ -25905,12 +25917,18 @@ msgstr ""
msgid "SecurityConfiguration|Available for on-demand DAST" msgid "SecurityConfiguration|Available for on-demand DAST"
msgstr "" 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." 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 "" msgstr ""
msgid "SecurityConfiguration|Configure" msgid "SecurityConfiguration|Configure"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Configure via Merge Request"
msgstr ""
msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later." msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -25950,6 +25968,9 @@ msgstr "" ...@@ -25950,6 +25968,9 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration" msgid "SecurityConfiguration|SAST Configuration"
msgstr "" msgstr ""
msgid "SecurityConfiguration|SAST merge request creation mutation failed"
msgstr ""
msgid "SecurityConfiguration|Security Control" msgid "SecurityConfiguration|Security Control"
msgstr "" msgstr ""
......
...@@ -23,7 +23,7 @@ RSpec.describe 'Help Pages' do ...@@ -23,7 +23,7 @@ RSpec.describe 'Help Pages' do
it 'opens shortcuts help dialog' do it 'opens shortcuts help dialog' do
find('.js-trigger-shortcut').click find('.js-trigger-shortcut').click
expect(page).to have_selector('#modal-shortcuts') expect(page).to have_selector('[data-testid="modal-shortcuts"]')
end end
end end
end end
......
...@@ -27,14 +27,13 @@ RSpec.describe 'User uses shortcuts', :js do ...@@ -27,14 +27,13 @@ RSpec.describe 'User uses shortcuts', :js do
open_modal_shortcut_keys open_modal_shortcut_keys
# modal-shortcuts still in the DOM, but hidden expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
expect(find('#modal-shortcuts', visible: false)).not_to be_visible
page.refresh page.refresh
open_modal_shortcut_keys open_modal_shortcut_keys
# after reload, shortcuts modal doesn't exist at all until we add it # 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 end
it 're-enables shortcuts' do it 're-enables shortcuts' do
...@@ -47,7 +46,7 @@ RSpec.describe 'User uses shortcuts', :js do ...@@ -47,7 +46,7 @@ RSpec.describe 'User uses shortcuts', :js do
close_modal close_modal
open_modal_shortcut_keys open_modal_shortcut_keys
expect(find('#modal-shortcuts')).to be_visible expect(find('[data-testid="modal-shortcuts"]')).to be_visible
end end
def open_modal_shortcut_keys 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 ...@@ -222,6 +222,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
end end
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 describe '.aggregated_metrics_weekly_data' do
subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data } 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