Commit 725c66a2 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'Clean-Up-Vulnerability-Footer-Props' into 'master'

Clean Up Vulnerability Footer Props

See merge request gitlab-org/gitlab!40796
parents 59c6c59b b4c262e1
...@@ -4,6 +4,7 @@ import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue' ...@@ -4,6 +4,7 @@ import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue'
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue'; import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue'; import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
import Api from 'ee/api'; import Api from 'ee/api';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
...@@ -16,42 +17,8 @@ export default { ...@@ -16,42 +17,8 @@ export default {
name: 'VulnerabilityFooter', name: 'VulnerabilityFooter',
components: { IssueNote, SolutionCard, MergeRequestNote, HistoryEntry, RelatedIssues }, components: { IssueNote, SolutionCard, MergeRequestNote, HistoryEntry, RelatedIssues },
props: { props: {
discussionsUrl: { vulnerability: {
type: String,
required: true,
},
notesUrl: {
type: String,
required: true,
},
project: {
type: Object,
required: true,
},
solutionInfo: {
type: Object,
required: true,
},
issueFeedback: {
type: Object,
required: false,
default: () => null,
},
mergeRequestFeedback: {
type: Object, type: Object,
required: false,
default: () => null,
},
vulnerabilityId: {
type: Number,
required: true,
},
canModifyRelatedIssues: {
type: Boolean,
required: true,
},
relatedIssuesHelpPath: {
type: String,
required: true, required: true,
}, },
}, },
...@@ -73,11 +40,40 @@ export default { ...@@ -73,11 +40,40 @@ export default {
return acc; return acc;
}, {}); }, {});
}, },
project() {
return {
url: this.vulnerability.project.full_path,
value: this.vulnerability.project.full_name,
};
},
solutionInfo() {
const {
solution,
has_mr: hasMr,
vulnerability_feedback_help_path: vulnerabilityFeedbackHelpPath,
remediations,
state,
} = this.vulnerability;
const remediation = remediations?.[0];
const hasDownload = Boolean(
state !== VULNERABILITY_STATE_OBJECTS.resolved.state && remediation?.diff?.length && !hasMr,
);
return {
solution,
remediation,
hasDownload,
hasMr,
vulnerabilityFeedbackHelpPath,
isStandaloneVulnerability: true,
};
},
hasSolution() { hasSolution() {
return Boolean(this.solutionInfo.solution || this.solutionInfo.remediation); return Boolean(this.solutionInfo.solution || this.solutionInfo.remediation);
}, },
issueLinksEndpoint() { issueLinksEndpoint() {
return Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace(':id', this.vulnerabilityId); return Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace(':id', this.vulnerability.id);
}, },
}, },
...@@ -101,7 +97,7 @@ export default { ...@@ -101,7 +97,7 @@ export default {
}, },
fetchDiscussions() { fetchDiscussions() {
axios axios
.get(this.discussionsUrl) .get(this.vulnerability.discussions_url)
.then(({ data, headers: { date } }) => { .then(({ data, headers: { date } }) => {
this.discussionsDictionary = data.reduce((acc, discussion) => { this.discussionsDictionary = data.reduce((acc, discussion) => {
acc[discussion.id] = discussion; acc[discussion.id] = discussion;
...@@ -137,7 +133,9 @@ export default { ...@@ -137,7 +133,9 @@ export default {
this.poll = new Poll({ this.poll = new Poll({
resource: { resource: {
fetchNotes: () => fetchNotes: () =>
axios.get(this.notesUrl, { headers: { 'X-Last-Fetched-At': this.lastFetchedAt } }), axios.get(this.vulnerability.notes_url, {
headers: { 'X-Last-Fetched-At': this.lastFetchedAt },
}),
}, },
method: 'fetchNotes', method: 'fetchNotes',
successCallback: ({ data: { notes, last_fetched_at: lastFetchedAt } }) => { successCallback: ({ data: { notes, last_fetched_at: lastFetchedAt } }) => {
...@@ -194,16 +192,19 @@ export default { ...@@ -194,16 +192,19 @@ export default {
<div data-qa-selector="vulnerability_footer"> <div data-qa-selector="vulnerability_footer">
<solution-card v-if="hasSolution" v-bind="solutionInfo" /> <solution-card v-if="hasSolution" v-bind="solutionInfo" />
<div v-if="issueFeedback || mergeRequestFeedback" class="card gl-mt-5"> <div
v-if="vulnerability.issue_feedback || vulnerability.merge_request_feedback"
class="card gl-mt-5"
>
<issue-note <issue-note
v-if="issueFeedback" v-if="vulnerability.issue_feedback"
:feedback="issueFeedback" :feedback="vulnerability.issue_feedback"
:project="project" :project="project"
class="card-body" class="card-body"
/> />
<merge-request-note <merge-request-note
v-if="mergeRequestFeedback" v-if="vulnerability.merge_request_feedback"
:feedback="mergeRequestFeedback" :feedback="vulnerability.merge_request_feedback"
:project="project" :project="project"
class="card-body" class="card-body"
/> />
...@@ -211,9 +212,9 @@ export default { ...@@ -211,9 +212,9 @@ export default {
<related-issues <related-issues
:endpoint="issueLinksEndpoint" :endpoint="issueLinksEndpoint"
:can-modify-related-issues="canModifyRelatedIssues" :can-modify-related-issues="vulnerability.can_modify_related_issues"
:project-path="project.url" :project-path="project.url"
:help-path="relatedIssuesHelpPath" :help-path="vulnerability.related_issues_help_path"
/> />
<hr /> <hr />
...@@ -223,7 +224,7 @@ export default { ...@@ -223,7 +224,7 @@ export default {
v-for="discussion in discussions" v-for="discussion in discussions"
:key="discussion.id" :key="discussion.id"
:discussion="discussion" :discussion="discussion"
:notes-url="notesUrl" :notes-url="vulnerability.notes_url"
/> />
</ul> </ul>
</div> </div>
......
...@@ -38,7 +38,9 @@ export default { ...@@ -38,7 +38,9 @@ export default {
isProcessingAction: false, isProcessingAction: false,
isLoadingVulnerability: false, isLoadingVulnerability: false,
isLoadingUser: false, isLoadingUser: false,
vulnerability: this.initialVulnerability, // 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,
refreshVulnerabilitySource: undefined, refreshVulnerabilitySource: undefined,
}; };
......
<script> <script>
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import VulnerabilityHeader from './header.vue'; import VulnerabilityHeader from './header.vue';
import VulnerabilityDetails from './details.vue'; import VulnerabilityDetails from './details.vue';
import VulnerabilityFooter from './footer.vue'; import VulnerabilityFooter from './footer.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
components: { VulnerabilityHeader, VulnerabilityDetails, VulnerabilityFooter }, components: { VulnerabilityHeader, VulnerabilityDetails, VulnerabilityFooter },
...@@ -15,62 +13,6 @@ export default { ...@@ -15,62 +13,6 @@ export default {
}, },
}, },
computed: {
footerInfo() {
const {
vulnerabilityFeedbackHelpPath,
hasMr,
discussionsUrl,
createIssueUrl,
state,
issueFeedback,
mergeRequestFeedback,
notesUrl,
projectFingerprint,
remediations,
reportType,
solution,
id,
canModifyRelatedIssues,
relatedIssuesHelpPath,
} = convertObjectPropsToCamelCase(this.vulnerability);
const remediation = remediations?.length ? remediations[0] : null;
const hasDownload = Boolean(
state !== VULNERABILITY_STATE_OBJECTS.resolved.state && remediation?.diff?.length && !hasMr,
);
const hasRemediation = Boolean(remediation);
const props = {
vulnerabilityId: id,
discussionsUrl,
notesUrl,
projectFingerprint,
solutionInfo: {
solution,
remediation,
hasDownload,
hasMr,
hasRemediation,
vulnerabilityFeedbackHelpPath,
isStandaloneVulnerability: true,
},
createIssueUrl,
reportType,
issueFeedback,
mergeRequestFeedback,
canModifyRelatedIssues,
project: {
url: this.vulnerability.project.full_path,
value: this.vulnerability.project.full_name,
},
relatedIssuesHelpPath,
};
return props;
},
},
methods: { methods: {
refreshHeader() { refreshHeader() {
this.$refs.header.refreshVulnerability(); this.$refs.header.refreshVulnerability();
...@@ -92,7 +34,7 @@ export default { ...@@ -92,7 +34,7 @@ export default {
<vulnerability-details :vulnerability="vulnerability" /> <vulnerability-details :vulnerability="vulnerability" />
<vulnerability-footer <vulnerability-footer
ref="footer" ref="footer"
v-bind="footerInfo" :vulnerability="vulnerability"
@vulnerability-state-change="refreshHeader" @vulnerability-state-change="refreshHeader"
/> />
</div> </div>
......
...@@ -18,42 +18,23 @@ jest.mock('~/user_popovers'); ...@@ -18,42 +18,23 @@ jest.mock('~/user_popovers');
describe('Vulnerability Footer', () => { describe('Vulnerability Footer', () => {
let wrapper; let wrapper;
const minimumProps = { const vulnerability = {
discussionsUrl: `/discussions`, id: 1,
solutionInfo: { discussions_url: '/discussions',
hasDownload: false, notes_url: '/notes',
hasMr: false,
hasRemediation: false,
isStandaloneVulnerability: true,
remediation: null,
solution: undefined,
vulnerabilityFeedbackHelpPath:
'/help/user/application_security/index#interacting-with-the-vulnerabilities',
},
finding: {},
notesUrl: '/notes',
project: { project: {
url: '/root/security-reports', full_path: '/root/security-reports',
value: 'Administrator / Security Reports', full_name: 'Administrator / Security Reports',
}, },
vulnerabilityId: 1, can_modify_related_issues: true,
canModifyRelatedIssues: true, related_issues_help_path: 'help/path',
relatedIssuesHelpPath: 'help/path', has_mr: false,
}; vulnerability_feedback_help_path: 'feedback/help/path',
const solutionInfoProp = {
hasDownload: true,
hasMr: false,
isStandaloneVulnerability: true,
remediation: {},
solution: 'Upgrade to fixed version.\n',
vulnerabilityFeedbackHelpPath:
'/help/user/application_security/index#interacting-with-the-vulnerabilities',
}; };
const createWrapper = (props = minimumProps) => { const createWrapper = (properties = {}) => {
wrapper = shallowMount(VulnerabilityFooter, { wrapper = shallowMount(VulnerabilityFooter, {
propsData: props, propsData: { vulnerability: { ...vulnerability, ...properties } },
}); });
}; };
...@@ -77,9 +58,18 @@ describe('Vulnerability Footer', () => { ...@@ -77,9 +58,18 @@ describe('Vulnerability Footer', () => {
describe('solution card', () => { describe('solution card', () => {
it('does show solution card when there is one', () => { it('does show solution card when there is one', () => {
createWrapper({ ...minimumProps, solutionInfo: solutionInfoProp }); const properties = { remediations: [{ diff: [{}] }], solution: 'some solution' };
createWrapper(properties);
expect(wrapper.find(SolutionCard).exists()).toBe(true); expect(wrapper.find(SolutionCard).exists()).toBe(true);
expect(wrapper.find(SolutionCard).props()).toMatchObject(solutionInfoProp); expect(wrapper.find(SolutionCard).props()).toEqual({
solution: properties.solution,
remediation: properties.remediations[0],
hasDownload: true,
hasMr: vulnerability.has_mr,
vulnerabilityFeedbackHelpPath: vulnerability.vulnerability_feedback_help_path,
isStandaloneVulnerability: true,
});
}); });
it('does not show solution card when there is not one', () => { it('does not show solution card when there is not one', () => {
...@@ -89,19 +79,17 @@ describe('Vulnerability Footer', () => { ...@@ -89,19 +79,17 @@ describe('Vulnerability Footer', () => {
}); });
describe.each` describe.each`
type | prop | component type | prop | component
${'issue'} | ${'issueFeedback'} | ${IssueNote} ${'issue'} | ${'issue_feedback'} | ${IssueNote}
${'merge request'} | ${'mergeRequestFeedback'} | ${MergeRequestNote} ${'merge request'} | ${'merge_request_feedback'} | ${MergeRequestNote}
`('$type note', ({ prop, component }) => { `('$type note', ({ prop, component }) => {
// The object itself does not matter, we just want to make sure it's passed to the issue note. // The object itself does not matter, we just want to make sure it's passed to the issue note.
const feedback = {}; const feedback = {};
it('shows issue note when an issue exists for the vulnerability', () => { it('shows issue note when an issue exists for the vulnerability', () => {
createWrapper({ ...minimumProps, [prop]: feedback }); createWrapper({ [prop]: feedback });
expect(wrapper.find(component).exists()).toBe(true); expect(wrapper.find(component).exists()).toBe(true);
expect(wrapper.find(component).props()).toMatchObject({ expect(wrapper.find(component).props('feedback')).toBe(feedback);
feedback,
});
}); });
it('does not show issue note when there is no issue for the vulnerability', () => { it('does not show issue note when there is no issue for the vulnerability', () => {
...@@ -111,7 +99,7 @@ describe('Vulnerability Footer', () => { ...@@ -111,7 +99,7 @@ describe('Vulnerability Footer', () => {
}); });
describe('state history', () => { describe('state history', () => {
const discussionUrl = '/discussions'; const discussionUrl = vulnerability.discussions_url;
const historyList = () => wrapper.find({ ref: 'historyList' }); const historyList = () => wrapper.find({ ref: 'historyList' });
const historyEntries = () => wrapper.findAll(HistoryEntry); const historyEntries = () => wrapper.findAll(HistoryEntry);
...@@ -166,7 +154,7 @@ describe('Vulnerability Footer', () => { ...@@ -166,7 +154,7 @@ describe('Vulnerability Footer', () => {
const getDiscussion = (entries, index) => entries.at(index).props('discussion'); const getDiscussion = (entries, index) => entries.at(index).props('discussion');
const createNotesRequest = (...notes) => const createNotesRequest = (...notes) =>
mockAxios mockAxios
.onGet(minimumProps.notesUrl) .onGet(vulnerability.notes_url)
.replyOnce(200, { notes, last_fetched_at: Date.now() }); .replyOnce(200, { notes, last_fetched_at: Date.now() });
// Following #217184 the vulnerability polling uses an initial timeout // Following #217184 the vulnerability polling uses an initial timeout
...@@ -243,7 +231,7 @@ describe('Vulnerability Footer', () => { ...@@ -243,7 +231,7 @@ describe('Vulnerability Footer', () => {
}); });
it('shows an error if the notes poll fails', () => { it('shows an error if the notes poll fails', () => {
mockAxios.onGet(minimumProps.notesUrl).replyOnce(500); mockAxios.onGet(vulnerability.notes_url).replyOnce(500);
return axios.waitForAll().then(async () => { return axios.waitForAll().then(async () => {
await startTimeoutsAndAwaitRequests(); await startTimeoutsAndAwaitRequests();
...@@ -276,16 +264,16 @@ describe('Vulnerability Footer', () => { ...@@ -276,16 +264,16 @@ describe('Vulnerability Footer', () => {
it('has the correct props', () => { it('has the correct props', () => {
const endpoint = Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace( const endpoint = Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace(
':id', ':id',
minimumProps.vulnerabilityId, vulnerability.id,
); );
createWrapper(); createWrapper();
expect(relatedIssues().exists()).toBe(true); expect(relatedIssues().exists()).toBe(true);
expect(relatedIssues().props()).toMatchObject({ expect(relatedIssues().props()).toMatchObject({
endpoint, endpoint,
canModifyRelatedIssues: minimumProps.canModifyRelatedIssues, canModifyRelatedIssues: vulnerability.can_modify_related_issues,
projectPath: minimumProps.project.url, projectPath: vulnerability.project.full_path,
helpPath: minimumProps.relatedIssuesHelpPath, helpPath: vulnerability.related_issues_help_path,
}); });
}); });
}); });
......
...@@ -70,32 +70,9 @@ describe('Vulnerability', () => { ...@@ -70,32 +70,9 @@ describe('Vulnerability', () => {
}); });
it('passes the correct properties to the children', () => { it('passes the correct properties to the children', () => {
expect(findHeader().props()).toMatchObject({ expect(findHeader().props('initialVulnerability')).toBe(vulnerability);
initialVulnerability: vulnerability, expect(findDetails().props('vulnerability')).toBe(vulnerability);
}); expect(findFooter().props('vulnerability')).toBe(vulnerability);
expect(findDetails().props()).toMatchObject({ vulnerability });
expect(findFooter().props()).toMatchObject({
vulnerabilityId: vulnerability.id,
discussionsUrl: vulnerability.discussions_url,
notesUrl: vulnerability.notes_url,
solutionInfo: {
solution: vulnerability.solution,
remediation: vulnerability.remediation,
hasDownload: Boolean(vulnerability.has_download),
hasMr: vulnerability.has_mr,
hasRemediation: Boolean(vulnerability.has_remediation),
vulnerabilityFeedbackHelpPath: vulnerability.vulnerability_feedback_help_path,
isStandaloneVulnerability: true,
},
issueFeedback: vulnerability.issue_feedback,
mergeRequestFeedback: vulnerability.merge_request_feedback,
canModifyRelatedIssues: vulnerability.can_modify_related_issues,
project: {
url: vulnerability.project.full_path,
value: vulnerability.project.full_name,
},
relatedIssuesHelpPath: vulnerability.related_issues_help_path,
});
}); });
}); });
......
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