From 0f7c9ecdd0e40be4c226ee861b5110d13c296b27 Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Wed, 22 Feb 2023 10:55:59 +0100 Subject: [PATCH] SONAR-18464 React Testing Library for Security Hotspot - As a user without edit permissions --- .../api/mocks/SecurityHotspotServiceMock.ts | 63 +++++++++++++++---- .../__tests__/SecurityHotspotsApp-it.tsx | 47 +++++++++++++- .../components/HotspotReviewHistory.tsx | 3 +- 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts index d4729ef47b9..d906c95bd4d 100644 --- a/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts @@ -17,8 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { cloneDeep, range, times } from 'lodash'; -import { mockHotspot, mockRawHotspot, mockStandards } from '../../helpers/mocks/security-hotspots'; +import { cloneDeep, times } from 'lodash'; +import { + mockHotspot, + mockHotspotComment, + mockRawHotspot, + mockStandards, +} from '../../helpers/mocks/security-hotspots'; import { mockSourceLine } from '../../helpers/mocks/sources'; import { getStandards } from '../../helpers/security-standard'; import { mockPaging, mockRuleDetails, mockUser } from '../../helpers/testMocks'; @@ -26,6 +31,7 @@ import { BranchParameters } from '../../types/branch-like'; import { Hotspot, HotspotAssignRequest, + HotspotComment, HotspotResolution, HotspotStatus, } from '../../types/security-hotspots'; @@ -34,6 +40,9 @@ import { getMeasures } from '../measures'; import { getRuleDetails } from '../rules'; import { assignSecurityHotspot, + commentSecurityHotspot, + deleteSecurityHotspotComment, + editSecurityHotspotComment, getSecurityHotspotDetails, getSecurityHotspotList, getSecurityHotspots, @@ -42,11 +51,12 @@ import { import { searchUsers } from '../users'; const NUMBER_OF_LINES = 20; -const MAX_END_RANGE = 10; export default class SecurityHotspotServiceMock { hotspots: Hotspot[] = []; nextAssignee: string | undefined; + canChangeStatus: boolean = true; + hotspotsComments: HotspotComment[] = []; constructor() { this.reset(); @@ -67,13 +77,40 @@ export default class SecurityHotspotServiceMock { }) ) ); + jest.mocked(commentSecurityHotspot).mockImplementation(this.handleCommentSecurityHotspot); + jest + .mocked(deleteSecurityHotspotComment) + .mockImplementation(this.handleDeleteSecurityHotspotComment); + jest + .mocked(editSecurityHotspotComment) + .mockImplementation(this.handleEditSecurityHotspotComment); jest.mocked(getStandards).mockImplementation(this.handleGetStandards); } - handleGetSources = (data: { key: string; from?: number; to?: number } & BranchParameters) => { - return this.reply( - range(data.from || 1, data.to || MAX_END_RANGE).map((line) => mockSourceLine({ line })) - ); + handleCommentSecurityHotspot = () => { + this.hotspotsComments = [ + mockHotspotComment({ + htmlText: 'This is a comment from john doe', + markdown: 'This is a comment from john doe', + updatable: true, + }), + ]; + return Promise.resolve(); + }; + + handleDeleteSecurityHotspotComment = () => { + this.hotspotsComments = []; + return Promise.resolve(); + }; + + handleEditSecurityHotspotComment = () => { + const response = mockHotspotComment({ + htmlText: 'This is a comment from john doe test', + markdown: 'This is a comment from john doe test', + updatable: true, + }); + this.hotspotsComments = [response]; + return Promise.resolve(response); }; handleGetStandards = () => { @@ -87,9 +124,9 @@ export default class SecurityHotspotServiceMock { handleSearchUsers = () => { return this.reply({ users: [ - mockUser({ name: 'User John' }), - mockUser({ name: 'User Doe' }), - mockUser({ name: 'User Foo' }), + mockUser({ name: 'User John', login: 'user.john' }), + mockUser({ name: 'User Doe', login: 'user.doe' }), + mockUser({ name: 'User Foo', login: 'user.foo' }), ], paging: mockPaging(), }); @@ -177,7 +214,8 @@ export default class SecurityHotspotServiceMock { this.nextAssignee = undefined; } - hotspot.canChangeStatus = true; + hotspot.canChangeStatus = this.canChangeStatus; + hotspot.comment = this.hotspotsComments; return this.reply(hotspot); }; @@ -199,6 +237,8 @@ export default class SecurityHotspotServiceMock { return Promise.resolve(); }; + setHotspotChangeStatusPermission = (value: boolean) => (this.canChangeStatus = value); + reply(response: T): Promise { return Promise.resolve(cloneDeep(response)); } @@ -212,5 +252,6 @@ export default class SecurityHotspotServiceMock { message: "'2' is a magic number.", }), ]; + this.canChangeStatus = true; }; } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx index 719c8aecaec..a7ba5c392ef 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx @@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Route } from 'react-router-dom'; import selectEvent from 'react-select-event'; -import { byRole, byTestId, byText } from 'testing-library-selector'; +import { byDisplayValue, byRole, byTestId, byText } from 'testing-library-selector'; import SecurityHotspotServiceMock from '../../../api/mocks/SecurityHotspotServiceMock'; import { getSecurityHotspots, setSecurityHotspotStatus } from '../../../api/security-hotspots'; import { searchUsers } from '../../../api/users'; @@ -60,6 +60,11 @@ const ui = { hotspotTitle: (name: string | RegExp) => byRole('heading', { name }), hotspotStatus: byRole('heading', { name: 'status: hotspots.status_option.FIXED' }), hotpostListTitle: byRole('heading', { name: 'hotspots.list_title.TO_REVIEW.2' }), + hotspotCommentBox: byRole('textbox', { name: 'hotspots.comment.field' }), + commentSubmitButton: byRole('button', { name: 'hotspots.comment.submit' }), + commentEditButton: byRole('button', { name: 'issue.comment.edit' }), + commentDeleteButton: byRole('button', { name: 'issue.comment.delete' }), + textboxWithText: (value: string) => byDisplayValue(value), activeAssignee: byTestId('assignee-name'), successGlobalMessage: byRole('status'), currentUserSelectionItem: byText('foo'), @@ -175,6 +180,12 @@ it('should be able to change the status of a hotspot', async () => { expect(ui.hotspotStatus.get()).toBeInTheDocument(); }); +it('should not be able to change the status if does not have edit permissions', async () => { + handler.setHotspotChangeStatusPermission(false); + renderSecurityHotspotsApp(); + expect(await ui.selectStatus.find()).toBeDisabled(); +}); + it('should remember the comment when toggling change status panel for the same security hotspot', async () => { const user = userEvent.setup(); renderSecurityHotspotsApp(); @@ -197,6 +208,40 @@ it('should remember the comment when toggling change status panel for the same s expect(await screen.findByText(comment)).toBeInTheDocument(); }); +it('should be able to add, edit and remove own comments', async () => { + const uiComment = { + saveButton: byRole('button', { name: 'save' }), + deleteButton: byRole('button', { name: 'delete' }), + }; + const user = userEvent.setup(); + const comment = 'This is a comment from john doe'; + renderSecurityHotspotsApp(); + + const commentSection = await ui.hotspotCommentBox.find(); + const submitButton = ui.commentSubmitButton.get(); + + // Add a new comment + await user.click(commentSection); + await user.keyboard(comment); + await user.click(submitButton); + + expect(await screen.findByText(comment)).toBeInTheDocument(); + + // Edit the comment + await user.click(ui.commentEditButton.get()); + await user.click(ui.textboxWithText(comment).get()); + await user.keyboard(' test'); + await user.click(uiComment.saveButton.get()); + + expect(await byText(`${comment} test`).find()).toBeInTheDocument(); + + // Delete the comment + await user.click(ui.commentDeleteButton.get()); + await user.click(uiComment.deleteButton.get()); + + expect(screen.queryByText(`${comment} test`)).not.toBeInTheDocument(); +}); + function renderSecurityHotspotsApp(navigateTo?: string) { renderAppWithComponentContext( 'security_hotspots', diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx index 3dad3b014b6..df68802a9e6 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx @@ -130,6 +130,7 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) { } > setEditedCommentKey(key)} /> @@ -150,7 +151,7 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) { } overlayPlacement={PopupPlacement.BottomRight} > - + )} -- 2.39.5