From 174124018dae157a01efea114202b31974f51447 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:32:04 +0100 Subject: [PATCH] fix: make sure npm is enabled only if package-lock.json is enabled Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> --- README.md | 2 +- src/main/java/com/redhat/exhort/Api.java | 5 +- .../com/redhat/exhort/impl/ExhortApi.java | 6 +- .../com/redhat/exhort/tools/Ecosystem.java | 13 ++- .../java/com/redhat/exhort/ExhortTest.java | 78 ++++++++++++++++- .../com/redhat/exhort/impl/ExhortApiIT.java | 85 +++++++++++++------ .../redhat/exhort/impl/Exhort_Api_Test.java | 17 ++-- 7 files changed, 163 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 2e4ebb07..afa398b0 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ public class ExhortExample { // get a AnalysisReport future holding a deserialized Component Analysis report var manifestContent = Files.readAllBytes(Paths.get("/path/to/pom.xml")); - CompletableFuture componentReport = exhortApi.componentAnalysis("pom.xml", manifestContent); + CompletableFuture componentReport = exhortApi.componentAnalysis("pom.xml", manifestContent, Paths.get("/path/to/pom.xml")); } } ``` diff --git a/src/main/java/com/redhat/exhort/Api.java b/src/main/java/com/redhat/exhort/Api.java index d728cffa..309e47a6 100644 --- a/src/main/java/com/redhat/exhort/Api.java +++ b/src/main/java/com/redhat/exhort/Api.java @@ -18,6 +18,7 @@ import com.redhat.exhort.api.AnalysisReport; import com.redhat.exhort.image.ImageRef; import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -104,8 +105,8 @@ public int hashCode() { * @return the deserialized Json report as an AnalysisReport wrapped in a CompletableFuture * @throws IOException when failed to load the manifest content */ - CompletableFuture componentAnalysis(String manifestType, byte[] manifestContent) - throws IOException; + CompletableFuture componentAnalysis( + String manifestType, byte[] manifestContent, Path manifestPath) throws IOException; CompletableFuture componentAnalysis(String manifestFile) throws IOException; diff --git a/src/main/java/com/redhat/exhort/impl/ExhortApi.java b/src/main/java/com/redhat/exhort/impl/ExhortApi.java index c60448c9..0618e674 100644 --- a/src/main/java/com/redhat/exhort/impl/ExhortApi.java +++ b/src/main/java/com/redhat/exhort/impl/ExhortApi.java @@ -39,6 +39,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -420,9 +421,10 @@ public static boolean debugLoggingIsNeeded() { @Override public CompletableFuture componentAnalysis( - final String manifestType, final byte[] manifestContent) throws IOException { + final String manifestType, final byte[] manifestContent, final Path manifestPath) + throws IOException { String exClientTraceId = commonHookBeginning(false); - var provider = Ecosystem.getProvider(manifestType); + var provider = Ecosystem.getProvider(manifestType, manifestPath); var uri = URI.create(String.format("%s/api/v4/analysis", this.endpoint)); var content = provider.provideComponent(manifestContent); commonHookAfterProviderCreatedSbomAndBeforeExhort(); diff --git a/src/main/java/com/redhat/exhort/tools/Ecosystem.java b/src/main/java/com/redhat/exhort/tools/Ecosystem.java index f84f5c60..554ac12a 100644 --- a/src/main/java/com/redhat/exhort/tools/Ecosystem.java +++ b/src/main/java/com/redhat/exhort/tools/Ecosystem.java @@ -21,6 +21,7 @@ import com.redhat.exhort.providers.JavaMavenProvider; import com.redhat.exhort.providers.JavaScriptNpmProvider; import com.redhat.exhort.providers.PythonPipProvider; +import java.nio.file.Files; import java.nio.file.Path; /** Utility class used for instantiating providers. * */ @@ -55,7 +56,7 @@ private Ecosystem() { * @return a Manifest record */ public static Provider getProvider(final Path manifestPath) { - return Ecosystem.getProvider(manifestPath.getFileName().toString()); + return Ecosystem.getProvider(manifestPath.getFileName().toString(), manifestPath); } /** @@ -64,12 +65,18 @@ public static Provider getProvider(final Path manifestPath) { * @param manifestType the type (filename + type) of the manifest * @return a Manifest record */ - public static Provider getProvider(final String manifestType) { + public static Provider getProvider(final String manifestType, final Path manifestPath) { switch (manifestType) { case "pom.xml": return new JavaMavenProvider(); case "package.json": - return new JavaScriptNpmProvider(); + Path lockFile = manifestPath.getParent().resolve("package-lock.json"); + if (Files.exists(lockFile)) { + return new JavaScriptNpmProvider(); + } else { + throw new IllegalStateException( + String.format("NPM Lock file could not be found for %s", manifestType)); + } case "go.mod": return new GoModulesProvider(); case "requirements.txt": diff --git a/src/test/java/com/redhat/exhort/ExhortTest.java b/src/test/java/com/redhat/exhort/ExhortTest.java index dca86b7c..2f18e494 100644 --- a/src/test/java/com/redhat/exhort/ExhortTest.java +++ b/src/test/java/com/redhat/exhort/ExhortTest.java @@ -15,11 +15,17 @@ */ package com.redhat.exhort; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import org.apache.commons.io.FileUtils; public class ExhortTest { @@ -36,8 +42,8 @@ protected String getStringFromFile(String... list) { return new String(bytes); } - public static InputStream getResourceAsStreamDecision( - Class theClass, String[] list) throws IOException { + public static InputStream getResourceAsStreamDecision(Class theClass, String[] list) + throws IOException { InputStream resourceAsStreamFromModule = theClass.getModule().getResourceAsStream(String.join("/", list)); if (Objects.isNull(resourceAsStreamFromModule)) { @@ -69,6 +75,74 @@ protected String getFileFromResource(String fileName, String... pathList) { return tmpFile.toString(); } + public static class TempDirFromResources { + private final Path tmpDir; + + public TempDirFromResources() throws IOException { + tmpDir = Files.createTempDirectory("exhort_test_"); + } + + public class AddPath { + private final String fileName; + + public AddPath(String fileName) { + this.fileName = fileName; + } + + public TempDirFromResources fromResources(String... pathList) { + Path tmpFile; + try { + tmpFile = Files.createFile(tmpDir.resolve(this.fileName)); + try (var is = getResourceAsStreamDecision(super.getClass(), pathList)) { + if (Objects.nonNull(is)) { + Files.write(tmpFile, is.readAllBytes()); + } else { + InputStream resourceIs = + getClass().getClassLoader().getResourceAsStream(String.join("/", pathList)); + Files.write(tmpFile, resourceIs.readAllBytes()); + resourceIs.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return TempDirFromResources.this; + } + } + + public AddPath addFile(String fileName) { + return new AddPath(fileName); + } + + public TempDirFromResources addDirectory(String dirName, String... pathList) { + File target = this.tmpDir.resolve(dirName).toFile(); + String join = String.join("/", pathList); + URL resource = this.getClass().getClassLoader().getResource(join); + File source = new File(Objects.requireNonNull(resource).getFile()); + try { + FileUtils.copyDirectory(source, target); + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + public TempDirFromResources addFile( + Optional fileName, Supplier> pathList) { + if (fileName.isEmpty()) { + return this; + } + + return new AddPath(fileName.get()).fromResources(pathList.get().toArray(new String[0])); + } + + public Path getTempDir() { + return this.tmpDir; + } + } + protected String getFileFromString(String fileName, String content) { Path tmpFile; try { diff --git a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java index 79346af5..b2d863f0 100644 --- a/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java +++ b/src/test/java/com/redhat/exhort/impl/ExhortApiIT.java @@ -39,6 +39,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -69,7 +71,8 @@ class ExhortApiIT extends ExhortTest { private static Api api; - private static Map ecoSystemsManifestNames; + private static Map>> + ecoSystemsManifestNames; private MockedStatic mockedOperations; @@ -81,17 +84,17 @@ static void beforeAll() { ecoSystemsManifestNames = Map.of( "golang", - "go.mod", + new SimpleEntry<>("go.mod", Optional.empty()), "maven", - "pom.xml", + new SimpleEntry<>("pom.xml", Optional.empty()), "npm", - "package.json", + new SimpleEntry<>("package.json", Optional.of("package-lock.json")), "pypi", - "requirements.txt", + new SimpleEntry<>("requirements.txt", Optional.empty()), "gradle-groovy", - "build.gradle", + new SimpleEntry<>("build.gradle", Optional.empty()), "gradle-kotlin", - "build.gradle.kts"); + new SimpleEntry<>("build.gradle.kts", Optional.empty())); } @Tag("IntegrationTest") @@ -104,19 +107,28 @@ static void afterAll() { private static List scenarios() { return ecoSystemsManifestNames.entrySet().stream() - .map(e -> Arguments.of(e.getKey(), e.getValue())) + .map(e -> Arguments.of(e.getKey(), e.getValue().getKey(), e.getValue().getValue())) .collect(Collectors.toList()); } @Tag("IntegrationTest") @ParameterizedTest(name = "StackAnalysis for: {0} with manifest: {1}") @MethodSource("scenarios") - void Integration_Test_End_To_End_Stack_Analysis(String useCase, String manifestFileName) + void Integration_Test_End_To_End_Stack_Analysis( + String useCase, String manifestFileName, Optional lockFilename) throws IOException, ExecutionException, InterruptedException { - var provider = Ecosystem.getProvider(manifestFileName); + Path manifestDirPath = + new TempDirFromResources() + .addFile(manifestFileName) + .fromResources("tst_manifests", "it", useCase, manifestFileName) + .addFile( + lockFilename, + () -> Arrays.asList("tst_manifests", "it", useCase, lockFilename.get())) + .getTempDir(); + Path manifestFileNamePath = manifestDirPath.resolve(manifestFileName); + var provider = Ecosystem.getProvider(manifestFileName, manifestFileNamePath); var packageManager = provider.ecosystem; - String pathToManifest = - getFileFromResource(manifestFileName, "tst_manifests", "it", useCase, manifestFileName); + String pathToManifest = manifestFileNamePath.toString(); preparePythonEnvironment(packageManager); // Github action runner with all maven and java versions seems to enter infinite loop in // integration tests of @@ -137,12 +149,21 @@ private void releaseStaticMock(Ecosystem.Type packageManager) { @Tag("IntegrationTest") @ParameterizedTest(name = "StackAnalysis Mixed for: {0} with manifest: {1}") @MethodSource("scenarios") - void Integration_Test_End_To_End_Stack_Analysis_Mixed(String useCase, String manifestFileName) + void Integration_Test_End_To_End_Stack_Analysis_Mixed( + String useCase, String manifestFileName, Optional lockFilename) throws IOException, ExecutionException, InterruptedException { - var provider = Ecosystem.getProvider(manifestFileName); + Path manifestDirPath = + new TempDirFromResources() + .addFile(manifestFileName) + .fromResources("tst_manifests", "it", useCase, manifestFileName) + .addFile( + lockFilename, + () -> Arrays.asList("tst_manifests", "it", useCase, lockFilename.get())) + .getTempDir(); + Path manifestFileNamePath = manifestDirPath.resolve(manifestFileName); + String pathToManifest = manifestFileNamePath.toString(); + var provider = Ecosystem.getProvider(manifestFileName, Path.of(pathToManifest)); var packageManager = provider.ecosystem; - String pathToManifest = - getFileFromResource(manifestFileName, "tst_manifests", "it", useCase, manifestFileName); preparePythonEnvironment(packageManager); // Github action runner with all maven and java versions seems to enter infinite loop in // integration tests of @@ -159,12 +180,21 @@ void Integration_Test_End_To_End_Stack_Analysis_Mixed(String useCase, String man @Tag("IntegrationTest") @ParameterizedTest(name = "StackAnalysis HTML for: {0} with manifest: {1}") @MethodSource("scenarios") - void Integration_Test_End_To_End_Stack_Analysis_Html(String useCase, String manifestFileName) + void Integration_Test_End_To_End_Stack_Analysis_Html( + String useCase, String manifestFileName, Optional lockFilename) throws IOException, ExecutionException, InterruptedException { - var provider = Ecosystem.getProvider(manifestFileName); + Path manifestDirPath = + new TempDirFromResources() + .addFile(manifestFileName) + .fromResources("tst_manifests", "it", useCase, manifestFileName) + .addFile( + lockFilename, + () -> Arrays.asList("tst_manifests", "it", useCase, lockFilename.get())) + .getTempDir(); + Path manifestFileNamePath = manifestDirPath.resolve(manifestFileName); + String pathToManifest = manifestFileNamePath.toString(); + var provider = Ecosystem.getProvider(manifestFileName, Path.of(pathToManifest)); var packageManager = provider.ecosystem; - String pathToManifest = - getFileFromResource(manifestFileName, "tst_manifests", "it", useCase, manifestFileName); preparePythonEnvironment(packageManager); // Github action runner with all maven and java versions seems to enter infinite loop in // integration tests of @@ -183,13 +213,18 @@ void Integration_Test_End_To_End_Stack_Analysis_Html(String useCase, String mani names = {"GOLANG", "MAVEN", "NPM", "PYTHON"}) void Integration_Test_End_To_End_Component_Analysis(Ecosystem.Type packageManager) throws IOException, ExecutionException, InterruptedException { - String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()); - byte[] manifestContent = - getStringFromFile("tst_manifests", "it", packageManager.getType(), manifestFileName) - .getBytes(); + String manifestFileName = ecoSystemsManifestNames.get(packageManager.getType()).getKey(); + + Path tempDir = + new TempDirFromResources() + .addDirectory(packageManager.getType(), "tst_manifests", "it", packageManager.getType()) + .getTempDir(); + Path manifestPath = tempDir.resolve(packageManager.getType()).resolve(manifestFileName); + byte[] manifestContent = Files.readAllBytes(manifestPath); + preparePythonEnvironment(packageManager); AnalysisReport analysisReportResult = - api.componentAnalysis(manifestFileName, manifestContent).get(); + api.componentAnalysis(manifestFileName, manifestContent, manifestPath).get(); handleJsonResponse(analysisReportResult, false); } diff --git a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java index 35bd45e6..d812318e 100644 --- a/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java +++ b/src/test/java/com/redhat/exhort/impl/Exhort_Api_Test.java @@ -53,6 +53,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -227,12 +228,12 @@ void stackAnalysis_with_pom_xml_should_return_json_object_from_the_backend() void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() throws IOException, ExecutionException, InterruptedException { // load pom.xml - byte[] targetPom; - try (var is = - getResourceAsStreamDecision( - this.getClass(), new String[] {"tst_manifests", "maven", "empty", "pom.xml"})) { - targetPom = is.readAllBytes(); - } + Path tempDir = + new TempDirFromResources() + .addFile("pom.xml") + .fromResources(new String[] {"tst_manifests", "maven", "empty", "pom.xml"}) + .getTempDir(); + byte[] targetPom = Files.readAllBytes(tempDir.resolve("pom.xml")); // stub the mocked provider with a fake content object given(mockProvider.provideComponent(targetPom)) @@ -274,14 +275,14 @@ void componentAnalysis_with_pom_xml_should_return_json_object_from_the_backend() // mock static getProvider utility function try (var ecosystemTool = mockStatic(Ecosystem.class)) { // stub static getProvider utility function to return our mock provider - ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml")).thenReturn(mockProvider); + ecosystemTool.when(() -> Ecosystem.getProvider("pom.xml", tempDir)).thenReturn(mockProvider); // stub the http client to return our mocked response when request matches our arg matcher given(mockHttpClient.sendAsync(argThat(matchesRequest), any())) .willReturn(CompletableFuture.completedFuture(mockHttpResponse)); // when invoking the api for a json stack analysis report - var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom); + var responseAnalysis = exhortApiSut.componentAnalysis("pom.xml", targetPom, tempDir); // verify we got the correct analysis report then(responseAnalysis.get()).isEqualTo(expectedReport); }