/* * SonarQube * Copyright (C) 2009-2023 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * 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, countBy, pick, trim } from 'lodash'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; import { mockCurrentUser, mockPaging, mockQualityProfile, mockRuleDetails, mockRuleRepository, } from '../../helpers/testMocks'; import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules'; import { RawIssuesResponse } from '../../types/issues'; import { SearchRulesQuery } from '../../types/rules'; import { Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types'; import { NoticeType } from '../../types/users'; import { getFacet } from '../issues'; import { Profile, SearchQualityProfilesParameters, SearchQualityProfilesResponse, bulkActivateRules, bulkDeactivateRules, searchQualityProfiles, } from '../quality-profiles'; import { getRuleDetails, getRulesApp, searchRules, updateRule } from '../rules'; import { dismissNotice, getCurrentUser } from '../users'; interface FacetFilter { languages?: string; available_since?: string; } const FACET_RULE_MAP: { [key: string]: keyof Rule } = { languages: 'lang', types: 'type', }; export default class CodingRulesServiceMock { defaultRules: RuleDetails[] = []; rules: RuleDetails[] = []; qualityProfile: Profile[] = []; repositories: RuleRepository[] = []; isAdmin = false; applyWithWarning = false; dismissedNoticesEP = false; constructor() { this.repositories = [ mockRuleRepository({ key: 'repo1' }), mockRuleRepository({ key: 'repo2' }), ]; this.qualityProfile = [ mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }), mockQualityProfile({ key: 'p2', name: 'QP Bar', language: 'js' }), mockQualityProfile({ key: 'p3', name: 'QP FooBar', language: 'java', languageName: 'Java' }), ]; const resourceContent = 'Some link Awsome Reading'; const introTitle = 'Introduction to this rule'; const rootCauseContent = 'Root cause'; const howToFixContent = 'This is how to fix'; this.defaultRules = [ mockRuleDetails({ key: 'rule1', type: 'BUG', lang: 'java', langName: 'Java', name: 'Awsome java rule', }), mockRuleDetails({ key: 'rule2', name: 'Hot hotspot', type: 'SECURITY_HOTSPOT', lang: 'js', descriptionSections: [ { key: RuleDescriptionSections.INTRODUCTION, content: introTitle }, { key: RuleDescriptionSections.ROOT_CAUSE, content: rootCauseContent }, { key: RuleDescriptionSections.HOW_TO_FIX, content: howToFixContent }, { key: RuleDescriptionSections.ASSESS_THE_PROBLEM, content: 'Assess' }, { key: RuleDescriptionSections.RESOURCES, content: resourceContent, }, ], langName: 'JavaScript', }), mockRuleDetails({ key: 'rule3', name: 'Unknown rule', lang: 'js', langName: 'JavaScript' }), mockRuleDetails({ key: 'rule4', type: 'BUG', lang: 'c', langName: 'C', name: 'Awsome C rule', }), mockRuleDetails({ key: 'rule5', type: 'VULNERABILITY', lang: 'py', langName: 'Python', name: 'Awsome Python rule', descriptionSections: [ { key: RuleDescriptionSections.INTRODUCTION, content: introTitle }, { key: RuleDescriptionSections.HOW_TO_FIX, content: rootCauseContent }, { key: RuleDescriptionSections.RESOURCES, content: resourceContent, }, ], }), mockRuleDetails({ key: 'rule6', type: 'VULNERABILITY', lang: 'py', langName: 'Python', name: 'Bad Python rule', isExternal: true, descriptionSections: undefined, }), mockRuleDetails({ key: 'rule7', type: 'VULNERABILITY', lang: 'py', langName: 'Python', name: 'Python rule with context', descriptionSections: [ { key: RuleDescriptionSections.INTRODUCTION, content: 'Introduction to this rule with context', }, { key: RuleDescriptionSections.HOW_TO_FIX, content: 'This is how to fix for spring', context: { key: 'spring', displayName: 'Spring' }, }, { key: RuleDescriptionSections.HOW_TO_FIX, content: 'This is how to fix for spring boot', context: { key: 'spring_boot', displayName: 'Spring boot' }, }, { key: RuleDescriptionSections.RESOURCES, content: resourceContent, }, ], }), mockRuleDetails({ createdAt: '2022-12-16T17:26:54+0100', key: 'rule8', type: 'VULNERABILITY', lang: 'py', langName: 'Python', name: 'Awesome Python rule with education principles', descriptionSections: [ { key: RuleDescriptionSections.INTRODUCTION, content: introTitle }, { key: RuleDescriptionSections.HOW_TO_FIX, content: rootCauseContent }, { key: RuleDescriptionSections.RESOURCES, content: resourceContent, }, ], educationPrinciples: ['defense_in_depth', 'never_trust_user_input'], }), ]; (updateRule as jest.Mock).mockImplementation(this.handleUpdateRule); (searchRules as jest.Mock).mockImplementation(this.handleSearchRules); (getRuleDetails as jest.Mock).mockImplementation(this.handleGetRuleDetails); (searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles); (getRulesApp as jest.Mock).mockImplementation(this.handleGetRulesApp); (bulkActivateRules as jest.Mock).mockImplementation(this.handleBulkActivateRules); (bulkDeactivateRules as jest.Mock).mockImplementation(this.handleBulkDeactivateRules); (getFacet as jest.Mock).mockImplementation(this.handleGetGacet); (getCurrentUser as jest.Mock).mockImplementation(this.handleGetCurrentUser); (dismissNotice as jest.Mock).mockImplementation(this.handleDismissNotification); this.rules = cloneDeep(this.defaultRules); } getRulesWithoutDetails(rules: RuleDetails[]) { return rules.map((r) => pick(r, [ 'isTemplate', 'key', 'lang', 'langName', 'name', 'params', 'severity', 'status', 'sysTags', 'tags', 'type', ]) ); } filterFacet({ languages, available_since }: FacetFilter) { let filteredRules = this.rules; if (languages) { filteredRules = filteredRules.filter((r) => r.lang && languages.includes(r.lang)); } if (available_since) { filteredRules = filteredRules.filter( (r) => r.createdAt && new Date(r.createdAt) > new Date(available_since) ); } return this.getRulesWithoutDetails(filteredRules); } setIsAdmin() { this.isAdmin = true; } activateWithWarning() { this.applyWithWarning = true; } reset() { this.isAdmin = false; this.applyWithWarning = false; this.dismissedNoticesEP = false; this.rules = cloneDeep(this.defaultRules); } allRulesCount() { return this.rules.length; } allRulesName() { return this.rules.map((r) => r.name); } allQualityProfile(language: string) { return this.qualityProfile.filter((qp) => qp.language === language); } handleGetGacet = (): Promise<{ facet: { count: number; val: string }[]; response: RawIssuesResponse; }> => { return this.reply({ facet: [], response: { components: [], effortTotal: 0, facets: [], issues: [], languages: [], paging: { total: 0, pageIndex: 1, pageSize: 1 }, }, }); }; handleGetRuleDetails = (parameters: { actives?: boolean; key: string; }): Promise<{ actives?: RuleActivation[]; rule: RuleDetails }> => { const rule = this.rules.find((r) => r.key === parameters.key); if (!rule) { return Promise.reject({ errors: [{ msg: `No rule has been found for id ${parameters.key}` }], }); } return this.reply({ actives: parameters.actives ? [] : undefined, rule }); }; handleUpdateRule = (data: RulesUpdateRequest): Promise => { const rule = this.rules.find((r) => r.key === data.key); if (rule === undefined) { return Promise.reject({ errors: [{ msg: `No rule has been found for id ${data.key}` }], }); } const template = this.rules.find((r) => r.key === rule.templateKey); // Lets not convert the md to html in test. rule.mdDesc = data.markdown_description !== undefined ? data.markdown_description : rule.mdDesc; rule.htmlDesc = data.markdown_description !== undefined ? data.markdown_description : rule.htmlDesc; rule.mdNote = data.markdown_note !== undefined ? data.markdown_note : rule.mdNote; rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote; rule.name = data.name !== undefined ? data.name : rule.name; if (template && data.params) { rule.params = []; data.params.split(';').forEach((param) => { const parts = param.split('='); const paramsDef = template.params?.find((p) => p.key === parts[0]); rule.params?.push({ key: parts[0], type: paramsDef?.type || 'STRING', defaultValue: trim(parts[1], '" '), htmlDesc: paramsDef?.htmlDesc, }); }); } rule.remFnBaseEffort = data.remediation_fn_base_effort !== undefined ? data.remediation_fn_base_effort : rule.remFnBaseEffort; rule.remFnType = data.remediation_fn_type !== undefined ? data.remediation_fn_type : rule.remFnType; rule.severity = data.severity !== undefined ? data.severity : rule.severity; rule.status = data.status !== undefined ? data.status : rule.status; rule.tags = data.tags !== undefined ? data.tags.split(';') : rule.tags; return this.reply(rule); }; handleSearchRules = ({ facets, languages, p, ps, available_since, rule_key, }: SearchRulesQuery): Promise => { const countFacet = (facets || '').split(',').map((facet: keyof Rule) => { const facetCount = countBy( this.rules.map((r) => r[FACET_RULE_MAP[facet] || facet] as string) ); return { property: facet, values: Object.keys(facetCount).map((val) => ({ val, count: facetCount[val] })), }; }); const currentPs = ps || 10; const currentP = p || 1; let filteredRules: Rule[] = []; if (rule_key) { filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key); } else { filteredRules = this.filterFacet({ languages, available_since }); } const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs); return this.reply({ rules: responseRules, facets: countFacet, paging: mockPaging({ total: filteredRules.length, pageIndex: currentP, pageSize: currentPs, }), }); }; handleBulkActivateRules = () => { if (this.applyWithWarning) { return this.reply({ succeeded: this.rules.length - 1, failed: 1, errors: [{ msg: 'c rule c:S6069 cannot be activated on cpp profile SonarSource' }], }); } return this.reply({ succeeded: this.rules.length, failed: 0, errors: [], }); }; handleBulkDeactivateRules = () => { return this.reply({ succeeded: this.rules.length, failed: 0, }); }; handleSearchQualityProfiles = ({ language, }: SearchQualityProfilesParameters = {}): Promise => { let profiles: Profile[] = this.isAdmin ? this.qualityProfile.map((p) => ({ ...p, actions: { edit: true } })) : this.qualityProfile; if (language) { profiles = profiles.filter((p) => p.language === language); } return this.reply({ profiles }); }; handleGetRulesApp = () => { return this.reply({ canWrite: this.isAdmin, repositories: this.repositories }); }; handleGetCurrentUser = () => { return this.reply( mockCurrentUser({ dismissedNotices: { educationPrinciples: this.dismissedNoticesEP, }, }) ); }; handleDismissNotification = (noticeType: NoticeType) => { if (noticeType === NoticeType.EDUCATION_PRINCIPLES) { this.dismissedNoticesEP = true; return this.reply(true); } return Promise.reject(); }; reply(response: T): Promise { return Promise.resolve(cloneDeep(response)); } }