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> <script>
import { GlLoadingIcon, GlButton, GlBadge } from '@gitlab/ui'; 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 vulnerabilityStateMutations from 'ee/security_dashboard/graphql/mutate_vulnerability_state';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue'; import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import createFlash from '~/flash'; 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 axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import UsersCache from '~/lib/utils/users_cache'; import UsersCache from '~/lib/utils/users_cache';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { import { VULNERABILITY_STATE_OBJECTS, FEEDBACK_TYPES, HEADER_ACTION_BUTTONS } from '../constants';
VULNERABILITY_STATE_OBJECTS,
FEEDBACK_TYPES,
HEADER_ACTION_BUTTONS,
gidPrefix,
} from '../constants';
import { normalizeGraphQLVulnerability } from '../helpers'; import { normalizeGraphQLVulnerability } from '../helpers';
import ResolutionAlert from './resolution_alert.vue'; import ResolutionAlert from './resolution_alert.vue';
import StatusDescription from './status_description.vue'; import StatusDescription from './status_description.vue';
...@@ -35,7 +31,7 @@ export default { ...@@ -35,7 +31,7 @@ export default {
}, },
props: { props: {
initialVulnerability: { vulnerability: {
type: Object, type: Object,
required: true, required: true,
}, },
...@@ -46,11 +42,7 @@ export default { ...@@ -46,11 +42,7 @@ export default {
isProcessingAction: false, isProcessingAction: false,
isLoadingVulnerability: false, isLoadingVulnerability: false,
isLoadingUser: 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, user: undefined,
shouldRefreshVulnerability: false,
}; };
}, },
...@@ -60,38 +52,6 @@ export default { ...@@ -60,38 +52,6 @@ export default {
detected: 'warning', 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: { computed: {
stateVariant() { stateVariant() {
return this.$options.badgeVariants[this.vulnerability.state] || 'neutral'; return this.$options.badgeVariants[this.vulnerability.state] || 'neutral';
...@@ -178,16 +138,17 @@ export default { ...@@ -178,16 +138,17 @@ export default {
try { try {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: vulnerabilityStateMutations[action], mutation: vulnerabilityStateMutations[action],
variables: { id: `${gidPrefix}${this.vulnerability.id}`, ...payload }, variables: {
id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerability.id),
...payload,
},
}); });
const [queryName] = Object.keys(data); const [queryName] = Object.keys(data);
this.vulnerability = { this.$emit('vulnerability-state-change', {
...this.vulnerability, ...this.vulnerability,
...normalizeGraphQLVulnerability(data[queryName].vulnerability), ...normalizeGraphQLVulnerability(data[queryName].vulnerability),
}; });
this.$emit('vulnerability-state-change');
} catch (error) { } catch (error) {
createFlash({ createFlash({
message: { message: {
...@@ -245,10 +206,6 @@ export default { ...@@ -245,10 +206,6 @@ export default {
fileName: `remediation.patch`, fileName: `remediation.patch`,
}); });
}, },
refreshVulnerability() {
this.isLoadingVulnerability = true;
this.shouldRefreshVulnerability = true;
},
}, },
}; };
</script> </script>
......
<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 FalsePositiveAlert from './false_positive_alert.vue';
import VulnerabilityFooter from './footer.vue'; import VulnerabilityFooter from './footer.vue';
import VulnerabilityHeader from './header.vue'; import VulnerabilityHeader from './header.vue';
...@@ -12,21 +19,58 @@ export default { ...@@ -12,21 +19,58 @@ export default {
FalsePositiveAlert, FalsePositiveAlert,
}, },
props: { props: {
vulnerability: { initialVulnerability: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
data() {
return {
shouldSkipQuery: true,
vulnerability: { ...this.initialVulnerability },
};
},
computed: { computed: {
hasFalsePositive() { hasFalsePositive() {
return this.vulnerability.falsePositive; 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: { methods: {
refreshHeader() { refreshHeader() {
this.$refs.header.refreshVulnerability(); this.shouldSkipQuery = false;
this.$apollo.queries.vulnerability.refetch();
}, },
refreshFooter() { refreshFooter(newVulnerability) {
this.vulnerability = newVulnerability;
this.$refs.footer.fetchDiscussions(); this.$refs.footer.fetchDiscussions();
}, },
}, },
...@@ -38,7 +82,7 @@ export default { ...@@ -38,7 +82,7 @@ export default {
<false-positive-alert v-if="hasFalsePositive" class="gl-mt-5" /> <false-positive-alert v-if="hasFalsePositive" class="gl-mt-5" />
<vulnerability-header <vulnerability-header
ref="header" ref="header"
:initial-vulnerability="vulnerability" :vulnerability="vulnerability"
@vulnerability-state-change="refreshFooter" @vulnerability-state-change="refreshFooter"
/> />
<vulnerability-details :vulnerability="vulnerability" /> <vulnerability-details :vulnerability="vulnerability" />
......
...@@ -35,7 +35,7 @@ export default (el) => { ...@@ -35,7 +35,7 @@ export default (el) => {
}, },
render: (h) => render: (h) =>
h(App, { h(App, {
props: { vulnerability }, props: { initialVulnerability: vulnerability },
}), }),
}); });
}; };
...@@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Api from 'ee/api'; 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 vulnerabilityStateMutations from 'ee/security_dashboard/graphql/mutate_vulnerability_state';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue'; import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import Header from 'ee/vulnerabilities/components/header.vue'; import Header from 'ee/vulnerabilities/components/header.vue';
...@@ -90,7 +89,7 @@ describe('Vulnerability Header', () => { ...@@ -90,7 +89,7 @@ describe('Vulnerability Header', () => {
localVue, localVue,
apolloProvider, apolloProvider,
propsData: { propsData: {
initialVulnerability: { vulnerability: {
...defaultVulnerability, ...defaultVulnerability,
...vulnerability, ...vulnerability,
}, },
...@@ -103,7 +102,6 @@ describe('Vulnerability Header', () => { ...@@ -103,7 +102,6 @@ describe('Vulnerability Header', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
mockAxios.reset(); mockAxios.reset();
createFlash.mockReset(); createFlash.mockReset();
}); });
...@@ -136,12 +134,14 @@ describe('Vulnerability Header', () => { ...@@ -136,12 +134,14 @@ describe('Vulnerability Header', () => {
createWrapper({ apolloProvider }); createWrapper({ apolloProvider });
}); });
it(`updates the state properly - ${action}`, async () => { it(`emits the updated vulnerability properly - ${action}`, async () => {
const dropdown = wrapper.find(VulnerabilityStateDropdown); const dropdown = wrapper.find(VulnerabilityStateDropdown);
dropdown.vm.$emit('change', { action }); dropdown.vm.$emit('change', { action });
await waitForPromises(); 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 () => { it(`emits an event when the state is changed - ${action}`, async () => {
...@@ -419,63 +419,4 @@ describe('Vulnerability Header', () => { ...@@ -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 { shallowMount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; 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 FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import Footer from 'ee/vulnerabilities/components/footer.vue'; import VulnerabilityFooter from 'ee/vulnerabilities/components/footer.vue';
import Header from 'ee/vulnerabilities/components/header.vue'; import VulnerabilityHeader from 'ee/vulnerabilities/components/header.vue';
import Main from 'ee/vulnerabilities/components/vulnerability.vue'; import Main from 'ee/vulnerabilities/components/vulnerability.vue';
import Details from 'ee/vulnerabilities/components/vulnerability_details.vue'; import Details from 'ee/vulnerabilities/components/vulnerability_details.vue';
import { stubComponent } from 'helpers/stub_component'; 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', () => { describe('Vulnerability', () => {
let wrapper; let wrapper;
const vulnerability = { const getVulnerability = (props) => ({
id: 1, id: 1,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
report_type: 'sast', report_type: 'sast',
...@@ -42,39 +48,55 @@ describe('Vulnerability', () => { ...@@ -42,39 +48,55 @@ describe('Vulnerability', () => {
merge_request_feedback: null, merge_request_feedback: null,
issue_feedback: null, issue_feedback: null,
remediation: 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, { wrapper = shallowMount(Main, {
localVue,
apolloProvider,
propsData: { propsData: {
vulnerability: { ...vulnerability, ...vulnData }, initialVulnerability: { ...getVulnerability(), ...vulnData },
},
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: false,
...provide,
}, },
stubs: { stubs: {
VulnerabilityHeader: stubComponent(Header), VulnerabilityFooter: stubComponent(VulnerabilityFooter),
VulnerabilityFooter: stubComponent(Footer),
}, },
}); });
}; };
afterEach(() => { afterEach(() => {
createFlash.mockReset();
wrapper.destroy(); wrapper.destroy();
wrapper = null;
mockAxios.reset();
}); });
const findHeader = () => wrapper.find(Header); const findHeader = () => wrapper.findComponent(VulnerabilityHeader);
const findDetails = () => wrapper.find(Details); const findDetails = () => wrapper.findComponent(Details);
const findFooter = () => wrapper.find(Footer); const findFooter = () => wrapper.findComponent(VulnerabilityFooter);
const findAlert = () => wrapper.find(FalsePositiveAlert); const findAlert = () => wrapper.findComponent(FalsePositiveAlert);
describe('default behavior', () => { describe('default behavior', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper({ apolloProvider: createApolloProviderForVulnerabilityStateChange() });
}); });
it('consists of header, details, and footer', () => { it('consists of header, details, and footer', () => {
...@@ -84,7 +106,8 @@ describe('Vulnerability', () => { ...@@ -84,7 +106,8 @@ describe('Vulnerability', () => {
}); });
it('passes the correct properties to the children', () => { 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(findDetails().props('vulnerability')).toEqual(vulnerability);
expect(findFooter().props('vulnerability')).toEqual(vulnerability); expect(findFooter().props('vulnerability')).toEqual(vulnerability);
}); });
...@@ -95,23 +118,27 @@ describe('Vulnerability', () => { ...@@ -95,23 +118,27 @@ describe('Vulnerability', () => {
let refreshVulnerability; let refreshVulnerability;
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper({ apolloProvider: createApolloProviderForVulnerabilityStateChange() });
refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability'); refreshVulnerability = jest.spyOn(wrapper.vm.$apollo.queries.vulnerability, 'refetch');
makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions'); makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions');
}); });
it('updates the footer notes when the vulnerbility state was changed', () => { 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(makeRequest).toHaveBeenCalledTimes(1);
expect(refreshVulnerability).not.toHaveBeenCalled(); 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'); findFooter().vm.$emit('vulnerability-state-change');
expect(makeRequest).not.toHaveBeenCalled(); expect(makeRequest).not.toHaveBeenCalled();
expect(refreshVulnerability).toHaveBeenCalledTimes(1); 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', () => { ...@@ -119,9 +146,41 @@ describe('Vulnerability', () => {
it('renders false positive alert', () => { it('renders false positive alert', () => {
createWrapper({ createWrapper({
vulnData: { falsePositive: true }, vulnData: { falsePositive: true },
provide: { canViewFalsePositive: true },
}); });
expect(findAlert().exists()).toBe(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