Commit f9551502 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'remove-sec-train-query' into 'master'

Adjust security training to use temp resolvers

See merge request gitlab-org/gitlab!82745
parents c756aa8c 569988c1
query getSecurityTrainingVulnerability($id: ID!) {
vulnerability(id: $id) {
vulnerability(id: $id) @client {
id
identifiers {
externalType
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import tempResolver from './temp_resolver';
Vue.use(VueApollo);
const defaultClient = createDefaultClient();
const defaultClient = createDefaultClient({
...tempResolver,
});
export default new VueApollo({
defaultClient,
......
// Note: this is behind a feature flag and only a placeholder
// until the actual GraphQL fields have been added
// https://gitlab.com/gitlab-org/gitlab/-/issues/349910
export default {
Query: {
vulnerability() {
/* eslint-disable @gitlab/require-i18n-strings */
return {
__typename: 'Vulnerability',
id: 'id: "gid://gitlab/Vulnerability/295"',
identifiers: [{ externalType: 'cwe', __typename: 'VulnerabilityIdentifier' }],
securityTrainingUrls: [
{
__typename: 'SecurityTrainingUrls',
id: 101,
name: 'Kontra',
url: null,
status: 'COMPLETED',
},
{
__typename: 'SecurityTrainingUrls',
id: 102,
name: 'Secure Code Warrior',
url: 'https://www.securecodewarrior.com/',
status: 'COMPLETED',
},
],
};
},
},
};
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import VulnerabilityTraining, {
i18n,
} 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,
SECURITY_TRAINING_URL_STATUS_PENDING,
} from 'ee/vulnerabilities/constants';
import {
TRACK_CLICK_TRAINING_LINK_ACTION,
TRACK_TRAINING_LOADED_ACTION,
} from '~/security_configuration/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
testProviderName,
testTrainingUrls,
getSecurityTrainingProvidersData,
tempProviderLogos,
} from 'jest/security_configuration/mock_data';
import { getSecurityTrainingVulnerabilityData, defaultProps } from './mock_data';
Vue.use(VueApollo);
const createIdentifiersData = (externalType = 'not supported identifier') =>
getSecurityTrainingVulnerabilityData({
identifiers: [{ externalType }],
});
const createTrainingData = (first = {}, second = {}, urls) =>
getSecurityTrainingVulnerabilityData({
urls,
urlOverrides: {
first,
second,
},
});
const projectFullPath = 'namespace/project';
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;
const createApolloProvider = ({ providersQueryHandler, vulnerabilityQueryHandler } = {}) => {
apolloProvider = createMockApollo([
[
securityTrainingProvidersQuery,
providersQueryHandler ||
jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response),
],
[
securityTrainingVulnerabilityQuery,
vulnerabilityQueryHandler ||
jest.fn().mockResolvedValue(TEST_TRAINING_VULNERABILITY_DEFAULT.response),
],
]);
};
const createComponent = (props = {}, { slots = {}, secureVulnerabilityTraining = true } = {}) => {
wrapper = shallowMountExtended(VulnerabilityTraining, {
propsData: {
...defaultProps,
...props,
},
slots,
apolloProvider,
provide: {
projectFullPath,
glFeatures: {
secureVulnerabilityTraining,
},
},
});
};
beforeEach(async () => {
createApolloProvider();
});
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
});
const waitForQueryToBeLoaded = () => waitForPromises();
const findDescription = () => wrapper.findByTestId('description');
const findUnavailableMessage = () => wrapper.findByTestId('unavailable-message');
const findTrainingItemName = (name) => wrapper.findByText(name);
const findTrainingItemLinks = () => wrapper.findAllComponents(GlLink);
const findTrainingItemLinkIcons = () => wrapper.findAllComponents(GlIcon);
const findTrainingLogos = () => wrapper.findAll('img');
describe('with the query being successful', () => {
describe('basic structure', () => {
it('displays the description', async () => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
expect(findDescription().text()).toBe(i18n.trainingDescription);
});
it('does not render component when there are no enabled securityTrainingProviders', async () => {
createApolloProvider({
providersQueryHandler: jest
.fn()
.mockResolvedValue(TEST_TRAINING_PROVIDERS_ALL_DISABLED.response),
});
createComponent();
await waitForQueryToBeLoaded();
expect(wrapper.html()).toBeFalsy();
});
it('watches showVulnerabilityTraining and emits change', async () => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
await nextTick();
// Note: the event emits twice - the second time is when the query is loaded
expect(wrapper.emitted('show-vulnerability-training')).toEqual([[false], [true]]);
});
});
describe('with title slot', () => {
it('renders slot content', async () => {
const mockSlotText = 'some title';
createComponent({}, { slots: { header: mockSlotText } });
await waitForQueryToBeLoaded();
expect(wrapper.text()).toContain(mockSlotText);
});
});
describe('training availability message', () => {
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
${SUPPORTED_IDENTIFIER_TYPES.cwe.toUpperCase()} | ${false}
${SUPPORTED_IDENTIFIER_TYPES.cwe.toLowerCase()} | ${false}
`('sets it to "$exists" for "$identifier"', async ({ identifier, exists }) => {
createApolloProvider({
vulnerabilityQueryHandler: jest
.fn()
.mockResolvedValue(createIdentifiersData(identifier).response),
});
createComponent();
await waitForQueryToBeLoaded();
expect(findUnavailableMessage().exists()).toBe(exists);
});
});
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);
});
});
describe('polling', () => {
beforeEach(() => {
createApolloProvider();
createComponent();
});
it('sets polling at 5000 ms', () => {
expect(wrapper.vm.$apollo.queries.vulnerability.options.pollInterval).toBe(5000);
});
it('stops polling when every training url status is completed', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.vulnerability, 'stopPolling')
.mockImplementation(jest.fn());
await waitForQueryToBeLoaded();
await nextTick();
expect(wrapper.vm.$apollo.queries.vulnerability.stopPolling).toHaveBeenCalled();
});
});
describe('training logo', () => {
beforeEach(async () => {
createApolloProvider();
createComponent();
wrapper.vm.$options.TEMP_PROVIDER_LOGOS = tempProviderLogos;
await waitForQueryToBeLoaded();
});
const providerIndexArray = [0, 1];
it.each(providerIndexArray)('displays the correct width for training item %s', (index) => {
expect(findTrainingLogos().at(index).attributes('width')).toBe('12');
});
it.each(providerIndexArray)('has a11y decorative attribute for provider %s', (index) => {
expect(findTrainingLogos().at(index).attributes('role')).toBe('presentation');
});
it.each(providerIndexArray)('displays the correct svg path for training item %s', (index) => {
expect(findTrainingLogos().at(index).attributes('src')).toBe(
tempProviderLogos[testProviderName[index]].svg,
);
});
});
describe('training item', () => {
it('displays correct number of training items', async () => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
expect(findTrainingItemLinks()).toHaveLength(testTrainingUrls.length);
});
it.each([0, 1])('displays training item %s', async (index) => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
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);
});
});
});
describe('with the query resulting in an error', () => {
beforeEach(() => {
jest.spyOn(Sentry, 'captureException');
createApolloProvider({ providersQueryHandler: jest.fn().mockResolvedValue(new Error()) });
createComponent();
});
it('reports the error to sentry', async () => {
expect(Sentry.captureException).not.toHaveBeenCalled();
await waitForQueryToBeLoaded();
expect(Sentry.captureException).toHaveBeenCalled();
});
});
describe('metrics', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
const expectedTrackingOptions = (index) => ({
label: `vendor_${testProviderName[index]}`,
property: projectFullPath,
});
it('tracks when the training link is loading', async () => {
createApolloProvider({
vulnerabilityQueryHandler: jest.fn().mockResolvedValue(
createTrainingData({
status: SECURITY_TRAINING_URL_STATUS_PENDING,
}).response,
),
});
createComponent();
await waitForQueryToBeLoaded();
expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TRAINING_LOADED_ACTION, {
property: projectFullPath,
});
});
it.each([0, 1])('tracks when training link %s gets clicked', async (index) => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
await findTrainingItemLinks().at(index).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
TRACK_CLICK_TRAINING_LINK_ACTION,
expectedTrackingOptions(index),
);
});
});
describe('when secureVulnerabilityTraining feature flag is disabled', () => {
it('does not render the VulnerabilityTraining component', () => {
createComponent({}, { secureVulnerabilityTraining: false });
expect(wrapper.html()).toBeFalsy();
});
});
});
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