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