Commit c3ee6824 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '228746-move-vuln-fetching-to-parent-component' into 'master'

Move the refresh header query to parent

See merge request gitlab-org/gitlab!71557
parents f3330498 cd28af36
<script>
import { GlLoadingIcon, GlButton, GlBadge } from '@gitlab/ui';
import fetchHeaderVulnerabilityQuery from 'ee/security_dashboard/graphql/header_vulnerability.graphql';
import vulnerabilityStateMutations from 'ee/security_dashboard/graphql/mutate_vulnerability_state';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility';
import UsersCache from '~/lib/utils/users_cache';
import { s__ } from '~/locale';
import {
VULNERABILITY_STATE_OBJECTS,
FEEDBACK_TYPES,
HEADER_ACTION_BUTTONS,
gidPrefix,
} from '../constants';
import { VULNERABILITY_STATE_OBJECTS, FEEDBACK_TYPES, HEADER_ACTION_BUTTONS } from '../constants';
import { normalizeGraphQLVulnerability } from '../helpers';
import ResolutionAlert from './resolution_alert.vue';
import StatusDescription from './status_description.vue';
......@@ -35,7 +31,7 @@ export default {
},
props: {
initialVulnerability: {
vulnerability: {
type: Object,
required: true,
},
......@@ -46,11 +42,7 @@ export default {
isProcessingAction: false,
isLoadingVulnerability: false,
isLoadingUser: false,
// Spread operator because the header could modify the `project`
// prop leading to an error in the footer component.
vulnerability: { ...this.initialVulnerability },
user: undefined,
shouldRefreshVulnerability: false,
};
},
......@@ -60,38 +52,6 @@ export default {
detected: 'warning',
},
apollo: {
vulnerability: {
query: fetchHeaderVulnerabilityQuery,
manual: true,
fetchPolicy: 'no-cache',
variables() {
return {
id: `${gidPrefix}${this.vulnerability.id}`,
};
},
result({ data: { vulnerability } }) {
this.shouldRefreshVulnerability = false;
this.isLoadingVulnerability = false;
this.vulnerability = {
...this.vulnerability,
...normalizeGraphQLVulnerability(vulnerability),
};
},
error() {
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to refresh the vulnerability. Please try again later.',
),
});
},
skip() {
return !this.shouldRefreshVulnerability;
},
},
},
computed: {
stateVariant() {
return this.$options.badgeVariants[this.vulnerability.state] || 'neutral';
......@@ -178,16 +138,17 @@ export default {
try {
const { data } = await this.$apollo.mutate({
mutation: vulnerabilityStateMutations[action],
variables: { id: `${gidPrefix}${this.vulnerability.id}`, ...payload },
variables: {
id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id),
...payload,
},
});
const [queryName] = Object.keys(data);
this.vulnerability = {
this.$emit('vulnerability-state-change', {
...this.vulnerability,
...normalizeGraphQLVulnerability(data[queryName].vulnerability),
};
this.$emit('vulnerability-state-change');
});
} catch (error) {
createFlash({
message: {
......@@ -245,10 +206,6 @@ export default {
fileName: `remediation.patch`,
});
},
refreshVulnerability() {
this.isLoadingVulnerability = true;
this.shouldRefreshVulnerability = true;
},
},
};
</script>
......
<script>
import fetchHeaderVulnerabilityQuery from 'ee/security_dashboard/graphql/header_vulnerability.graphql';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import { normalizeGraphQLVulnerability } from '../helpers';
import FalsePositiveAlert from './false_positive_alert.vue';
import VulnerabilityFooter from './footer.vue';
import VulnerabilityHeader from './header.vue';
......@@ -12,21 +19,58 @@ export default {
FalsePositiveAlert,
},
props: {
vulnerability: {
initialVulnerability: {
type: Object,
required: true,
},
},
data() {
return {
shouldSkipQuery: true,
vulnerability: { ...this.initialVulnerability },
};
},
computed: {
hasFalsePositive() {
return this.vulnerability.falsePositive;
},
},
apollo: {
vulnerability: {
manual: true,
query: fetchHeaderVulnerabilityQuery,
fetchPolicy: fetchPolicies.NO_CACHE,
variables() {
return {
id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id),
};
},
result({ data: { vulnerability } }) {
this.shouldSkipQuery = true;
this.vulnerability = {
...this.vulnerability,
...normalizeGraphQLVulnerability(vulnerability),
};
},
error() {
createFlash({
message: s__(
'VulnerabilityManagement|Something went wrong while trying to refresh the vulnerability. Please try again later.',
),
});
},
skip() {
return this.shouldSkipQuery;
},
},
},
methods: {
refreshHeader() {
this.$refs.header.refreshVulnerability();
this.shouldSkipQuery = false;
this.$apollo.queries.vulnerability.refetch();
},
refreshFooter() {
refreshFooter(newVulnerability) {
this.vulnerability = newVulnerability;
this.$refs.footer.fetchDiscussions();
},
},
......@@ -38,7 +82,7 @@ export default {
<false-positive-alert v-if="hasFalsePositive" class="gl-mt-5" />
<vulnerability-header
ref="header"
:initial-vulnerability="vulnerability"
:vulnerability="vulnerability"
@vulnerability-state-change="refreshFooter"
/>
<vulnerability-details :vulnerability="vulnerability" />
......
......@@ -35,7 +35,7 @@ export default (el) => {
},
render: (h) =>
h(App, {
props: { vulnerability },
props: { initialVulnerability: vulnerability },
}),
});
};
......@@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo';
import Api from 'ee/api';
import fetchHeaderVulnerabilityQuery from 'ee/security_dashboard/graphql/header_vulnerability.graphql';
import vulnerabilityStateMutations from 'ee/security_dashboard/graphql/mutate_vulnerability_state';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import Header from 'ee/vulnerabilities/components/header.vue';
......@@ -90,7 +89,7 @@ describe('Vulnerability Header', () => {
localVue,
apolloProvider,
propsData: {
initialVulnerability: {
vulnerability: {
...defaultVulnerability,
...vulnerability,
},
......@@ -103,7 +102,6 @@ describe('Vulnerability Header', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
mockAxios.reset();
createFlash.mockReset();
});
......@@ -136,12 +134,14 @@ describe('Vulnerability Header', () => {
createWrapper({ apolloProvider });
});
it(`updates the state properly - ${action}`, async () => {
it(`emits the updated vulnerability properly - ${action}`, async () => {
const dropdown = wrapper.find(VulnerabilityStateDropdown);
dropdown.vm.$emit('change', { action });
await waitForPromises();
expect(findBadge().text()).toBe(expected);
expect(wrapper.emitted('vulnerability-state-change')[0][0]).toMatchObject({
state: expected,
});
});
it(`emits an event when the state is changed - ${action}`, async () => {
......@@ -419,63 +419,4 @@ describe('Vulnerability Header', () => {
});
});
});
describe('refresh vulnerability', () => {
describe('on success', () => {
beforeEach(() => {
const apolloProvider = createApolloProvider([
fetchHeaderVulnerabilityQuery,
jest.fn().mockResolvedValue({
data: {
errors: [],
vulnerability: {
id: 'gid://gitlab/Vulnerability/54',
[`resolvedAt`]: '2020-09-16T11:13:26Z',
state: 'RESOLVED',
},
},
}),
]);
createWrapper({
apolloProvider,
vulnerability: getVulnerability({}),
});
});
it('fetches the vulnerability when refreshVulnerability method is called', async () => {
expect(findBadge().text()).toBe('detected');
wrapper.vm.refreshVulnerability();
await waitForPromises();
expect(findBadge().text()).toBe('resolved');
});
});
describe('on failure', () => {
beforeEach(() => {
const apolloProvider = createApolloProvider([
fetchHeaderVulnerabilityQuery,
jest.fn().mockRejectedValue({
data: {
errors: [{ message: 'something went wrong while fetching the vulnerability' }],
vulnerability: null,
},
}),
]);
createWrapper({
apolloProvider,
vulnerability: getVulnerability({}),
});
});
it('calls createFlash', async () => {
expect(findBadge().text()).toBe('detected');
wrapper.vm.refreshVulnerability();
await waitForPromises();
expect(findBadge().text()).toBe('detected');
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import fetchHeaderVulnerabilityQuery from 'ee/security_dashboard/graphql/header_vulnerability.graphql';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import Footer from 'ee/vulnerabilities/components/footer.vue';
import Header from 'ee/vulnerabilities/components/header.vue';
import VulnerabilityFooter from 'ee/vulnerabilities/components/footer.vue';
import VulnerabilityHeader from 'ee/vulnerabilities/components/header.vue';
import Main from 'ee/vulnerabilities/components/vulnerability.vue';
import Details from 'ee/vulnerabilities/components/vulnerability_details.vue';
import { stubComponent } from 'helpers/stub_component';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
const mockAxios = new AxiosMockAdapter();
const localVue = createLocalVue();
localVue.use(VueApollo);
jest.mock('~/flash');
describe('Vulnerability', () => {
let wrapper;
const vulnerability = {
const getVulnerability = (props) => ({
id: 1,
created_at: new Date().toISOString(),
report_type: 'sast',
......@@ -42,39 +48,55 @@ describe('Vulnerability', () => {
merge_request_feedback: null,
issue_feedback: null,
remediation: null,
...props,
});
const createApolloProvider = (...queries) => {
return createMockApollo([...queries]);
};
const createApolloProviderForVulnerabilityStateChange = () => {
return createApolloProvider([
fetchHeaderVulnerabilityQuery,
jest.fn().mockResolvedValue({
data: {
errors: [],
vulnerability: {
id: 'gid://gitlab/Vulnerability/54',
resolvedAt: '2020-09-16T11:13:26Z',
state: 'RESOLVED',
},
},
}),
]);
};
const createWrapper = ({ vulnData, provide } = {}) => {
const createWrapper = ({ vulnData, apolloProvider } = {}) => {
wrapper = shallowMount(Main, {
localVue,
apolloProvider,
propsData: {
vulnerability: { ...vulnerability, ...vulnData },
},
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: false,
...provide,
initialVulnerability: { ...getVulnerability(), ...vulnData },
},
stubs: {
VulnerabilityHeader: stubComponent(Header),
VulnerabilityFooter: stubComponent(Footer),
VulnerabilityFooter: stubComponent(VulnerabilityFooter),
},
});
};
afterEach(() => {
createFlash.mockReset();
wrapper.destroy();
wrapper = null;
mockAxios.reset();
});
const findHeader = () => wrapper.find(Header);
const findDetails = () => wrapper.find(Details);
const findFooter = () => wrapper.find(Footer);
const findAlert = () => wrapper.find(FalsePositiveAlert);
const findHeader = () => wrapper.findComponent(VulnerabilityHeader);
const findDetails = () => wrapper.findComponent(Details);
const findFooter = () => wrapper.findComponent(VulnerabilityFooter);
const findAlert = () => wrapper.findComponent(FalsePositiveAlert);
describe('default behavior', () => {
beforeEach(() => {
createWrapper();
createWrapper({ apolloProvider: createApolloProviderForVulnerabilityStateChange() });
});
it('consists of header, details, and footer', () => {
......@@ -84,7 +106,8 @@ describe('Vulnerability', () => {
});
it('passes the correct properties to the children', () => {
expect(findHeader().props('initialVulnerability')).toEqual(vulnerability);
const vulnerability = getVulnerability();
expect(findHeader().props('vulnerability')).toEqual(vulnerability);
expect(findDetails().props('vulnerability')).toEqual(vulnerability);
expect(findFooter().props('vulnerability')).toEqual(vulnerability);
});
......@@ -95,23 +118,27 @@ describe('Vulnerability', () => {
let refreshVulnerability;
beforeEach(() => {
createWrapper();
refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability');
createWrapper({ apolloProvider: createApolloProviderForVulnerabilityStateChange() });
refreshVulnerability = jest.spyOn(wrapper.vm.$apollo.queries.vulnerability, 'refetch');
makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions');
});
it('updates the footer notes when the vulnerbility state was changed', () => {
findHeader().vm.$emit('vulnerability-state-change');
findHeader().vm.$emit('vulnerability-state-change', getVulnerability());
expect(makeRequest).toHaveBeenCalledTimes(1);
expect(refreshVulnerability).not.toHaveBeenCalled();
});
it('updates the header when the footer received a state-change note', () => {
it('updates the header when the footer received a state-change note', async () => {
findFooter().vm.$emit('vulnerability-state-change');
expect(makeRequest).not.toHaveBeenCalled();
expect(refreshVulnerability).toHaveBeenCalledTimes(1);
await waitForPromises();
expect(findHeader().props('vulnerability')).toEqual(
getVulnerability({ id: '54', state: 'resolved', resolvedAt: '2020-09-16T11:13:26Z' }),
);
});
});
......@@ -119,9 +146,41 @@ describe('Vulnerability', () => {
it('renders false positive alert', () => {
createWrapper({
vulnData: { falsePositive: true },
provide: { canViewFalsePositive: true },
});
expect(findAlert().exists()).toBe(true);
});
});
describe('refresh vulnerability', () => {
describe('on success', () => {
beforeEach(() => {
createWrapper({
apolloProvider: createApolloProviderForVulnerabilityStateChange(),
vulnerability: getVulnerability({}),
});
});
});
describe('on failure', () => {
beforeEach(() => {
const apolloProvider = createApolloProvider([
fetchHeaderVulnerabilityQuery,
jest.fn().mockRejectedValue({
data: {
errors: [{ message: 'something went wrong while fetching the vulnerability' }],
vulnerability: null,
},
}),
]);
createWrapper({ apolloProvider });
});
it('calls createFlash', async () => {
findFooter().vm.$emit('vulnerability-state-change');
await waitForPromises();
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
});
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