From ed1e83d84c98672cfc0fc492bc4229eb90620e33 Mon Sep 17 00:00:00 2001 From: stanislavh Date: Fri, 14 Jul 2023 10:18:54 +0200 Subject: [PATCH] SONAR-18424 Add RTL tests for custom rule --- .../js/api/mocks/CodingRulesServiceMock.ts | 53 +++++++++++---- .../coding-rules/__tests__/CodingRules-it.ts | 68 +++++++++++++------ .../coding-rules/components/RuleListItem.tsx | 1 + .../components/SimilarRulesFilter.tsx | 4 +- .../{utils-test.tsx => utils-tests.tsx} | 8 ++- .../resources/org/sonar/l10n/core.properties | 1 + 6 files changed, 95 insertions(+), 40 deletions(-) rename server/sonar-web/src/main/js/apps/coding-rules/{utils-test.tsx => utils-tests.tsx} (96%) diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts index 1132bbe8d15..ba49e9f5cfc 100644 --- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts @@ -68,6 +68,7 @@ type FacetFilter = Pick< | 'severities' | 'repositories' | 'qprofile' + | 'activation' | 'sonarsourceSecurity' | 'owaspTop10' | 'owaspTop10-2021' @@ -98,15 +99,19 @@ export default class CodingRulesServiceMock { standardsToRules: Partial<{ [category in keyof Standards]: { [standard: string]: string[] } }> = {}; - qualityProfilesToRules: { [qp: string]: string[] } = {}; - constructor() { this.repositories = [ mockRuleRepository({ key: 'repo1', name: 'Repository 1' }), mockRuleRepository({ key: 'repo2', name: 'Repository 2' }), ]; this.qualityProfile = [ - mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }), + mockQualityProfile({ + key: 'p1', + name: 'QP Foo', + language: 'java', + languageName: 'Java', + actions: { edit: true }, + }), mockQualityProfile({ key: 'p2', name: 'QP Bar', language: 'js' }), mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' }), mockQualityProfile({ @@ -249,7 +254,6 @@ export default class CodingRulesServiceMock { ], templateKey: 'rule8', }), - // Keep this last mockRuleDetails({ createdAt: '2022-12-16T17:26:54+0100', key: 'rule10', @@ -269,6 +273,13 @@ export default class CodingRulesServiceMock { ], educationPrinciples: ['defense_in_depth', 'never_trust_user_input'], }), + mockRuleDetails({ + key: 'rule11', + type: 'BUG', + lang: 'java', + langName: 'Java', + name: 'Common java rule', + }), ]; this.defaultRulesActivations = { @@ -291,10 +302,6 @@ export default class CodingRulesServiceMock { }, }; - this.qualityProfilesToRules = { - p3: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6', 'rule7', 'rule8'], - }; - jest.mocked(updateRule).mockImplementation(this.handleUpdateRule); jest.mocked(createRule).mockImplementation(this.handleCreateRule); jest.mocked(deleteRule).mockImplementation(this.handleDeleteRule); @@ -347,6 +354,7 @@ export default class CodingRulesServiceMock { owaspTop10, 'owaspTop10-2021': owasp2021Top10, cwe, + activation, }: FacetFilter) { let filteredRules = this.rules; if (types) { @@ -358,6 +366,18 @@ export default class CodingRulesServiceMock { if (severities) { filteredRules = filteredRules.filter((r) => r.severity && severities.includes(r.severity)); } + if (qprofile && activation !== undefined) { + const qProfileLang = this.qualityProfile.find((p) => p.key === qprofile)?.language; + filteredRules = filteredRules + .filter((r) => r.lang === qProfileLang) + .filter((r) => { + const qProfilesInRule = this.rulesActivations[r.key]?.map((ra) => ra.qProfile) ?? []; + const ruleHasQueriedProfile = qProfilesInRule.includes(qprofile); + return activation === 'true' ? ruleHasQueriedProfile : !ruleHasQueriedProfile; + }); + + console.log(filteredRules); + } if (available_since) { filteredRules = filteredRules.filter( (r) => r.createdAt && new Date(r.createdAt) > new Date(available_since) @@ -369,10 +389,6 @@ export default class CodingRulesServiceMock { if (repositories) { filteredRules = filteredRules.filter((r) => r.lang && repositories.includes(r.repo)); } - if (qprofile) { - const rules = this.qualityProfilesToRules[qprofile] ?? []; - filteredRules = filteredRules.filter((r) => rules.includes(r.key)); - } if (sonarsourceSecurity) { const matchingRules = this.standardsToRules[SecurityStandard.SONARSOURCE]?.[sonarsourceSecurity] ?? []; @@ -552,6 +568,7 @@ export default class CodingRulesServiceMock { q, rule_key, is_template, + activation, }: SearchRulesQuery): Promise => { const standards = await getStandards(); const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = []; @@ -595,6 +612,7 @@ export default class CodingRulesServiceMock { filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key); } else { filteredRules = this.filterFacet({ + qprofile, languages, available_since, q, @@ -603,11 +621,11 @@ export default class CodingRulesServiceMock { types, tags, is_template, - qprofile, sonarsourceSecurity, owaspTop10, 'owaspTop10-2021': owasp2021Top10, cwe, + activation, }); } const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs); @@ -652,13 +670,20 @@ export default class CodingRulesServiceMock { severity?: string; }) => { const nextActivation = mockRuleActivation({ qProfile: data.key, severity: data.severity }); + + if (!this.rulesActivations[data.rule]) { + this.rulesActivations[data.rule] = [nextActivation]; + return this.reply(undefined); + } + const activationIndex = this.rulesActivations[data.rule]?.findIndex((activation) => { return activation.qProfile === data.key; }); + if (activationIndex !== -1) { this.rulesActivations[data.rule][activationIndex] = nextActivation; } else { - this.rulesActivations[data.rule] = [...this.rulesActivations[data.rule], nextActivation]; + this.rulesActivations[data.rule].push(nextActivation); } return this.reply(undefined); }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 39a5ef3e8e6..c3edaea9492 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { act, fireEvent, screen } from '@testing-library/react'; -import { last } from 'lodash'; import selectEvent from 'react-select-event'; import CodingRulesServiceMock, { RULE_TAGS_MOCK } from '../../../api/mocks/CodingRulesServiceMock'; import { RULE_TYPES } from '../../../helpers/constants'; @@ -27,7 +26,7 @@ import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { dateInputEvent, renderAppRoutes } from '../../../helpers/testReactTestingUtils'; import { CurrentUser } from '../../../types/users'; import routes from '../routes'; -import { getPageObjects } from '../utils-test'; +import { getPageObjects } from '../utils-tests'; jest.mock('../../../api/rules'); jest.mock('../../../api/issues'); @@ -88,7 +87,7 @@ describe('Rules app', () => { renderCodingRulesApp(mockCurrentUser()); await ui.appLoaded(); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); // Filter by language facet await act(async () => { @@ -115,7 +114,7 @@ describe('Rules app', () => { await act(async () => { await user.click(ui.clearAllFiltersButton.get()); }); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); // Filter by repository await act(async () => { @@ -135,14 +134,14 @@ describe('Rules app', () => { await act(async () => { await user.click(ui.clearAllFiltersButton.get()); }); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); // Filter by quality profile await act(async () => { await user.click(ui.qpFacet.get()); - await user.click(ui.facetItem('QP FooBar Java').get()); + await user.click(ui.facetItem('QP Foo Java').get()); }); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(8); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); // Filter by tag await act(async () => { @@ -165,7 +164,7 @@ describe('Rules app', () => { renderCodingRulesApp(mockCurrentUser()); await ui.appLoaded(); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); await act(async () => { await user.click(ui.standardsFacet.get()); await user.click(ui.facetItem('Buffer Overflow').get()); @@ -202,7 +201,7 @@ describe('Rules app', () => { await act(async () => { await user.click(ui.facetClear('issues.facet.standards').get()); }); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); }); it('filters by similar rules', async () => { @@ -210,25 +209,25 @@ describe('Rules app', () => { renderCodingRulesApp(mockCurrentUser()); await ui.appLoaded(); - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(11); - const lastRule = last(ui.ruleListItem.getAll()); + const ruleName = 'Awesome Python rule with education principles'; - await user.click(ui.similarIssuesButton.get(lastRule)); - await user.click(ui.similarIssuesFilterByLang('Python').get(lastRule)); + await user.click(ui.similarIssuesButton(ruleName).get()); + await user.click(ui.similarIssuesFilterByLang('Python').get()); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6); - await user.click(ui.similarIssuesButton.get(lastRule)); - await user.click(ui.similarIssuesFilterByType('issue.type.VULNERABILITY').get(lastRule)); + await user.click(ui.similarIssuesButton(ruleName).get()); + await user.click(ui.similarIssuesFilterByType('issue.type.VULNERABILITY').get()); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(3); expect(ui.facetItem('issue.type.VULNERABILITY').get()).toBeChecked(); - await user.click(ui.similarIssuesButton.get(lastRule)); - await user.click(ui.similarIssuesFilterBySeverity('MINOR').get(lastRule)); + await user.click(ui.similarIssuesButton(ruleName).get()); + await user.click(ui.similarIssuesFilterBySeverity('MINOR').get()); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2); - await user.click(ui.similarIssuesButton.get(lastRule)); - await user.click(ui.similarIssuesFilterByTag('awesome').get(lastRule)); + await user.click(ui.similarIssuesButton(ruleName).get()); + await user.click(ui.similarIssuesFilterByTag('awesome').get()); expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); }); @@ -314,7 +313,33 @@ describe('Rules app', () => { it('should be able to deactivate for specific quality profile', async () => {}); }); - it('can activate/deactivate specific rule for quality profile', async () => {}); + it('can activate/deactivate specific rule for quality profile', async () => { + const { ui, user } = getPageObjects(); + renderCodingRulesApp(mockLoggedInUser()); + await ui.appLoaded(); + + await user.click(ui.qpFacet.get()); + await user.click(ui.facetItem('QP Foo Java').get()); + + // Only one rule is activated in selected QP + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); + + // Switch to inactive rules + await user.click(ui.qpInactiveRadio.get(ui.facetItem('QP Foo Java').get())); + expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1); + + // Activate Rule for qp + await user.click(ui.activateButton.get()); + await user.click(ui.activateButton.get(ui.activateQPDialog.get())); + expect(ui.activateButton.query()).not.toBeInTheDocument(); + expect(ui.deactivateButton.get()).toBeInTheDocument(); + + // Deactivate activated rule + await user.click(ui.deactivateButton.get()); + await user.click(ui.yesButton.get()); + expect(ui.deactivateButton.query()).not.toBeInTheDocument(); + expect(ui.activateButton.get()).toBeInTheDocument(); + }); it('navigates by keyboard', async () => { const { user, ui } = getPageObjects(); @@ -490,7 +515,7 @@ describe('Rule app', () => { await user.click(ui.cancelButton.get()); // Deactivate rule in quality profile - await user.click(ui.deactivateButton('QP FooBar').get()); + await user.click(ui.deactivateInQPButton('QP FooBar').get()); await act(() => user.click(ui.yesButton.get())); expect(ui.qpLink('QP FooBar').query()).not.toBeInTheDocument(); }); @@ -621,7 +646,6 @@ describe('Rule app', () => { await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get())); // Shows the list of rules, custom rule should not be included - expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(9); expect(ui.ruleListItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument(); }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index 7631457d32d..1f5f0e74ccc 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -130,6 +130,7 @@ export default class RuleListItem extends React.PureComponent { renderActions = () => { const { activation, isLoggedIn, rule, selectedProfile } = this.props; + if (!selectedProfile || !isLoggedIn) { return null; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx index 73fc930e32f..4d76aeea820 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx @@ -19,8 +19,8 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { Button, ButtonPlain } from '../../../components/controls/buttons'; import Dropdown from '../../../components/controls/Dropdown'; +import { Button, ButtonPlain } from '../../../components/controls/buttons'; import DropdownIcon from '../../../components/icons/DropdownIcon'; import FilterIcon from '../../../components/icons/FilterIcon'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; @@ -139,7 +139,7 @@ export default class SimilarRulesFilter extends React.PureComponent { >