Commit 8dec1e15 authored by Savas Vedova's avatar Savas Vedova

Merge branch 'vulnerability-details-use-be-service' into 'master'

Update to vulnerability details to use backend service

See merge request gitlab-org/gitlab!82436
parents 69065bae b6008e3f
query getSecurityTrainingVulnerability($id: ID!) {
vulnerability(id: $id) {
id
identifiers {
externalType
}
securityTrainingUrls {
name
url
status
}
}
}
...@@ -3,17 +3,10 @@ import { GlFriendlyWrap, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui ...@@ -3,17 +3,10 @@ import { GlFriendlyWrap, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants'; import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue'; import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue'; import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import { import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
SUPPORTING_MESSAGE_TYPES, import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
VULNERABILITY_TRAINING_HEADING,
} from 'ee/vulnerabilities/constants';
import {
convertObjectPropsToCamelCase,
convertArrayOfObjectsToCamelCase,
} from '~/lib/utils/common_utils';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import getFileLocation from '../store/utils/get_file_location'; import getFileLocation from '../store/utils/get_file_location';
import { bodyWithFallBack } from './helpers'; import { bodyWithFallBack } from './helpers';
import SeverityBadge from './severity_badge.vue'; import SeverityBadge from './severity_badge.vue';
...@@ -30,17 +23,11 @@ export default { ...@@ -30,17 +23,11 @@ export default {
GlLink, GlLink,
GlBadge, GlBadge,
FalsePositiveAlert, FalsePositiveAlert,
VulnerabilityTraining,
}, },
directives: { directives: {
SafeHtml: GlSafeHtmlDirective, SafeHtml: GlSafeHtmlDirective,
}, },
props: { vulnerability: { type: Object, required: true } }, props: { vulnerability: { type: Object, required: true } },
data() {
return {
showTraining: false,
};
},
computed: { computed: {
url() { url() {
return this.vulnerability.request?.url || getFileLocation(this.vulnLocation); return this.vulnerability.request?.url || getFileLocation(this.vulnLocation);
...@@ -154,9 +141,6 @@ export default { ...@@ -154,9 +141,6 @@ export default {
hasRecordedResponse() { hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse); return Boolean(this.constructedRecordedResponse);
}, },
camelCaseFormattedIdentifiers() {
return convertArrayOfObjectsToCamelCase(this.identifiers);
},
}, },
methods: { methods: {
getHeadersAsCodeBlockLines(headers) { getHeadersAsCodeBlockLines(headers) {
...@@ -191,12 +175,6 @@ export default { ...@@ -191,12 +175,6 @@ export default {
? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('') ? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('')
: ''; : '';
}, },
handleShowTraining(showVulnerabilityTraining) {
this.showTraining = showVulnerabilityTraining;
},
},
i18n: {
VULNERABILITY_TRAINING_HEADING,
}, },
}; };
</script> </script>
...@@ -331,13 +309,5 @@ export default { ...@@ -331,13 +309,5 @@ export default {
class="gl-mt-4" class="gl-mt-4"
:details="vulnerability.details" :details="vulnerability.details"
/> />
<div v-if="identifiers" v-show="showTraining">
<vulnerability-detail :label="$options.i18n.VULNERABILITY_TRAINING_HEADING.title">
<vulnerability-training
:identifiers="camelCaseFormattedIdentifiers"
@show-vulnerability-training="handleShowTraining"
/>
</vulnerability-detail>
</div>
</div> </div>
</template> </template>
...@@ -380,7 +380,7 @@ export default { ...@@ -380,7 +380,7 @@ export default {
</ul> </ul>
</template> </template>
<vulnerability-training :identifiers="vulnerability.identifiers"> <vulnerability-training :id="vulnerability.id">
<template #header> <template #header>
<h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3> <h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3>
</template> </template>
......
...@@ -3,14 +3,13 @@ import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui'; ...@@ -3,14 +3,13 @@ import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import securityTrainingVulnerabilityQuery from '~/security_configuration/graphql/security_training_vulnerability.query.graphql';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import { TRACK_CLICK_TRAINING_LINK_ACTION } from '~/security_configuration/constants';
TRACK_CLICK_TRAINING_LINK_ACTION, import { SUPPORTED_IDENTIFIER_TYPES, SECURITY_TRAINING_URL_STATUS_COMPLETED } from '../constants';
TRACK_TRAINING_LOADED_ACTION,
} from '~/security_configuration/constants';
import { SUPPORTED_IDENTIFIER_TYPES } from '../constants';
export const i18n = { export const i18n = {
trainingDescription: s__( trainingDescription: s__(
...@@ -21,12 +20,6 @@ export const i18n = { ...@@ -21,12 +20,6 @@ export const i18n = {
loading: __('Loading'), loading: __('Loading'),
}; };
export const mockProvider = {
path: 'https://integration-api.securecodewarrior.com/api/v1/trial',
id: 'gitlab',
name: s__('Vulnerability|Secure Code Warrior'),
};
export default { export default {
i18n, i18n,
components: { components: {
...@@ -37,8 +30,8 @@ export default { ...@@ -37,8 +30,8 @@ export default {
mixins: [glFeatureFlagsMixin(), Tracking.mixin()], mixins: [glFeatureFlagsMixin(), Tracking.mixin()],
inject: ['projectFullPath'], inject: ['projectFullPath'],
props: { props: {
identifiers: { id: {
type: Array, type: Number,
required: true, required: true,
}, },
}, },
...@@ -57,33 +50,58 @@ export default { ...@@ -57,33 +50,58 @@ export default {
}; };
}, },
}, },
vulnerability: {
query: securityTrainingVulnerabilityQuery,
update({ vulnerability }) {
const allUrlsAreReady = vulnerability?.securityTrainingUrls?.every(
({ status }) => status === SECURITY_TRAINING_URL_STATUS_COMPLETED,
);
if (allUrlsAreReady) {
// note: once we add polling, we can call `.stopPolling` here
this.isUrlsLoading = false;
}
return vulnerability;
},
variables() {
return { id: convertToGraphQLId(TYPE_VULNERABILITY, this.id) };
},
error(e) {
Sentry.captureException(e);
},
},
}, },
data() { data() {
return { return {
securityTrainingProviders: [], securityTrainingProviders: [],
vulnerability: {},
training: null, training: null,
isLoading: true, isUrlsLoading: true,
hasError: false,
}; };
}, },
computed: { computed: {
showVulnerabilityTraining() { showVulnerabilityTraining() {
return Boolean( return Boolean(
this.glFeatures.secureVulnerabilityTraining && this.glFeatures.secureVulnerabilityTraining && this.hasSecurityTrainingProviders,
this.enabledSecurityTrainingProviders?.length &&
this.identifiers?.length,
); );
}, },
enabledSecurityTrainingProviders() { showTrainingNotFound() {
return this.securityTrainingProviders?.filter((provider) => provider.isEnabled); return !this.hasSupportedIdentifier || !this.hasSecurityTrainingUrls;
},
hasSecurityTrainingProviders() {
return this.securityTrainingProviders?.some(({ isEnabled }) => isEnabled);
}, },
supportedIdentifier() { hasSupportedIdentifier() {
return this.identifiers?.find( return this.vulnerability?.identifiers?.some(
({ externalType }) => externalType?.toLowerCase() === SUPPORTED_IDENTIFIER_TYPES.cwe, ({ externalType }) => externalType?.toLowerCase() === SUPPORTED_IDENTIFIER_TYPES.cwe,
); );
}, },
showTrainingNotFound() { hasSecurityTrainingUrls() {
return !this.supportedIdentifier || this.hasError; return this.vulnerability?.securityTrainingUrls?.length > 0;
},
securityTrainingUrls() {
return this.vulnerability?.securityTrainingUrls;
}, },
}, },
watch: { watch: {
...@@ -93,49 +111,15 @@ export default { ...@@ -93,49 +111,15 @@ export default {
this.$emit('show-vulnerability-training', showVulnerabilityTraining); this.$emit('show-vulnerability-training', showVulnerabilityTraining);
}, },
}, },
supportedIdentifier: {
immediate: true,
handler(supportedIdentifier) {
if (supportedIdentifier) {
const { externalType, externalId } = supportedIdentifier;
this.fetchTraining(externalType, externalId);
} else {
this.isLoading = false;
}
},
},
}, },
methods: { methods: {
async fetchTraining(mappingList, mappingKey) { clickTrainingLink(name, url) {
const { path, id, name } = mockProvider; this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION, name, url);
const params = {
id,
mappingList,
mappingKey,
};
try {
const {
data: { url },
} = await axios.get(path, { params });
this.triggerMetric(TRACK_TRAINING_LOADED_ACTION);
this.training = { name, url };
} catch {
this.hasError = true;
} finally {
this.isLoading = false;
}
},
clickTrainingLink() {
this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION);
}, },
triggerMetric(action) { triggerMetric(action, name, url) {
const { name } = this.supportedIdentifier;
const { id } = mockProvider;
this.track(action, { this.track(action, {
label: `vendor_${id}`, property: url,
property: name, label: `vendor_${name}`,
}); });
}, },
}, },
...@@ -151,15 +135,19 @@ export default { ...@@ -151,15 +135,19 @@ export default {
<p v-if="showTrainingNotFound" data-testid="unavailable-message"> <p v-if="showTrainingNotFound" data-testid="unavailable-message">
{{ $options.i18n.trainingUnavailable }} {{ $options.i18n.trainingUnavailable }}
</p> </p>
<div v-else-if="isLoading"> <div v-else-if="isUrlsLoading">
<gl-skeleton-loader :width="200" :lines="3" /> <gl-skeleton-loader :width="200" :lines="3" />
</div> </div>
<div v-else> <div v-else>
<div class="gl-font-weight-bold gl-font-base">{{ training.name }}</div> <div v-for="({ name, url }, index) in securityTrainingUrls" :key="index" class="gl-mt-6">
<gl-link :href="training.url" target="_blank" @click="clickTrainingLink"> <div>
<span class="gl-font-weight-bold gl-font-base">{{ name }}</span>
</div>
<gl-link :href="url" target="_blank" @click="clickTrainingLink(name, url)">
{{ $options.i18n.viewTraining }} {{ $options.i18n.viewTraining }}
<gl-icon class="gl-ml-2" name="external-link" :size="12" /> <gl-icon class="gl-ml-2" name="external-link" :size="12" />
</gl-link> </gl-link>
</div> </div>
</div> </div>
</div>
</template> </template>
...@@ -93,3 +93,6 @@ export const SUPPORTED_IDENTIFIER_TYPES = { ...@@ -93,3 +93,6 @@ export const SUPPORTED_IDENTIFIER_TYPES = {
export const VULNERABILITY_TRAINING_HEADING = { export const VULNERABILITY_TRAINING_HEADING = {
title: s__('Vulnerability|Training'), title: s__('Vulnerability|Training'),
}; };
export const SECURITY_TRAINING_URL_STATUS_COMPLETED = 'COMPLETED';
export const SECURITY_TRAINING_URL_STATUS_PENDING = 'PENDING';
...@@ -200,17 +200,5 @@ key2: value2 ...@@ -200,17 +200,5 @@ key2: value2
<!----> <!---->
<!----> <!---->
<div
style="display: none;"
>
<vulnerability-detail-stub
label="Training"
>
<vulnerability-training-stub
identifiers="[object Object],[object Object]"
/>
</vulnerability-detail-stub>
</div>
</div> </div>
`; `;
...@@ -9,7 +9,6 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r ...@@ -9,7 +9,6 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import { mockFindings } from '../mock_data'; import { mockFindings } from '../mock_data';
function makeVulnerability(changes = {}) { function makeVulnerability(changes = {}) {
...@@ -138,17 +137,6 @@ describe('VulnerabilityDetails component', () => { ...@@ -138,17 +137,6 @@ describe('VulnerabilityDetails component', () => {
); );
}); });
it('renders vulnerability training', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }];
const vulnerability = makeVulnerability({ identifiers });
componentFactory(vulnerability);
expect(wrapper.findComponent(VulnerabilityTraining).props()).toMatchObject({
identifiers,
});
});
describe('does not render XSS links', () => { describe('does not render XSS links', () => {
// eslint-disable-next-line no-script-url // eslint-disable-next-line no-script-url
const badUrl = 'javascript:alert("")'; const badUrl = 'javascript:alert("")';
......
import { testProviderName, testTrainingUrls } from 'jest/security_configuration/mock_data';
import {
SUPPORTED_IDENTIFIER_TYPES,
SECURITY_TRAINING_URL_STATUS_COMPLETED,
} from 'ee/vulnerabilities/constants';
export const testIdentifiers = [
{ externalType: SUPPORTED_IDENTIFIER_TYPES.cwe },
{ externalType: 'cve' },
];
export const generateNote = ({ id = 1295 } = {}) => ({ export const generateNote = ({ id = 1295 } = {}) => ({
id: `gid://gitlab/DiscussionNote/${id}`, id: `gid://gitlab/DiscussionNote/${id}`,
body: 'Created a note.', body: 'Created a note.',
...@@ -31,3 +42,41 @@ export const addTypenamesToDiscussion = (discussion) => { ...@@ -31,3 +42,41 @@ export const addTypenamesToDiscussion = (discussion) => {
}, },
}; };
}; };
export const defaultProps = {
id: 200,
};
const createSecurityTrainingVulnerability = ({ urlOverrides = {}, urls, identifiers } = {}) => ({
...defaultProps,
identifiers: identifiers || testIdentifiers,
securityTrainingUrls: urls || [
{
name: testProviderName[0],
url: testTrainingUrls[0],
status: SECURITY_TRAINING_URL_STATUS_COMPLETED,
...urlOverrides.first,
},
{
name: testProviderName[1],
url: testTrainingUrls[1],
status: SECURITY_TRAINING_URL_STATUS_COMPLETED,
...urlOverrides.second,
},
],
});
export const getSecurityTrainingVulnerabilityData = (vulnerabilityOverrides = {}) => {
const vulnerability = createSecurityTrainingVulnerability(vulnerabilityOverrides);
const response = {
data: {
vulnerability,
},
};
return {
response,
data: vulnerability,
};
};
...@@ -13,12 +13,12 @@ describe('Vulnerability Details', () => { ...@@ -13,12 +13,12 @@ describe('Vulnerability Details', () => {
let wrapper; let wrapper;
const vulnerability = { const vulnerability = {
id: 123,
severity: 'bad severity', severity: 'bad severity',
confidence: 'high confidence', confidence: 'high confidence',
reportType: 'Some report type', reportType: 'Some report type',
description: 'vulnerability description', description: 'vulnerability description',
descriptionHtml: 'vulnerability description <code>sample</code>', descriptionHtml: 'vulnerability description <code>sample</code>',
identifiers: [],
}; };
const createWrapper = (vulnerabilityOverrides, { mountFn = mount, options = {} } = {}) => { const createWrapper = (vulnerabilityOverrides, { mountFn = mount, options = {} } = {}) => {
...@@ -204,31 +204,24 @@ describe('Vulnerability Details', () => { ...@@ -204,31 +204,24 @@ describe('Vulnerability Details', () => {
}); });
describe('VulnerabilityTraining', () => { describe('VulnerabilityTraining', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }]; const { id } = vulnerability;
it('renders component', () => { it('renders component', () => {
createShallowWrapper({ createShallowWrapper();
identifiers,
});
expect(findVulnerabilityTraining().props()).toMatchObject({ expect(findVulnerabilityTraining().props()).toMatchObject({
identifiers, id,
}); });
}); });
it('renders title text', () => { it('renders title text', () => {
createShallowWrapper( createShallowWrapper(null, {
{
identifiers,
},
{
stubs: { stubs: {
VulnerabilityTraining: { VulnerabilityTraining: {
template: '<div><slot name="header"></slot></div>', template: '<div><slot name="header"></slot></div>',
}, },
}, },
}, });
);
expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title); expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title);
}); });
......
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui'; import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import VulnerabilityTraining, { import VulnerabilityTraining, {
i18n, i18n,
mockProvider,
} from 'ee/vulnerabilities/components/vulnerability_training.vue'; } from 'ee/vulnerabilities/components/vulnerability_training.vue';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import securityTrainingVulnerabilityQuery from '~/security_configuration/graphql/security_training_vulnerability.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { SUPPORTED_IDENTIFIER_TYPES } from 'ee/vulnerabilities/constants';
import { import {
TRACK_CLICK_TRAINING_LINK_ACTION, SUPPORTED_IDENTIFIER_TYPES,
TRACK_TRAINING_LOADED_ACTION, SECURITY_TRAINING_URL_STATUS_PENDING,
} from '~/security_configuration/constants'; } from 'ee/vulnerabilities/constants';
import { TRACK_CLICK_TRAINING_LINK_ACTION } from '~/security_configuration/constants';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { getSecurityTrainingProvidersData } from 'jest/security_configuration/mock_data'; import {
testProviderName,
testTrainingUrls,
getSecurityTrainingProvidersData,
} from 'jest/security_configuration/mock_data';
import { getSecurityTrainingVulnerabilityData, defaultProps } from './mock_data';
const defaultProps = { Vue.use(VueApollo);
identifiers: [
{ externalType: SUPPORTED_IDENTIFIER_TYPES.cwe, name: 'CWE-81' },
{ externalType: 'cve' },
],
};
const mockSuccessTrainingUrl = 'training/path'; const createIdentifiersData = (externalType = 'not supported identifier') =>
getSecurityTrainingVulnerabilityData({
identifiers: [{ externalType }],
});
Vue.use(VueApollo); const createTrainingData = (first = {}, second = {}, urls) =>
getSecurityTrainingVulnerabilityData({
urls,
urlOverrides: {
first,
second,
},
});
const TEST_TRAINING_PROVIDERS_ALL_DISABLED = getSecurityTrainingProvidersData(); const TEST_TRAINING_PROVIDERS_ALL_DISABLED = getSecurityTrainingProvidersData();
const TEST_TRAINING_PROVIDERS_FIRST_ENABLED = getSecurityTrainingProvidersData({ const TEST_TRAINING_PROVIDERS_FIRST_ENABLED = getSecurityTrainingProvidersData({
providerOverrides: { first: { isEnabled: true } }, providerOverrides: { first: { isEnabled: true } },
}); });
const TEST_TRAINING_PROVIDERS_DEFAULT = TEST_TRAINING_PROVIDERS_FIRST_ENABLED; const TEST_TRAINING_PROVIDERS_DEFAULT = TEST_TRAINING_PROVIDERS_FIRST_ENABLED;
const TEST_TRAINING_VULNERABILITY_DEFAULT = getSecurityTrainingVulnerabilityData();
const TEST_TRAINING_VULNERABILITY_NO_URLS = createTrainingData(null, null, []);
describe('VulnerabilityTraining component', () => { describe('VulnerabilityTraining component', () => {
let wrapper; let wrapper;
let apolloProvider; let apolloProvider;
let mock;
const createApolloProvider = ({ queryHandler } = {}) => { const createApolloProvider = ({ providersQueryHandler, vulnerabilityQueryHandler } = {}) => {
apolloProvider = createMockApollo([ apolloProvider = createMockApollo([
[ [
securityTrainingProvidersQuery, securityTrainingProvidersQuery,
queryHandler || jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response), providersQueryHandler ||
jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response),
],
[
securityTrainingVulnerabilityQuery,
vulnerabilityQueryHandler ||
jest.fn().mockResolvedValue(TEST_TRAINING_VULNERABILITY_DEFAULT.response),
], ],
]); ]);
}; };
...@@ -71,26 +84,20 @@ describe('VulnerabilityTraining component', () => { ...@@ -71,26 +84,20 @@ describe('VulnerabilityTraining component', () => {
}; };
beforeEach(async () => { beforeEach(async () => {
mock = new MockAdapter(axios);
createApolloProvider(); createApolloProvider();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
apolloProvider = null; apolloProvider = null;
mock.restore();
}); });
const delayTrainingResponse = async () =>
mock.onGet(mockProvider.path).reply(() => new Promise(() => {}));
const mockTrainingSuccess = async () =>
mock.onGet(mockProvider.path).reply(httpStatus.OK, { url: mockSuccessTrainingUrl });
const waitForQueryToBeLoaded = () => waitForPromises(); const waitForQueryToBeLoaded = () => waitForPromises();
const findDescription = () => wrapper.findByTestId('description'); const findDescription = () => wrapper.findByTestId('description');
const findUnavailableMessage = () => wrapper.findByTestId('unavailable-message'); const findUnavailableMessage = () => wrapper.findByTestId('unavailable-message');
const findTrainingItemName = () => wrapper.findByText(mockProvider.name); const findTrainingItemName = (name) => wrapper.findByText(name);
const findTrainingItemLink = () => wrapper.findComponent(GlLink); const findTrainingItemLinks = () => wrapper.findAllComponents(GlLink);
const findTrainingItemLinkIcon = () => wrapper.findComponent(GlIcon); const findTrainingItemLinkIcons = () => wrapper.findAllComponents(GlIcon);
describe('with the query being successful', () => { describe('with the query being successful', () => {
describe('basic structure', () => { describe('basic structure', () => {
...@@ -101,15 +108,11 @@ describe('VulnerabilityTraining component', () => { ...@@ -101,15 +108,11 @@ describe('VulnerabilityTraining component', () => {
expect(findDescription().text()).toBe(i18n.trainingDescription); expect(findDescription().text()).toBe(i18n.trainingDescription);
}); });
it('does not render component when there are no identifiers', () => {
createApolloProvider();
createComponent({ identifiers: [] });
expect(wrapper.html()).toBeFalsy();
});
it('does not render component when there are no enabled securityTrainingProviders', async () => { it('does not render component when there are no enabled securityTrainingProviders', async () => {
createApolloProvider({ createApolloProvider({
queryHandler: jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_ALL_DISABLED.response), providersQueryHandler: jest
.fn()
.mockResolvedValue(TEST_TRAINING_PROVIDERS_ALL_DISABLED.response),
}); });
createComponent(); createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
...@@ -139,54 +142,89 @@ describe('VulnerabilityTraining component', () => { ...@@ -139,54 +142,89 @@ describe('VulnerabilityTraining component', () => {
}); });
describe('training availability message', () => { describe('training availability message', () => {
it('displays the message', async () => { it('displays message when there are no supported identifier', async () => {
createComponent({ createApolloProvider({
identifiers: [{ externalType: 'not supported identifier' }], vulnerabilityQueryHandler: jest.fn().mockResolvedValue(createIdentifiersData().response),
}); });
createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
expect(findUnavailableMessage().text()).toBe(i18n.trainingUnavailable); expect(findUnavailableMessage().text()).toBe(i18n.trainingUnavailable);
}); });
it('displays message when there are no security training urls', async () => {
createApolloProvider({
vulnerabilityQueryHandler: jest
.fn()
.mockResolvedValue(TEST_TRAINING_VULNERABILITY_NO_URLS.response),
});
createComponent();
await waitForQueryToBeLoaded();
expect(findUnavailableMessage().exists()).toBe(true);
});
it.each` it.each`
identifier | exists identifier | exists
${'not supported identifier'} | ${true}
${SUPPORTED_IDENTIFIER_TYPES.cwe.toUpperCase()} | ${false} ${SUPPORTED_IDENTIFIER_TYPES.cwe.toUpperCase()} | ${false}
${SUPPORTED_IDENTIFIER_TYPES.cwe.toLowerCase()} | ${false} ${SUPPORTED_IDENTIFIER_TYPES.cwe.toLowerCase()} | ${false}
`('sets it to "$exists" for "$identifier"', async ({ identifier, exists }) => { `('sets it to "$exists" for "$identifier"', async ({ identifier, exists }) => {
await mockTrainingSuccess(); createApolloProvider({
createComponent({ identifiers: [{ externalType: identifier }] }); vulnerabilityQueryHandler: jest
.fn()
.mockResolvedValue(createIdentifiersData(identifier).response),
});
createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
expect(findUnavailableMessage().exists()).toBe(exists); expect(findUnavailableMessage().exists()).toBe(exists);
}); });
}); });
describe('training item', () => { describe('GlSkeletonLoader', () => {
it('displays GlSkeletonLoader when loading', async () => { it('displays when there are supported identifiers and some urls are in pending status', async () => {
await delayTrainingResponse(); createApolloProvider({
vulnerabilityQueryHandler: jest.fn().mockResolvedValue(
createTrainingData({
status: SECURITY_TRAINING_URL_STATUS_PENDING,
}).response,
),
});
createComponent(); createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
}); });
});
describe('training item', () => {
it('displays correct number of training items', async () => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
expect(findTrainingItemLinks()).toHaveLength(testTrainingUrls.length);
});
it('displays training item information', async () => { it.each([0, 1])('displays training item %s', async (index) => {
await mockTrainingSuccess(); createApolloProvider();
createComponent(); createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
expect(findTrainingItemName().exists()).toBe(true); expect(findTrainingItemName(testProviderName[index]).exists()).toBe(true);
expect(findTrainingItemLink().attributes('href')).toBe(mockSuccessTrainingUrl); expect(findTrainingItemLinks().at(index).attributes('href')).toBe(testTrainingUrls[index]);
expect(findTrainingItemLinkIcon().attributes('name')).toBe('external-link'); expect(findTrainingItemLinkIcons().at(index).attributes('name')).toBe('external-link');
}); });
it('does not display training item information for non supported identifier', async () => { it('does not display training item if there are no securityTrainingUrls', async () => {
await mockTrainingSuccess(); createApolloProvider({
createComponent({ identifiers: [{ externalType: 'not supported identifier' }] }); vulnerabilityQueryHandler: jest
.fn()
.mockResolvedValue(TEST_TRAINING_VULNERABILITY_NO_URLS.response),
});
createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
expect(findTrainingItemName().exists()).toBe(false); expect(findTrainingItemLinks().exists()).toBe(false);
expect(findTrainingItemLink().exists()).toBe(false); expect(findTrainingItemLinkIcons().exists()).toBe(false);
expect(findTrainingItemLinkIcon().exists()).toBe(false);
}); });
}); });
}); });
...@@ -194,7 +232,7 @@ describe('VulnerabilityTraining component', () => { ...@@ -194,7 +232,7 @@ describe('VulnerabilityTraining component', () => {
describe('with the query resulting in an error', () => { describe('with the query resulting in an error', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
createApolloProvider({ queryHandler: jest.fn().mockResolvedValue(new Error()) }); createApolloProvider({ providersQueryHandler: jest.fn().mockResolvedValue(new Error()) });
createComponent(); createComponent();
}); });
...@@ -212,7 +250,7 @@ describe('VulnerabilityTraining component', () => { ...@@ -212,7 +250,7 @@ describe('VulnerabilityTraining component', () => {
beforeEach(async () => { beforeEach(async () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await mockTrainingSuccess(); createApolloProvider();
createComponent(); createComponent();
await waitForQueryToBeLoaded(); await waitForQueryToBeLoaded();
}); });
...@@ -221,26 +259,18 @@ describe('VulnerabilityTraining component', () => { ...@@ -221,26 +259,18 @@ describe('VulnerabilityTraining component', () => {
unmockTracking(); unmockTracking();
}); });
const expectedTrackingOptions = { const expectedTrackingOptions = (index) => ({
property: defaultProps.identifiers[0].name, property: testTrainingUrls[index],
label: `vendor_${mockProvider.id}`, label: `vendor_${testProviderName[index]}`,
};
it('tracks when the training link gets loaded', () => {
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
TRACK_TRAINING_LOADED_ACTION,
expectedTrackingOptions,
);
}); });
it('tracks when a training link gets clicked', async () => { it.each([0, 1])('tracks when training link %s gets clicked', async (index) => {
await findTrainingItemLink().vm.$emit('click'); await findTrainingItemLinks().at(index).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith( expect(trackingSpy).toHaveBeenCalledWith(
undefined, undefined,
TRACK_CLICK_TRAINING_LINK_ACTION, TRACK_CLICK_TRAINING_LINK_ACTION,
expectedTrackingOptions, expectedTrackingOptions(index),
); );
}); });
}); });
......
...@@ -41056,9 +41056,6 @@ msgstr "" ...@@ -41056,9 +41056,6 @@ msgstr ""
msgid "Vulnerability|Scanner Provider" msgid "Vulnerability|Scanner Provider"
msgstr "" msgstr ""
msgid "Vulnerability|Secure Code Warrior"
msgstr ""
msgid "Vulnerability|Security Audit" msgid "Vulnerability|Security Audit"
msgstr "" msgstr ""
......
export const testProjectPath = 'foo/bar'; export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103]; export const testProviderIds = [101, 102, 103];
export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3']; export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3'];
export const testTrainingUrls = [
'https://www.vendornameone.com/url',
'https://www.vendornametwo.com/url',
];
const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [
{ {
......
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