diff --git a/pom.xml b/pom.xml index e57b1aa9..8c2d399a 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,10 @@ jakarta.servlet-api 6.1.0 + + org.springframework.boot + spring-boot-starter-thymeleaf + diff --git a/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java new file mode 100644 index 00000000..ef212b17 --- /dev/null +++ b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java @@ -0,0 +1,534 @@ +/** + * Copyright (c) 2024, Samsung Electronics Co., Ltd. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +package com.lpvs.entity.report; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.entity.LPVSQueue; +import com.lpvs.entity.enums.LPVSVcs; +import com.lpvs.service.LPVSLicenseService; +import io.micrometer.common.util.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +import static com.lpvs.util.LPVSCommentUtil.getMatchedLinesAsLink; + +/** + * A class responsible for building reports based on the results of license scanning. + */ +@Component +@Slf4j +public class LPVSReportBuilder { + + /** + * Creates a new instance of the LPVSReportBuilder class. + * + * @param templateEngine the template engine to use for generating reports + */ + @Autowired + public LPVSReportBuilder(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + /** + * The template engine to use for generating reports. + */ + private final TemplateEngine templateEngine; + + /** + * The type of license detection scanner. + */ + @Value("${scanner:scanoss}") + private String scannerType; + + /** + * The version of LPVS application. + */ + @Value("${lpvs.version:Unknown}") + private String lpvsVersion; + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final String permitted = "PERMITTED"; + private final String restricted = "RESTRICTED"; + private final String prohibited = "PROHIBITED"; + private final String unreviewed = "UNREVIEWED"; + + /** + * A class representing a group of elements with a count. + * + * @param the type of elements in the group + */ + @AllArgsConstructor + private static class GroupInfo { + /** + * The number of elements in the group. + */ + @Getter private long count; + + /** + * The elements in the group. + */ + private T elements; + } + + /** + * Generates an HTML report for a single scan. + * + * @param path the path to the source folder for scan or pull request URL + * @param scanResults the results of the license scan + * @param conflicts a list of license conflicts found during the scan + * @param webhookConfig configuration related to the repository and webhook + * @param vcs the string representation of the version control system + * @return the HTML code of the generated report + */ + public String generateHtmlReportSingleScan( + String path, + List scanResults, + List> conflicts, + LPVSQueue webhookConfig, + LPVSVcs vcs) { + Context context = new Context(); + String date = sdf.format(new Date()); + + context.setVariable("title", "Report-LPVS-" + date); + context.setVariable("scanDate", date); + context.setVariable("codeLocation", path); + context.setVariable("usedScanner", scannerType); + context.setVariable("lpvsVersion", lpvsVersion); + + Map> detectedLicenseInfo = + groupScanResultsForLicenseTable(scanResults); + + long prohibitedLicenses = getDetectedLicenseCountByType(detectedLicenseInfo, prohibited); + long restrictedLicenses = getDetectedLicenseCountByType(detectedLicenseInfo, restricted); + long unreviewedLicenses = getDetectedLicenseCountByType(detectedLicenseInfo, unreviewed); + long licenseDetected = prohibitedLicenses + restrictedLicenses + unreviewedLicenses; + + context.setVariable("licenseDetected", licenseDetected); + context.setVariable("prohibitedLicenses", prohibitedLicenses); + context.setVariable("restrictedLicenses", restrictedLicenses); + context.setVariable("unreviewedLicenses", unreviewedLicenses); + + if (scanResults != null && !scanResults.isEmpty()) { + context.setVariable( + "licenseTable", + generateLicenseTableHTML(detectedLicenseInfo, webhookConfig, vcs)); + } else { + context.setVariable("licenseTable", null); + } + + if (conflicts != null && !conflicts.isEmpty()) { + context.setVariable("licenseConflicts", conflicts.size()); + context.setVariable("conflictTable", generateLicenseConflictsTableHTML(conflicts)); + } else { + context.setVariable("licenseConflicts", 0); + context.setVariable("conflictTable", null); + } + + return templateEngine.process("report_single_scan", context); + } + + /** + * Saves HTML report to given location. + * + * @param htmlContent The string, containing report in HTML format. + * @param filePath The path to expected html report file. + */ + public static void saveHTMLToFile(String htmlContent, String filePath) { + File file = new File(filePath); + try (BufferedWriter writer = + new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) { + writer.write(htmlContent); + log.info("LPVS report saved to: " + filePath); + } catch (IOException ex) { + log.error("Error during saving HTML report: " + ex.getMessage()); + } + } + + /** + * Generates the license conflicts table HTML content. + * + * @param conflicts a list of license conflicts + * @return the HTML content for the license conflicts table + */ + private String generateLicenseConflictsTableHTML( + List> conflicts) { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder + .append("") + .append("") + .append("") + .append(""); + + for (LPVSLicenseService.Conflict conflict : conflicts) { + htmlBuilder + .append("") + .append("") + .append("") + .append(""); + } + htmlBuilder.append("
ConflictExplanation
") + .append(conflict.l1) + .append(" and ") + .append(conflict.l2) + .append("") + .append(getExplanationForLicenseConflict(conflict.l1, conflict.l2)) + .append("
"); + return htmlBuilder.toString(); + } + + /** + * Retrieves the explanation for a specific license conflict. + * + * @param lic1 the first license involved in the conflict + * @param lic2 the second license involved in the conflict + * @return the explanation for the specified license conflict + */ + private String getExplanationForLicenseConflict(String lic1, String lic2) { + return "These two licenses are incompatible due to their conflicting terms and conditions. " + + "It is recommended to resolve this conflict by choosing either " + + lic1 + + " or " + + lic2 + + " for the affected components."; + } + + /** + * Generates the license table HTML content. + * + * @param detectedLicenseInfo grouped scan results by license SPDX ID and access type, component name and vendor + * @param webhookConfig configuration related to the repository and webhook + * @param vcs the string representation of the version control system + * @return the HTML content for the license table + */ + private String generateLicenseTableHTML( + Map> detectedLicenseInfo, LPVSQueue webhookConfig, LPVSVcs vcs) { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append(""); + // Prohibited licenses + addBlockOfTableForLicenseTypeHTML( + htmlBuilder, detectedLicenseInfo, prohibited, webhookConfig, vcs); + // Restricted licenses + addBlockOfTableForLicenseTypeHTML( + htmlBuilder, detectedLicenseInfo, restricted, webhookConfig, vcs); + // Unreviewed licenses + addBlockOfTableForLicenseTypeHTML( + htmlBuilder, detectedLicenseInfo, unreviewed, webhookConfig, vcs); + // Permitted licenses + addBlockOfTableForLicenseTypeHTML( + htmlBuilder, detectedLicenseInfo, permitted, webhookConfig, vcs); + + htmlBuilder.append("
License Type / ExplanationLicense SPDX IDVendor / ComponentVersionRepository File PathComponent File PathMatched LinesMatch Value
"); + return htmlBuilder.toString(); + } + + /** + * Adds a block of HTML content for a specific license type to the license table. + * + * @param htmlBuilder the StringBuilder object to which the HTML content will be appended + * @param detectedLicenseInfo grouped scan results by license SPDX ID and access type, component name and vendor + * @param type the license type for which to add the block of HTML content + * @param webhookConfig configuration related to the repository and webhook + * @param vcs the string representation of the version control system + */ + private void addBlockOfTableForLicenseTypeHTML( + StringBuilder htmlBuilder, + Map> detectedLicenseInfo, + String type, + LPVSQueue webhookConfig, + LPVSVcs vcs) { + long detectedLicenseCountByType = getDetectedLicenseCountByType(detectedLicenseInfo, type); + boolean isNewRow; + if (detectedLicenseCountByType > 0) { + htmlBuilder.append(""); + isNewRow = true; + + htmlBuilder + .append(""); + switch (type.toUpperCase()) { + case prohibited: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + case restricted: + case unreviewed: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + case permitted: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + } + htmlBuilder.append(" / "); + htmlBuilder.append(getExplanationForLicenseType(type)); + htmlBuilder.append(""); + + // license spdx + Map> licenseSpdxIds = + (Map>) detectedLicenseInfo.get(type).elements; + for (String licenseSpdxId : licenseSpdxIds.keySet()) { + if (!isNewRow) { + htmlBuilder.append(""); + isNewRow = true; + } + htmlBuilder + .append(""); + htmlBuilder.append(licenseSpdxId); + htmlBuilder.append(""); + + // vendor + component + Map> componentAndVendor = + (Map>) licenseSpdxIds.get(licenseSpdxId).elements; + for (String componentInfo : componentAndVendor.keySet()) { + if (!isNewRow) { + htmlBuilder.append(""); + isNewRow = true; + } + + htmlBuilder + .append(""); + htmlBuilder + .append("") + .append(componentInfo.split(":::")[0]) + .append(""); + htmlBuilder.append(""); + + // file info + List fileInfos = + (List) componentAndVendor.get(componentInfo).elements; + for (LPVSFile fileInfo : fileInfos) { + if (!isNewRow) { + htmlBuilder.append(""); + } + htmlBuilder + .append("") + .append(fileInfo.getComponentVersion()) + .append("") + .append(fileInfo.getFilePath()) + .append(""); + + if (!StringUtils.isBlank(fileInfo.getComponentFileUrl())) { + htmlBuilder + .append("") + .append(fileInfo.getComponentFilePath()) + .append(""); + } else { + htmlBuilder.append(fileInfo.getComponentFilePath()); + } + + htmlBuilder + .append("") + .append(getMatchedLinesAsLink(webhookConfig, fileInfo, vcs)) + .append("") + .append(fileInfo.getSnippetMatch()) + .append(""); + + htmlBuilder.append(""); + isNewRow = false; + } + } + } + } + } + + /** + * Retrieves the explanation for a specific license type. + * + * @param type the license type for which to retrieve the explanation + * @return the explanation for the specified license type + */ + private String getExplanationForLicenseType(String type) { + switch (type.toUpperCase()) { + case prohibited: + return "This license prohibits the use of the licensed code in certain contexts, such as commercial software development."; + case restricted: + return "This license required compliance with specific obligations. It is crucial to carefully review and adhere to these obligations before using the licensed code."; + case permitted: + return "This license permits free usage, modification, and distribution of the licensed code without any restrictions."; + case unreviewed: + default: + return "This license has not been reviewed thoroughly and may contain unknown risks or limitations. It is recommended to review these licenses carefully before using the licensed code."; + } + } + + /** + * Function that returns the number of licenses detected for the given type. + * + * @param detectedLicenseInfo grouped scan results by license SPDX ID and access type, component name and vendor + * @param type the license type to count + * @return the number of licenses detected for the given type + */ + private long getDetectedLicenseCountByType( + Map> detectedLicenseInfo, String type) { + if (detectedLicenseInfo == null || detectedLicenseInfo.get(type) == null) { + return 0; + } + return ((Map>) detectedLicenseInfo.get(type).elements).size(); + } + + /** + * Function that returns the converted list of LPVS files with a single license. + * + * @param scanResults the results of the license scan + * @return a list of LPVS files with a single license + */ + private List getLpvsFilesFromScanResults(List scanResults) { + List filesScanResults = new ArrayList<>(); + for (LPVSFile file : scanResults) { + Set licenses = file.getLicenses(); + for (LPVSLicense license : licenses) { + LPVSFile file_ = + new LPVSFile() { + { + setFilePath(file.getFilePath()); + setAbsoluteFilePath(file.getAbsoluteFilePath()); + setSnippetType(file.getSnippetType()); + setSnippetMatch(file.getSnippetMatch()); + setMatchedLines(file.getMatchedLines()); + setLicenses(new HashSet<>(Collections.singletonList(license))); + setComponentFilePath(file.getComponentFilePath()); + setComponentFileUrl(file.getComponentFileUrl()); + setComponentName(file.getComponentName()); + setComponentLines(file.getComponentLines()); + setComponentUrl(file.getComponentUrl()); + setComponentVendor(file.getComponentVendor()); + setComponentVersion(file.getComponentVersion()); + } + }; + filesScanResults.add(file_); + } + } + return filesScanResults; + } + + /** + * Groups the scan results by license type for display in the license table. + * + * @param scanResults the results of the license scan + */ + private Map> groupScanResultsForLicenseTable(List scanResults) { + Map> detectedLicenseInfo = null; + if (scanResults != null && !scanResults.isEmpty()) { + List filesScanResults = getLpvsFilesFromScanResults(scanResults); + + detectedLicenseInfo = + filesScanResults.stream() + .collect( + Collectors.groupingBy( + this::getLicenseAccess, + Collectors.collectingAndThen( + Collectors.groupingBy( + this::getLicenseSpdxId, + Collectors.collectingAndThen( + Collectors.groupingBy( + this::getComponentKey, + Collectors + .collectingAndThen( + Collectors + .toList(), + files -> + new GroupInfo<>( + files + .size(), + files))), + this::sumGroupInfo)), + this::sumGroupInfo))); + } + return detectedLicenseInfo; + } + + /** + * Sums the counts of all GroupInfo objects in the given map and returns a new GroupInfo object + * containing the total count and the original map. + * + * @param groupedBy a map of strings to GroupInfo objects + * @return a new GroupInfo object containing the total count of all GroupInfo objects in the map + * and the original map + */ + private GroupInfo sumGroupInfo(Map> groupedBy) { + return new GroupInfo<>( + groupedBy.values().stream().mapToLong(GroupInfo::getCount).sum(), groupedBy); + } + + /** + * Grouping criteria for the components + * + * @param lpvsFile the LPVSFile whose license SPDX ID is to be retrieved + * @return the component key that contains component name, vendor name and component URL + */ + private String getComponentKey(LPVSFile lpvsFile) { + return lpvsFile.getComponentVendor() + + " / " + + lpvsFile.getComponentName() + + ":::" + + lpvsFile.getComponentUrl(); + } + + /** + * Grouping criteria for the license SPDX ID + * + * @param lpvsFile the LPVSFile whose license SPDX ID is to be retrieved + * @return the SPDX ID of the license + */ + private String getLicenseSpdxId(LPVSFile lpvsFile) { + return lpvsFile.getLicenses().stream().findFirst().get().getSpdxId(); + } + + /** + * Grouping criteria for the license access type + * + * @param lpvsFile the LPVSFile whose license SPDX ID is to be retrieved + * @return the access type of the license + */ + private String getLicenseAccess(LPVSFile lpvsFile) { + return lpvsFile.getLicenses().stream().findFirst().get().getAccess().toUpperCase(); + } +} diff --git a/src/main/java/com/lpvs/service/scan/LPVSDetectService.java b/src/main/java/com/lpvs/service/scan/LPVSDetectService.java index 8b3fb110..6e2ae929 100644 --- a/src/main/java/com/lpvs/service/scan/LPVSDetectService.java +++ b/src/main/java/com/lpvs/service/scan/LPVSDetectService.java @@ -12,6 +12,7 @@ import java.util.Date; import java.util.List; +import com.lpvs.entity.report.LPVSReportBuilder; import com.lpvs.service.LPVSGitHubConnectionService; import com.lpvs.service.LPVSGitHubService; import com.lpvs.service.LPVSLicenseService; @@ -32,6 +33,8 @@ import lombok.extern.slf4j.Slf4j; +import static com.lpvs.entity.report.LPVSReportBuilder.saveHTMLToFile; + /** * Service class for detecting licenses in GitHub pull requests using a specified scanner. */ @@ -59,6 +62,11 @@ public class LPVSDetectService { */ private LPVSScanService scanService; + /** + * Component responsible for the generation of HTML reports. + */ + private LPVSReportBuilder reportBuilder; + /** * Trigger value to start a single scan of a pull request (optional). */ @@ -91,6 +99,7 @@ public class LPVSDetectService { * @param licenseService Service for license conflict analysis. * @param gitHubService Service for GitHub connection and operation. * @param scanServiceFactory Service for creating instance of the scanner. + * @param reportBuilder Service for generating HTML reports. */ @Autowired public LPVSDetectService( @@ -99,12 +108,14 @@ public LPVSDetectService( LPVSGitHubConnectionService gitHubConnectionService, LPVSLicenseService licenseService, LPVSGitHubService gitHubService, - LPVSScanServiceFactory scanServiceFactory) { + LPVSScanServiceFactory scanServiceFactory, + LPVSReportBuilder reportBuilder) { this.gitHubConnectionService = gitHubConnectionService; this.licenseService = licenseService; this.gitHubService = gitHubService; this.scanService = scanServiceFactory.createScanService(scannerType, isInternal); log.info("License detection scanner: " + scannerType); + this.reportBuilder = reportBuilder; } /** @@ -117,6 +128,7 @@ public void runSingleScan() { LPVSQueue webhookConfig = null; List scanResult = null; List> detectedConflicts = null; + String path = null; // Error case when both pull request scan and local files scan are set to true if (!StringUtils.isBlank(trigger) && !StringUtils.isBlank(localPath)) { @@ -138,6 +150,7 @@ public void runSingleScan() { detectedConflicts = licenseService.findConflicts(webhookConfig, scanResult); generateReport = true; + path = HtmlUtils.htmlEscape(trigger); log.info("Single scan of pull request completed."); } catch (Exception ex) { log.error("Single scan of pull request failed with error: " + ex.getMessage()); @@ -165,6 +178,7 @@ public void runSingleScan() { detectedConflicts = licenseService.findConflicts(webhookConfig, scanResult); generateReport = true; + path = localFile.getAbsolutePath(); log.info("Single scan of local file(s) completed."); } else { throw new Exception("File path does not exist: " + localPath); @@ -187,9 +201,9 @@ public void runSingleScan() { File folder = new File(folderPath); if (folder.exists() && folder.isDirectory()) { String reportFile = - LPVSCommentUtil.buildHTMLComment( - webhookConfig, scanResult, detectedConflicts); - LPVSCommentUtil.saveHTMLToFile(reportFile, report.getAbsolutePath()); + reportBuilder.generateHtmlReportSingleScan( + path, scanResult, detectedConflicts, null, null); + saveHTMLToFile(reportFile, report.getAbsolutePath()); } else { log.error("Error: The parent directory '" + folder.getPath() + "' does not exist."); } diff --git a/src/main/java/com/lpvs/util/LPVSCommentUtil.java b/src/main/java/com/lpvs/util/LPVSCommentUtil.java index bacb9136..8e650ef5 100644 --- a/src/main/java/com/lpvs/util/LPVSCommentUtil.java +++ b/src/main/java/com/lpvs/util/LPVSCommentUtil.java @@ -6,11 +6,6 @@ */ package com.lpvs.util; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; import com.lpvs.entity.LPVSDetectedLicense; @@ -44,6 +39,9 @@ public class LPVSCommentUtil { */ public static String getMatchedLinesAsLink( LPVSQueue webhookConfig, LPVSFile file, LPVSVcs vcs) { + if (webhookConfig == null) { + return file.getMatchedLines(); + } String prefix = LPVSPayloadUtil.getRepositoryUrl(webhookConfig) + "/blob/" @@ -138,90 +136,4 @@ public static String reportCommentBuilder( return commitCommentBuilder.toString(); } - - /** - * Generates a formatted string for an HTML report with scan results. - * - * @param webhookConfig The {@link LPVSQueue} configuration for the webhook. - * @param scanResults List containing preformatted scan results. - * @param conflicts List containing license conflict information. - * @return A string containing scan results in HTML format. - */ - public static String buildHTMLComment( - LPVSQueue webhookConfig, - List scanResults, - List> conflicts) { - StringBuilder htmlBuilder = new StringBuilder(); - - htmlBuilder.append(""); - - if (scanResults != null && scanResults.size() != 0) { - htmlBuilder.append("

Detected licenses:

"); - for (LPVSFile file : scanResults) { - htmlBuilder - .append("

File: ") - .append(file.getFilePath()) - .append("

"); - htmlBuilder - .append("

License(s): ") - .append(file.convertLicensesToString(LPVSVcs.GITHUB)) - .append("

"); - htmlBuilder - .append("

Component: ") - .append(file.getComponentName()) - .append(" (") - .append(file.getComponentFilePath()) - .append(")

"); - htmlBuilder - .append("

Matched Lines: ") - .append( - LPVSCommentUtil.getMatchedLinesAsLink( - webhookConfig, file, LPVSVcs.GITHUB)) - .append("

"); - htmlBuilder - .append("

Snippet Match: ") - .append(file.getSnippetMatch()) - .append("

"); - htmlBuilder.append("
"); - } - } - - if (conflicts != null && conflicts.size() > 0) { - htmlBuilder.append("

Detected license conflicts:

"); - htmlBuilder.append("
    "); - for (LPVSLicenseService.Conflict conflict : conflicts) { - htmlBuilder - .append("
  • ") - .append(conflict.l1) - .append(" and ") - .append(conflict.l2) - .append("
  • "); - } - htmlBuilder.append("
"); - if (webhookConfig.getHubLink() != null) { - htmlBuilder.append("

").append(webhookConfig.getHubLink()).append("

"); - } - } - - htmlBuilder.append(""); - - return htmlBuilder.toString(); - } - - /** - * Saves HTML report to given location. - * - * @param htmlContent The string, containing report in HTML format. - * @param filePath The path to expected html report file. - */ - public static void saveHTMLToFile(String htmlContent, String filePath) { - File file = new File(filePath); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) { - writer.write(htmlContent); - log.info("LPVS report saved to: " + filePath); - } catch (IOException ex) { - log.error("error during saving HTML report: " + ex.getMessage()); - } - } } diff --git a/src/main/resources/templates/report_single_scan.html b/src/main/resources/templates/report_single_scan.html new file mode 100644 index 00000000..64860931 --- /dev/null +++ b/src/main/resources/templates/report_single_scan.html @@ -0,0 +1,141 @@ + + + + + + + + title + + + + + +
+

Report - License Pre-Validation Service (LPVS)


+
+
+ Scan date: scanDate
+ Source code location: codeLocation
+ Used scanner: usedScanner
+ Version of LPVS: lpvsVersion
+
+
+

Detected Licenses

+
+ Potential license problem(s) detected: licenseDetected +
    +
  • Prohibited license(s): prohibitedLicenses
  • +
  • Restricted license(s): restrictedLicenses
  • +
  • Unreviewed license(s): unreviewedLicenses
  • +
+
+
+ No license problems detected. +
+ +
+ +
+

+
+
+
+

Detected License Conflicts

+
+ Potential license conflict(s) detected: licenseConflicts +
+
+ No license conflicts detected. +
+
+ +
+

+
+
+ + + + diff --git a/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java b/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java new file mode 100644 index 00000000..a53ac20c --- /dev/null +++ b/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2024, Samsung Electronics Co., Ltd. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +package com.lpvs.entity.report; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.service.LPVSLicenseService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.thymeleaf.TemplateEngine; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.*; + +import static com.lpvs.entity.report.LPVSReportBuilder.saveHTMLToFile; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ImportAutoConfiguration(ThymeleafAutoConfiguration.class) +@ContextConfiguration(classes = {LPVSReportBuilder.class}) +@Slf4j +public class LPVSReportBuilderTest { + + @Autowired private TemplateEngine templateEngine; + + LPVSFile fileLicPermitted, + fileLicProhibitedRestricted, + fileLicUnreviewed_1, + fileLicUnreviewed_2, + fileLicUnreviewed_3, + fileLicIncorrect; + LPVSLicense licPermitted, + licProhibited, + licRestricted, + licUnreviewed, + licUnreviewed_2, + licIncorrect; + LPVSLicenseService.Conflict conflict1, conflict2; + LPVSReportBuilder reportBuilder; + + @TempDir Path tempDir; + + @BeforeEach + public void setUp() throws Exception { + licPermitted = + new LPVSLicense() { + { + setLicenseName("MIT License"); + setAccess("PERMITTED"); + setSpdxId("MIT"); + } + }; + licProhibited = + new LPVSLicense() { + { + setLicenseName("GNU General Public License v3.0 only"); + setAccess("PROHIBITED"); + setSpdxId("GPL-3.0-only"); + } + }; + licRestricted = + new LPVSLicense() { + { + setLicenseName("GNU Lesser General Public License v2.0 or later"); + setAccess("RESTRICTED"); + setSpdxId("LGPL-2.0-or-later"); + } + }; + licUnreviewed = + new LPVSLicense() { + { + setLicenseName("Apache License 2.0"); + setAccess("UNREVIEWED"); + setSpdxId("Apache-2.0"); + } + }; + + licUnreviewed_2 = + new LPVSLicense() { + { + setLicenseName("BSD 3-Clause \"New\" or \"Revised\" License"); + setAccess("UNREVIEWED"); + setSpdxId("BSD-3-Clause"); + } + }; + + licIncorrect = + new LPVSLicense() { + { + setLicenseName("Some incorrect license"); + setAccess("INCORRECT"); + setSpdxId("Some-license"); + } + }; + + fileLicPermitted = new LPVSFile(); + fileLicPermitted.setLicenses( + new HashSet<>() { + { + add(licPermitted); + } + }); + fileLicPermitted.setFilePath("local_file_path_1"); + fileLicPermitted.setComponentFilePath("component_file_path_1"); + fileLicPermitted.setComponentFileUrl("http://component_name_1/file_url"); + fileLicPermitted.setComponentName("component_name_1"); + fileLicPermitted.setComponentUrl("http://component_name_1/url"); + fileLicPermitted.setComponentVersion("v1.0.0"); + fileLicPermitted.setComponentVendor("component_vendor_1"); + fileLicPermitted.setSnippetMatch("80%"); + fileLicPermitted.setMatchedLines("5-17"); + + fileLicProhibitedRestricted = new LPVSFile(); + fileLicProhibitedRestricted.setLicenses( + new HashSet<>() { + { + add(licProhibited); + add(licRestricted); + } + }); + fileLicProhibitedRestricted.setFilePath("local_file_path_2"); + fileLicProhibitedRestricted.setComponentFilePath("component_file_path_2"); + fileLicProhibitedRestricted.setComponentName("component_name_2"); + fileLicProhibitedRestricted.setComponentUrl("http://component_name_2/url"); + fileLicProhibitedRestricted.setComponentVersion("v2.0.0"); + fileLicProhibitedRestricted.setComponentVendor("component_vendor_2"); + fileLicProhibitedRestricted.setSnippetMatch("100%"); + fileLicProhibitedRestricted.setMatchedLines("all"); + + fileLicUnreviewed_1 = new LPVSFile(); + fileLicUnreviewed_1.setLicenses( + new HashSet<>() { + { + add(licUnreviewed_2); + } + }); + fileLicUnreviewed_1.setFilePath("local_file_path_3"); + fileLicUnreviewed_1.setComponentFilePath("component_file_path_3"); + fileLicUnreviewed_1.setComponentFileUrl("http://component_name_3/file_url"); + fileLicUnreviewed_1.setComponentName("component_name_3"); + fileLicUnreviewed_1.setComponentUrl("http://component_name_3/url"); + fileLicUnreviewed_1.setComponentVersion("v3.0.0"); + fileLicUnreviewed_1.setComponentVendor("component_vendor_3"); + fileLicUnreviewed_1.setSnippetMatch("20%"); + fileLicUnreviewed_1.setMatchedLines("1-10"); + + fileLicUnreviewed_2 = new LPVSFile(); + fileLicUnreviewed_2.setLicenses( + new HashSet<>() { + { + add(licUnreviewed); + } + }); + fileLicUnreviewed_2.setFilePath("local_file_path_4"); + fileLicUnreviewed_2.setComponentFilePath("component_file_path_4"); + fileLicUnreviewed_2.setComponentName("component_name_4"); + fileLicUnreviewed_2.setComponentUrl("http://component_name_4/url"); + fileLicUnreviewed_2.setComponentVersion("v4.0.0"); + fileLicUnreviewed_2.setComponentVendor("component_vendor_4"); + fileLicUnreviewed_2.setSnippetMatch("50%"); + fileLicUnreviewed_2.setMatchedLines("5-10"); + + fileLicUnreviewed_3 = new LPVSFile(); + fileLicUnreviewed_3.setLicenses( + new HashSet<>() { + { + add(licUnreviewed); + } + }); + fileLicUnreviewed_3.setFilePath("local_file_path_4"); + fileLicUnreviewed_3.setComponentFilePath("component_file_path_4"); + fileLicUnreviewed_3.setComponentName("component_name_4"); + fileLicUnreviewed_3.setComponentUrl("http://component_name_4/url"); + fileLicUnreviewed_3.setComponentVersion("v4.0.0"); + fileLicUnreviewed_3.setComponentVendor("component_vendor_4"); + fileLicUnreviewed_3.setSnippetMatch("40%"); + fileLicUnreviewed_3.setMatchedLines("1-10"); + + fileLicIncorrect = new LPVSFile(); + fileLicIncorrect.setLicenses( + new HashSet<>() { + { + add(licIncorrect); + } + }); + fileLicIncorrect.setFilePath("local_file_path_4"); + fileLicIncorrect.setComponentFilePath("component_file_path_4"); + fileLicIncorrect.setComponentName("component_name_4"); + fileLicIncorrect.setComponentUrl("http://component_name_4/url"); + fileLicIncorrect.setComponentVersion("v4.0.0"); + fileLicIncorrect.setComponentVendor("component_vendor_4"); + fileLicIncorrect.setSnippetMatch("50%"); + fileLicIncorrect.setMatchedLines("5-10"); + + conflict1 = new LPVSLicenseService.Conflict<>("GPL-3.0-only", "Apache-2.0"); + conflict2 = new LPVSLicenseService.Conflict<>("LGPL-2.0-or-later", "MIT"); + + reportBuilder = new LPVSReportBuilder(templateEngine); + } + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + @Test + public void testGenerateHtmlReportSingleScan_Empty() { + String actual = + reportBuilder.generateHtmlReportSingleScan( + "some/path", new ArrayList<>(), null, null, null); + assertThat(actual).contains(sdf.format(new Date())); // check title and scanDate + assertThat(actual).contains("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithLicensesAndConflicts() { + List scanResults = + List.of( + fileLicPermitted, + fileLicProhibitedRestricted, + fileLicUnreviewed_1, + fileLicUnreviewed_2); + List> conflicts = List.of(conflict1, conflict2); + String actual = + reportBuilder.generateHtmlReportSingleScan( + "some/path", scanResults, conflicts, null, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).doesNotContain("No license problems detected"); + assertThat(actual).doesNotContain("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithLicenses() { + List scanResults = List.of(fileLicUnreviewed_1, fileLicUnreviewed_2); + String actual = + reportBuilder.generateHtmlReportSingleScan( + "some/path", scanResults, null, null, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).doesNotContain("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithPermittedLicenses() { + List scanResults = List.of(fileLicPermitted); + String actual = + reportBuilder.generateHtmlReportSingleScan( + "some/path", scanResults, null, null, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).contains("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithSimilarLicenses() { + List scanResults = + List.of(fileLicUnreviewed_1, fileLicUnreviewed_2, fileLicUnreviewed_3); + String actual = + reportBuilder.generateHtmlReportSingleScan( + "some/path", scanResults, new ArrayList<>(), null, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).doesNotContain("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + void testSaveHTMLToFile() throws IOException { + String htmlContent = "

Test HTML

"; + String filePath = "test-output.html"; + + saveHTMLToFile(htmlContent, filePath); + + Path path = Paths.get(filePath); + assertTrue(Files.exists(path)); + String fileContent = Files.readString(path); + assertEquals(htmlContent, fileContent); + + // Clean up: delete the created file + Files.deleteIfExists(path); + } + + @Test + void saveHTMLToFile_CatchBlock_N() { + String htmlContent = ""; + Path invalidPath = tempDir.resolve("invalid/path/with/special/characters"); + saveHTMLToFile(htmlContent, invalidPath.toString()); + assertFalse(Files.exists(invalidPath)); + } +} diff --git a/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java b/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java index 6fd0c860..c9f72e26 100644 --- a/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java +++ b/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java @@ -9,6 +9,7 @@ import com.lpvs.entity.LPVSFile; import com.lpvs.entity.LPVSLicense; import com.lpvs.entity.LPVSQueue; +import com.lpvs.entity.report.LPVSReportBuilder; import com.lpvs.service.LPVSGitHubConnectionService; import com.lpvs.service.LPVSGitHubService; import com.lpvs.service.LPVSLicenseService; @@ -60,6 +61,7 @@ class TestRunScan__Scanoss { LPVSLicenseService licenseservice_mock = mock(LPVSLicenseService.class); LPVSGitHubService githubservice_mock = mock(LPVSGitHubService.class); LPVSScanServiceFactory scanServiceFactory_mock = mock(LPVSScanServiceFactory.class); + LPVSReportBuilder reportBuilder_mock = mock(LPVSReportBuilder.class); GitHub mockGitHub = mock(GitHub.class); GHCommitPointer mockCommitPointer = mock(GHCommitPointer.class); GHRepository mockRepository = mock(GHRepository.class); @@ -82,7 +84,8 @@ void setUp() throws IOException { github_mock, licenseservice_mock, githubservice_mock, - scanServiceFactory_mock); + scanServiceFactory_mock, + reportBuilder_mock); webhookConfig = new LPVSQueue(); webhookConfig.setId(1L); @@ -149,7 +152,13 @@ void testRunOneScanPullRequestWithNullTrigger() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "trigger", null); @@ -166,7 +175,13 @@ void testRunOneScanLocalFileWithNullTrigger() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "localPath", null); @@ -183,7 +198,13 @@ void testRunOneScanBothPullRequestAndLocalFile() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); setPrivateField(lpvsDetectService, "trigger", ""); @@ -198,7 +219,13 @@ void testRunOneScanBothPullRequestAndLocalFile2() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); setPrivateField(lpvsDetectService, "trigger", "some-pull-request"); @@ -214,7 +241,13 @@ void testRunOneScan_PullRequest_Default() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "trigger", "fake-trigger-value"); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); @@ -235,7 +268,13 @@ void testRunOneScan_PullRequest_Branch2() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); // Mock the necessary GitHub objects for LPVSQueue when(mockGitHub.getRepository(any())).thenReturn(mockRepository); @@ -284,7 +323,13 @@ void testRunOneScan_Branch3() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(detectService, "trigger", "github/owner/repo/branch/123"); setPrivateField(detectService, "htmlReport", "build"); @@ -302,6 +347,9 @@ void testRunOneScan_Branch3() .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -331,7 +379,13 @@ void testRunOneScan_LocalFiles_WithConsoleReport() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -381,7 +435,13 @@ void testRunOneScan_LocalFiles_WithHtmlReport() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -407,6 +467,9 @@ void testRunOneScan_LocalFiles_WithHtmlReport() .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -437,7 +500,13 @@ void testRunOneScan_LocalFiles_NoFile() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -511,6 +580,9 @@ void testRunOneScan_TriggerNotNull() throws Exception { .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -555,6 +627,9 @@ void testRunOneScan_TriggerNotNull_Branch2() throws Exception { .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -598,6 +673,9 @@ void testRunOneScan_TriggerNotNull_Branch3() throws Exception { when(mockCommitPointer.getRepository()).thenReturn(mockHeadRepository2); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -639,6 +717,9 @@ void testRunOneScan_TriggerNotNull_NoDirectory() throws Exception { when(mockCommitPointer.getRepository()).thenReturn(mockHeadRepository2); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan( + anyString(), anyList(), anyList(), any(), any())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -667,11 +748,8 @@ void testCommentBuilder_ConflictFilePresent() { List scanResults = new ArrayList<>(); String commentGitHub = LPVSCommentUtil.reportCommentBuilder(webhookConfig, scanResults, expected); - String commentHTML = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, expected); assertNotNull(commentGitHub); - assertNotNull(commentHTML); } @Test @@ -681,11 +759,8 @@ void testCommentBuilder_NoConflictNoLicense() { List scanResults = new ArrayList<>(); String commentGitHub = LPVSCommentUtil.reportCommentBuilder(webhookConfig, scanResults, expected); - String commentHTML = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, expected); assertEquals(commentGitHub, ""); - assertEquals(commentHTML, ""); } @Test @@ -737,6 +812,7 @@ class TestRunScan__ScanossException { LPVSLicenseService licenseservice_mock = mock(LPVSLicenseService.class); LPVSGitHubService githubservice_mock = mock(LPVSGitHubService.class); LPVSScanServiceFactory scanServiceFactory_mock = mock(LPVSScanServiceFactory.class); + LPVSReportBuilder reportBuilder_mock = mock(LPVSReportBuilder.class); LPVSQueue webhookConfig; final String test_path = "test_path"; @@ -751,7 +827,8 @@ void setUp() { github_mock, licenseservice_mock, githubservice_mock, - scanServiceFactory_mock); + scanServiceFactory_mock, + reportBuilder_mock); webhookConfig = new LPVSQueue(); webhookConfig.setId(1L); @@ -801,7 +878,7 @@ class TestRunScan__NotScanoss { void setUp() { detectService = new LPVSDetectService( - "not_scanoss", false, null, null, null, scanServiceFactory_mock); + "not_scanoss", false, null, null, null, scanServiceFactory_mock, null); } @Test diff --git a/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java b/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java index c94a99d0..0c7a2490 100644 --- a/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java +++ b/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java @@ -20,11 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -152,48 +148,4 @@ void testReportCommentBuilder_HubLink() { assertNotNull(comment); } - - @Test - void testBuildHTMLComment() { - LPVSQueue webhookConfig = new LPVSQueue(); - List scanResults = new ArrayList<>(); - scanResults.add(createSampleFile("testPath1", "test1")); - List> conflicts = new ArrayList<>(); - - String htmlComment = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, conflicts); - - assertNotNull(htmlComment); - } - - @Test - void testBuildHTMLComment_HubLink() { - LPVSQueue webhookConfig = new LPVSQueue(); - List scanResults = new ArrayList<>(); - scanResults.add(createSampleFile("testPath1", "test1")); - LPVSLicenseService.Conflict conflict_1 = - new LPVSLicenseService.Conflict<>("MIT", "Apache-2.0"); - List> conflicts = - List.of(conflict_1, conflict_1); - webhookConfig.setHubLink("some_link"); - String htmlComment = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, conflicts); - - assertNotNull(htmlComment); - } - - @Test - void testSaveHTMLToFile() throws IOException { - String htmlContent = "

Test HTML

"; - String filePath = "test-output.html"; - - LPVSCommentUtil.saveHTMLToFile(htmlContent, filePath); - - assertTrue(Files.exists(Paths.get(filePath))); - String fileContent = Files.readString(Paths.get(filePath)); - assertEquals(htmlContent, fileContent); - - // Clean up: delete the created file - Files.deleteIfExists(Paths.get(filePath)); - } }