Commit 1f39de01 authored by Savas Vedova's avatar Savas Vedova Committed by Dave Pisek

Move the refresh header query to parent

As part of https://gitlab.com/gitlab-org/gitlab/-/issues/228746 we
are changing the fetching of a vulnerability to GraphQL. Currently,
it's being injected through HAML. Since the GraphQL fields are still
not complete, we decided to go step by step and work on this iteratively.
The first step is moving the refetching of the header to the vulnerability
component. UI-wise nothing significant changes here.
parent 86541261
<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 { 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 +18,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: '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 +81,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 findHeader = () => wrapper.find(VulnerabilityHeader);
const findDetails = () => wrapper.find(Details);
const findFooter = () => wrapper.find(Footer);
const findFooter = () => wrapper.find(VulnerabilityFooter);
const findAlert = () => wrapper.find(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