diff --git a/pom.xml b/pom.xml index fa54048f..7737d5ae 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ 17 3.27.3 + 2.55 2.18.2 5.11.4 4.7.6 @@ -71,6 +72,11 @@ packageurl-java ${packageurl.version} + + com.google.dagger + dagger + ${dagger.version} + org.junit.jupiter @@ -238,6 +244,11 @@ picocli-codegen ${picocli.version} + + com.google.dagger + dagger-compiler + ${dagger.version} + -Aproject=${project.groupId}/${project.artifactId} diff --git a/src/main/java/it/mulders/mcs/App.java b/src/main/java/it/mulders/mcs/App.java index 2333a6db..513dc162 100644 --- a/src/main/java/it/mulders/mcs/App.java +++ b/src/main/java/it/mulders/mcs/App.java @@ -1,9 +1,7 @@ package it.mulders.mcs; -import it.mulders.mcs.cli.Cli; -import it.mulders.mcs.cli.CommandClassFactory; -import it.mulders.mcs.cli.SystemPropertyLoader; -import it.mulders.mcs.common.McsExecutionExceptionHandler; +import it.mulders.mcs.dagger.Application; +import it.mulders.mcs.dagger.DaggerApplication; import java.net.URI; import java.net.URISyntaxException; import picocli.CommandLine; @@ -15,20 +13,19 @@ public static void main(final String... args) { // Visible for testing static int doMain(final String... originalArgs) { - return doMain(new Cli(), new SystemPropertyLoader(), originalArgs); - } + final Application components = DaggerApplication.create(); - static int doMain(final Cli cli, final SystemPropertyLoader systemPropertyLoader, final String... originalArgs) { + var systemPropertyLoader = components.systemPropertyLoader(); System.setProperties(systemPropertyLoader.getProperties()); setUpProxy(); - var program = new CommandLine(cli, new CommandClassFactory(cli)) - .setExecutionExceptionHandler(new McsExecutionExceptionHandler()); - var args = isInvocationWithoutSearchCommand(program, originalArgs) + var commandLine = components.commandLine(); + + var args = isInvocationWithoutSearchCommand(commandLine, originalArgs) ? prependSearchCommandToArgs(originalArgs) : originalArgs; - return program.execute(args); + return commandLine.execute(args); } private static void setUpProxy() { diff --git a/src/main/java/it/mulders/mcs/cli/ClassSearchCommand.java b/src/main/java/it/mulders/mcs/cli/ClassSearchCommand.java new file mode 100644 index 00000000..4f20641b --- /dev/null +++ b/src/main/java/it/mulders/mcs/cli/ClassSearchCommand.java @@ -0,0 +1,59 @@ +package it.mulders.mcs.cli; + +import it.mulders.mcs.search.SearchCommandHandler; +import it.mulders.mcs.search.SearchQuery; +import jakarta.inject.Inject; +import java.util.concurrent.Callable; +import picocli.CommandLine; + +@CommandLine.Command( + name = "class-search", + description = "Search artifacts in Maven Central by class name", + usageHelpAutoWidth = true) +public class ClassSearchCommand implements Callable { + @CommandLine.Parameters( + arity = "1", + description = { + "The class name to search for.", + }) + private String query; + + @CommandLine.Option( + names = {"-f", "--full-name"}, + negatable = true, + arity = "0", + description = "Class name includes package") + private boolean fullName; + + @CommandLine.Option( + names = {"-l", "--limit"}, + description = "Show results", + paramLabel = "") + private Integer limit; + + private final SearchCommandHandler searchCommandHandler; + + @Inject + public ClassSearchCommand(final SearchCommandHandler searchCommandHandler) { + this.searchCommandHandler = searchCommandHandler; + } + + // Visible for testing + ClassSearchCommand(final SearchCommandHandler searchCommandHandler, String query, Integer limit, boolean fullName) { + this(searchCommandHandler); + this.fullName = fullName; + this.limit = limit; + this.query = query; + } + + @Override + public Integer call() { + System.out.printf("Searching for artifacts containing %s...%n", query); + var searchQuery = SearchQuery.classSearch(this.query) + .isFullyQualified(this.fullName) + .withLimit(limit) + .build(); + searchCommandHandler.search(searchQuery, "maven", false); + return 0; + } +} diff --git a/src/main/java/it/mulders/mcs/cli/Cli.java b/src/main/java/it/mulders/mcs/cli/Cli.java index 8ba46aff..101b309f 100644 --- a/src/main/java/it/mulders/mcs/cli/Cli.java +++ b/src/main/java/it/mulders/mcs/cli/Cli.java @@ -1,15 +1,11 @@ package it.mulders.mcs.cli; -import it.mulders.mcs.search.FormatType; -import it.mulders.mcs.search.SearchCommandHandler; -import it.mulders.mcs.search.SearchQuery; -import it.mulders.mcs.search.printer.CoordinatePrinter; -import java.util.concurrent.Callable; +import jakarta.inject.Inject; import picocli.CommandLine; @CommandLine.Command( name = "mcs", - subcommands = {Cli.SearchCommand.class, Cli.ClassSearchCommand.class}, + subcommands = {SearchCommand.class, ClassSearchCommand.class}, usageHelpAutoWidth = true, versionProvider = ClasspathVersionProvider.class) public class Cli { @@ -27,100 +23,6 @@ public class Cli { usageHelp = true) private boolean usageHelpRequested; - public SearchCommand createSearchCommand() { - return new SearchCommand(); - } - - public ClassSearchCommand createClassSearchCommand() { - return new ClassSearchCommand(); - } - - @CommandLine.Command( - name = "search", - description = "Search artifacts in Maven Central by coordinates", - usageHelpAutoWidth = true) - public class SearchCommand implements Callable { - @CommandLine.Parameters( - arity = "1..n", - description = { - "What to search for.", - "If the search term contains a colon ( : ), it is considered a literal groupId and artifactId", - "Otherwise, the search term is considered a wildcard search" - }) - private String[] query; - - @CommandLine.Option( - names = {"-l", "--limit"}, - description = "Show results", - paramLabel = "") - private Integer limit; - - @CommandLine.Option( - names = {"-f", "--format"}, - description = - """ - Show result in format - Supported types are: - maven, gradle, gradle-short, gradle-kotlin, sbt, ivy, grape, leiningen, buildr, jbang, gav - """, - paramLabel = "") - private String responseFormat; - - @CommandLine.Option( - names = {"-s", "--show-vulnerabilities"}, - description = "Show reported security vulnerabilities", - paramLabel = "") - private boolean showVulnerabilities; - - @Override - public Integer call() { - var combinedQuery = String.join(" ", query); - System.out.printf("Searching for %s...%n", combinedQuery); - var searchQuery = - SearchQuery.search(combinedQuery).withLimit(this.limit).build(); - - CoordinatePrinter coordinatePrinter = FormatType.providePrinter(responseFormat); - var searchCommandHandler = new SearchCommandHandler(coordinatePrinter, showVulnerabilities); - searchCommandHandler.search(searchQuery); - return 0; - } - } - - @CommandLine.Command( - name = "class-search", - description = "Search artifacts in Maven Central by class name", - usageHelpAutoWidth = true) - public class ClassSearchCommand implements Callable { - @CommandLine.Parameters( - arity = "1", - description = { - "The class name to search for.", - }) - private String query; - - @CommandLine.Option( - names = {"-f", "--full-name"}, - negatable = true, - arity = "0", - description = "Class name includes package") - private boolean fullName; - - @CommandLine.Option( - names = {"-l", "--limit"}, - description = "Show results", - paramLabel = "") - private Integer limit; - - @Override - public Integer call() { - System.out.printf("Searching for artifacts containing %s...%n", query); - var searchQuery = SearchQuery.classSearch(this.query) - .isFullyQualified(this.fullName) - .withLimit(limit) - .build(); - var searchCommandHandler = new SearchCommandHandler(); - searchCommandHandler.search(searchQuery); - return 0; - } - } + @Inject + public Cli() {} } diff --git a/src/main/java/it/mulders/mcs/cli/CommandClassFactory.java b/src/main/java/it/mulders/mcs/cli/CommandClassFactory.java deleted file mode 100644 index da91670f..00000000 --- a/src/main/java/it/mulders/mcs/cli/CommandClassFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package it.mulders.mcs.cli; - -import picocli.CommandLine; - -/** - * Implementation of the {@link CommandLine.IFactory} interface that can construct instances of the {@link Cli} nested - * command classes. Since these classes get their dependencies from their parent class, they cannot be static classes. - */ -public class CommandClassFactory implements CommandLine.IFactory { - private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory(); - private final Cli cli; - - public CommandClassFactory(final Cli cli) { - this.cli = cli; - } - - @Override - @SuppressWarnings("unchecked") - public K create(Class cls) throws Exception { - if (cls == Cli.SearchCommand.class) { - return (K) cli.createSearchCommand(); - } else if (cls == Cli.ClassSearchCommand.class) { - return (K) cli.createClassSearchCommand(); - } - - return defaultFactory.create(cls); - } -} diff --git a/src/main/java/it/mulders/mcs/cli/SearchCommand.java b/src/main/java/it/mulders/mcs/cli/SearchCommand.java new file mode 100644 index 00000000..7d1438c1 --- /dev/null +++ b/src/main/java/it/mulders/mcs/cli/SearchCommand.java @@ -0,0 +1,77 @@ +package it.mulders.mcs.cli; + +import it.mulders.mcs.search.SearchCommandHandler; +import it.mulders.mcs.search.SearchQuery; +import jakarta.inject.Inject; +import java.util.concurrent.Callable; +import picocli.CommandLine; + +@CommandLine.Command( + name = "search", + description = "Search artifacts in Maven Central by coordinates", + usageHelpAutoWidth = true) +public class SearchCommand implements Callable { + @CommandLine.Parameters( + arity = "1..n", + description = { + "What to search for.", + "If the search term contains a colon ( : ), it is considered a literal groupId and artifactId", + "Otherwise, the search term is considered a wildcard search" + }) + private String[] query; + + @CommandLine.Option( + names = {"-l", "--limit"}, + description = "Show results", + paramLabel = "") + private Integer limit; + + @CommandLine.Option( + names = {"-f", "--format"}, + description = + """ + Show result in format + Supported types are: + maven, gradle, gradle-short, gradle-kotlin, sbt, ivy, grape, leiningen, buildr, jbang, gav + """, + paramLabel = "") + private String responseFormat; + + @CommandLine.Option( + names = {"-s", "--show-vulnerabilities"}, + description = "Show reported security vulnerabilities", + paramLabel = "") + private boolean showVulnerabilities; + + private final SearchCommandHandler searchCommandHandler; + + @Inject + public SearchCommand(final SearchCommandHandler searchCommandHandler) { + this.searchCommandHandler = searchCommandHandler; + } + + // Visible for testing + SearchCommand( + SearchCommandHandler searchCommandHandler, + String[] query, + Integer limit, + String responseFormat, + boolean showVulnerabilities) { + this(searchCommandHandler); + this.limit = limit; + this.query = query; + this.responseFormat = responseFormat; + this.showVulnerabilities = showVulnerabilities; + } + + @Override + public Integer call() { + var combinedQuery = String.join(" ", query); + System.out.printf("Searching for %s...%n", combinedQuery); + var searchQuery = + SearchQuery.search(combinedQuery).withLimit(this.limit).build(); + + searchCommandHandler.search(searchQuery, responseFormat, showVulnerabilities); + return 0; + } +} diff --git a/src/main/java/it/mulders/mcs/cli/SystemPropertyLoader.java b/src/main/java/it/mulders/mcs/cli/SystemPropertyLoader.java index 82213038..0bae8eb4 100644 --- a/src/main/java/it/mulders/mcs/cli/SystemPropertyLoader.java +++ b/src/main/java/it/mulders/mcs/cli/SystemPropertyLoader.java @@ -1,5 +1,6 @@ package it.mulders.mcs.cli; +import jakarta.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -17,8 +18,8 @@ public class SystemPropertyLoader { private static final Path MCS_PROPERTIES_FILE = Paths.get(System.getProperty("user.home"), ".mcs", "mcs.config"); private final Properties properties = new Properties(); - ; + @Inject public SystemPropertyLoader() { this(MCS_PROPERTIES_FILE); } diff --git a/src/main/java/it/mulders/mcs/common/McsExecutionExceptionHandler.java b/src/main/java/it/mulders/mcs/common/McsExecutionExceptionHandler.java index 766928c5..bd603f4c 100644 --- a/src/main/java/it/mulders/mcs/common/McsExecutionExceptionHandler.java +++ b/src/main/java/it/mulders/mcs/common/McsExecutionExceptionHandler.java @@ -1,8 +1,12 @@ package it.mulders.mcs.common; +import jakarta.inject.Inject; import picocli.CommandLine; public class McsExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { + @Inject + public McsExecutionExceptionHandler() {} + @Override public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { var message = diff --git a/src/main/java/it/mulders/mcs/dagger/Application.java b/src/main/java/it/mulders/mcs/dagger/Application.java new file mode 100644 index 00000000..3476e965 --- /dev/null +++ b/src/main/java/it/mulders/mcs/dagger/Application.java @@ -0,0 +1,15 @@ +package it.mulders.mcs.dagger; + +import dagger.Component; +import it.mulders.mcs.cli.SearchCommand; +import it.mulders.mcs.cli.SystemPropertyLoader; +import picocli.CommandLine; + +@Component(modules = {CommandLineModule.class, OutputModule.class, SearchModule.class}) +public interface Application { + CommandLine commandLine(); + + SystemPropertyLoader systemPropertyLoader(); + + SearchCommand searchCommand(); +} diff --git a/src/main/java/it/mulders/mcs/dagger/CommandLineModule.java b/src/main/java/it/mulders/mcs/dagger/CommandLineModule.java new file mode 100644 index 00000000..f952f4a2 --- /dev/null +++ b/src/main/java/it/mulders/mcs/dagger/CommandLineModule.java @@ -0,0 +1,28 @@ +package it.mulders.mcs.dagger; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import it.mulders.mcs.cli.Cli; +import it.mulders.mcs.common.McsExecutionExceptionHandler; +import picocli.CommandLine; +import picocli.CommandLine.IExecutionExceptionHandler; + +@Module +public interface CommandLineModule { + // @Provides static Cli provideCli(SearchCommandHandler searchCommandHandler) { + // return new Cli(); + // } + + @Provides + static CommandLine provideCommandLine( + final Cli cli, final DaggerFactory factory, final CommandLine.IExecutionExceptionHandler exceptionHandler) { + return new CommandLine(cli, factory).setExecutionExceptionHandler(exceptionHandler); + } + + // @Binds Cli.SearchCommand bindSearchCommand(final Cli.SearchCommand command); + // @Binds Cli.ClassSearchCommand bindClassSearchCommand(final Cli.ClassSearchCommand command); + // @Binds SearchCommandHandler bindSearchCommandHandler(final SearchCommandHandler handler); + @Binds + IExecutionExceptionHandler bindExecutionExceptionHandler(final McsExecutionExceptionHandler handler); +} diff --git a/src/main/java/it/mulders/mcs/dagger/DaggerFactory.java b/src/main/java/it/mulders/mcs/dagger/DaggerFactory.java new file mode 100644 index 00000000..e73f2cd2 --- /dev/null +++ b/src/main/java/it/mulders/mcs/dagger/DaggerFactory.java @@ -0,0 +1,33 @@ +package it.mulders.mcs.dagger; + +import it.mulders.mcs.cli.ClassSearchCommand; +import it.mulders.mcs.cli.SearchCommand; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import picocli.CommandLine; + +public class DaggerFactory implements CommandLine.IFactory { + private final Provider classSearchCommandProvider; + private final Provider searchCommandProvider; + private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory(); + + @Inject + public DaggerFactory( + final Provider classSearchCommandProvider, + final Provider searchCommandProvider) { + this.classSearchCommandProvider = classSearchCommandProvider; + this.searchCommandProvider = searchCommandProvider; + } + + @Override + public K create(Class cls) throws Exception { + return switch (cls.getName()) { + case "it.mulders.mcs.cli.SearchCommand": + yield (K) this.searchCommandProvider.get(); + case "it.mulders.mcs.cli.ClassSearchCommand": + yield (K) this.classSearchCommandProvider.get(); + default: + yield defaultFactory.create(cls); + }; + } +} diff --git a/src/main/java/it/mulders/mcs/dagger/OutputModule.java b/src/main/java/it/mulders/mcs/dagger/OutputModule.java new file mode 100644 index 00000000..a0acfeb4 --- /dev/null +++ b/src/main/java/it/mulders/mcs/dagger/OutputModule.java @@ -0,0 +1,6 @@ +package it.mulders.mcs.dagger; + +import dagger.Module; + +@Module +public interface OutputModule {} diff --git a/src/main/java/it/mulders/mcs/dagger/SearchModule.java b/src/main/java/it/mulders/mcs/dagger/SearchModule.java new file mode 100644 index 00000000..8dda608e --- /dev/null +++ b/src/main/java/it/mulders/mcs/dagger/SearchModule.java @@ -0,0 +1,13 @@ +package it.mulders.mcs.dagger; + +import dagger.Module; +import dagger.Provides; +import java.net.http.HttpClient; + +@Module +public interface SearchModule { + @Provides + static HttpClient provideHttpClient() { + return HttpClient.newHttpClient(); + } +} diff --git a/src/main/java/it/mulders/mcs/search/SearchClient.java b/src/main/java/it/mulders/mcs/search/SearchClient.java index 6dc60f2d..ac821fd9 100644 --- a/src/main/java/it/mulders/mcs/search/SearchClient.java +++ b/src/main/java/it/mulders/mcs/search/SearchClient.java @@ -2,6 +2,7 @@ import it.mulders.mcs.common.Result; import it.mulders.mcs.common.SearchResponseBodyHandler; +import jakarta.inject.Inject; import java.io.IOException; import java.net.ConnectException; import java.net.URI; @@ -10,14 +11,16 @@ public class SearchClient { private final String hostname; - private final HttpClient client = HttpClient.newHttpClient(); + private final HttpClient client; - public SearchClient() { - this("https://search.maven.org"); + @Inject + public SearchClient(final HttpClient client) { + this(client, "https://search.maven.org"); } // Visible for testing - SearchClient(final String hostname) { + SearchClient(final HttpClient client, final String hostname) { + this.client = client; this.hostname = hostname; } diff --git a/src/main/java/it/mulders/mcs/search/SearchCommandHandler.java b/src/main/java/it/mulders/mcs/search/SearchCommandHandler.java index c5c84e88..72d58efa 100644 --- a/src/main/java/it/mulders/mcs/search/SearchCommandHandler.java +++ b/src/main/java/it/mulders/mcs/search/SearchCommandHandler.java @@ -5,47 +5,40 @@ import it.mulders.mcs.common.McsRuntimeException; import it.mulders.mcs.common.Result; import it.mulders.mcs.search.printer.DelegatingOutputPrinter; -import it.mulders.mcs.search.printer.OutputPrinter; +import it.mulders.mcs.search.printer.OutputFactory; import it.mulders.mcs.search.vulnerability.ComponentReportClient; import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; +import jakarta.inject.Inject; import java.util.stream.Stream; public class SearchCommandHandler { + private final OutputFactory outputFactory; private final SearchClient searchClient; private final ComponentReportClient reportClient; - private final OutputPrinter outputPrinter; - private final boolean showVulnerabilities; - public SearchCommandHandler() { - this(Constants.DEFAULT_PRINTER, false); - } - - public SearchCommandHandler(final OutputPrinter coordinateOutput, final boolean showVulnerabilities) { - this( - new DelegatingOutputPrinter(coordinateOutput, showVulnerabilities), - showVulnerabilities, - new SearchClient(), - new ComponentReportClient()); - } - - // Visible for testing - SearchCommandHandler( - final OutputPrinter outputPrinter, - final boolean showVulnerabilities, - final SearchClient searchClient, - final ComponentReportClient reportClient) { - this.searchClient = searchClient; - this.outputPrinter = outputPrinter; + @Inject + public SearchCommandHandler( + final ComponentReportClient reportClient, + final OutputFactory outputFactory, + final SearchClient searchClient) { + this.outputFactory = outputFactory; this.reportClient = reportClient; - this.showVulnerabilities = showVulnerabilities; + this.searchClient = searchClient; } - public void search(final SearchQuery query) { + public void search(final SearchQuery query, final String outputFormat, final boolean reportVulnerabilities) { performSearch(query) .map(response -> performAdditionalSearch(query, response)) - .ifPresentOrElse(response -> processResponse(query, response), failure -> { - throw new McsRuntimeException(failure); - }); + .ifPresentOrElse( + response -> { + if (reportVulnerabilities) { + processResponse(query, response); + } + printResponse(query, response, outputFormat, reportVulnerabilities); + }, + failure -> { + throw new McsRuntimeException(failure); + }); } private SearchResponse.Response performAdditionalSearch( @@ -82,26 +75,28 @@ private Result performSearch(final SearchQuery query) { } private void processResponse(final SearchQuery query, final SearchResponse.Response searchResponse) { - if (showVulnerabilities) { - reportClient - .search(searchResponse.docs()) - .ifPresentOrElse( - componentResponse -> processComponentReports( - componentResponse.componentReports(), searchResponse.docs()), - failure -> { - throw new McsRuntimeException(failure); - }); - } - printResponse(query, searchResponse); + reportClient + .search(searchResponse.docs()) + .ifPresentOrElse( + componentResponse -> + assignComponentReports(componentResponse.componentReports(), searchResponse.docs()), + failure -> { + throw new McsRuntimeException(failure); + }); } - private void processComponentReports( + private void assignComponentReports( final ComponentReport[] componentReports, final SearchResponse.Response.Doc[] docs) { Stream.of(componentReports) .forEach(componentReport -> reportClient.assignComponentReport(componentReport, docs)); } - private void printResponse(final SearchQuery query, final SearchResponse.Response response) { - outputPrinter.print(query, response, System.out); + private void printResponse( + final SearchQuery query, + final SearchResponse.Response response, + final String outputFormat, + final boolean showVulnerabilities) { + var printer = new DelegatingOutputPrinter(outputFactory.findOutputPrinter(outputFormat), showVulnerabilities); + printer.print(query, response, System.out); } } diff --git a/src/main/java/it/mulders/mcs/search/SearchResponse.java b/src/main/java/it/mulders/mcs/search/SearchResponse.java index 367b4a97..d9fe26ad 100644 --- a/src/main/java/it/mulders/mcs/search/SearchResponse.java +++ b/src/main/java/it/mulders/mcs/search/SearchResponse.java @@ -3,6 +3,11 @@ import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; public record SearchResponse(Object responseHeader, Response response) { + public SearchResponse(Response response) { + // Convenience for testing + this(null, response); + } + public record Response(int numFound, int start, Doc[] docs) { public record Doc( String id, diff --git a/src/main/java/it/mulders/mcs/search/printer/DelegatingOutputPrinter.java b/src/main/java/it/mulders/mcs/search/printer/DelegatingOutputPrinter.java index 6f810b17..039abf9d 100644 --- a/src/main/java/it/mulders/mcs/search/printer/DelegatingOutputPrinter.java +++ b/src/main/java/it/mulders/mcs/search/printer/DelegatingOutputPrinter.java @@ -12,10 +12,6 @@ public class DelegatingOutputPrinter implements OutputPrinter { private final OutputPrinter coordinateOutput; private final OutputPrinter tabularSearchOutput; - public DelegatingOutputPrinter(final OutputPrinter coordinateOutput) { - this(coordinateOutput, false); - } - public DelegatingOutputPrinter(final OutputPrinter coordinateOutput, final boolean showVulnerabilities) { this(new NoOutputPrinter(), coordinateOutput, new TabularOutputPrinter(showVulnerabilities)); } diff --git a/src/main/java/it/mulders/mcs/search/printer/OutputFactory.java b/src/main/java/it/mulders/mcs/search/printer/OutputFactory.java new file mode 100644 index 00000000..3db2a896 --- /dev/null +++ b/src/main/java/it/mulders/mcs/search/printer/OutputFactory.java @@ -0,0 +1,13 @@ +package it.mulders.mcs.search.printer; + +import it.mulders.mcs.search.FormatType; +import jakarta.inject.Inject; + +public class OutputFactory { + @Inject + public OutputFactory() {} + + public OutputPrinter findOutputPrinter(final String formatName) { + return FormatType.providePrinter(formatName); + } +} diff --git a/src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportClient.java b/src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportClient.java index 537d1e28..7032410c 100644 --- a/src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportClient.java +++ b/src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportClient.java @@ -5,6 +5,7 @@ import it.mulders.mcs.common.McsRuntimeException; import it.mulders.mcs.common.Result; import it.mulders.mcs.search.SearchResponse; +import jakarta.inject.Inject; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -22,14 +23,16 @@ public class ComponentReportClient { for the Sonatype OSS Index. See https://ossindex.sonatype.org for details on how this may impact your usage."""; private final String hostname; - private final HttpClient client = HttpClient.newHttpClient(); + private final HttpClient client; - public ComponentReportClient() { - this("https://ossindex.sonatype.org"); + @Inject + public ComponentReportClient(final HttpClient client) { + this(client, "https://ossindex.sonatype.org"); } // Visible for testing - ComponentReportClient(final String hostname) { + ComponentReportClient(final HttpClient client, final String hostname) { + this.client = client; this.hostname = hostname; } diff --git a/src/test/java/it/mulders/mcs/AppIT.java b/src/test/java/it/mulders/mcs/AppIT.java index e4cd3147..00a551f7 100644 --- a/src/test/java/it/mulders/mcs/AppIT.java +++ b/src/test/java/it/mulders/mcs/AppIT.java @@ -4,115 +4,149 @@ import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable; import static java.util.Arrays.asList; -import it.mulders.mcs.cli.Cli; -import it.mulders.mcs.cli.SystemPropertyLoader; import java.util.List; -import java.util.Properties; import org.assertj.core.api.WithAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.junitpioneer.jupiter.StdIo; +import org.junitpioneer.jupiter.StdOut; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class AppIT implements WithAssertions { - private final Cli command = new Cli() { - @Override - public SearchCommand createSearchCommand() { - return new SearchCommand() { - public Integer call() { - return 0; - } - }; + @Nested + class TechnicalIT { + @BeforeEach + void clearProxyProperties() { + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); } - @Override - public ClassSearchCommand createClassSearchCommand() { - return new ClassSearchCommand() { - public Integer call() { - return 0; - } - }; + @Test + void should_show_version() throws Exception { + var output = tapSystemOut(() -> App.doMain("-V")); + assertThat(output).contains("mcs v"); } - }; - - @BeforeEach - void clearProxyProperties() { - System.clearProperty("http.proxyHost"); - System.clearProperty("http.proxyPort"); - System.clearProperty("https.proxyHost"); - System.clearProperty("https.proxyPort"); - } - @Test - void should_show_version() throws Exception { - var output = tapSystemOut(() -> App.doMain("-V")); - assertThat(output).contains("mcs v"); - } + @Test + void should_exit_cleanly() { + assertThat(App.doMain("-V")).isEqualTo(0); + } - @Test - void should_exit_cleanly() { - assertThat(App.doMain("-V")).isEqualTo(0); - } + @Test + void should_exit_nonzero_on_wrong_invocation() { + assertThat(App.doMain("--does-not-exist")).isNotEqualTo(0); + } - @Test - void should_exit_nonzero_on_wrong_invocation() { - assertThat(App.doMain("--does-not-exist")).isNotEqualTo(0); - } + @Test + void runs_without_search_command_specified() { + assertThat(App.doMain("info.picocli:picocli")).isEqualTo(0); + } + + @Test + void should_not_set_proxy_system_properties_when_no_env_variable_is_present() throws Exception { + List values = withEnvironmentVariable("HTTP_PROXY", null) + .and("HTTPS_PROXY", null) + .execute(() -> { + App.doMain("info.picocli:picocli"); + + return asList( + System.getProperty("http.proxyHost"), + System.getProperty("http.proxyPort"), + System.getProperty("https.proxyHost"), + System.getProperty("https.proxyPort")); + }); + + assertThat(values).isEqualTo(asList(null, null, null, null)); + } - @Test - void runs_without_search_command_specified() { - assertThat(App.doMain(command, new SystemPropertyLoader(), "info.picocli:picocli")) - .isEqualTo(0); + @Test + void should_set_proxy_system_properties_when_env_variables_are_present() throws Exception { + List values = withEnvironmentVariable("HTTP_PROXY", "http://http.proxy.example.com:8080") + .and("HTTPS_PROXY", "http://https.proxy.example.com:8484") + .execute(() -> { + App.doMain("info.picocli:picocli"); + + return asList( + System.getProperty("http.proxyHost"), + System.getProperty("http.proxyPort"), + System.getProperty("https.proxyHost"), + System.getProperty("https.proxyPort")); + }); + + assertThat(values).isEqualTo(asList("http.proxy.example.com", "8080", "https.proxy.example.com", "8484")); + } } - @Test - void should_load_additional_system_properties() { - var loader = new SystemPropertyLoader() { - @Override - public Properties getProperties() { - var tmp = super.getProperties(); - tmp.put("example", "value"); - return tmp; - } - }; + @Nested + class FunctionalIT { + @StdIo + @Test + void should_find_plexus_utils_341(StdOut out) { + App.doMain("search", "org.codehaus.plexus:plexus-utils:3.4.1"); - App.doMain(command, loader, "info.picocli:picocli"); + var output = out.capturedLines(); - assertThat(System.getProperty("example")).isEqualTo("value"); - } + assertThat(output).anySatisfy(line -> assertThat(line).contains("org.codehaus.plexus")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("plexus-utils")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("3.4.1")); + } - @Test - void should_not_set_proxy_system_properties_when_no_env_variable_is_present() throws Exception { - List values = withEnvironmentVariable("HTTP_PROXY", null) - .and("HTTPS_PROXY", null) - .execute(() -> { - App.doMain(command, new SystemPropertyLoader(), "info.picocli:picocli"); - - return asList( - System.getProperty("http.proxyHost"), - System.getProperty("http.proxyPort"), - System.getProperty("https.proxyHost"), - System.getProperty("https.proxyPort")); - }); - - assertThat(values).isEqualTo(asList(null, null, null, null)); - } + @StdIo + @Test + void should_find_plexus_utils_341_without_search(StdOut out) { + App.doMain("org.codehaus.plexus:plexus-utils:3.4.1"); + + var output = out.capturedLines(); + + assertThat(output).anySatisfy(line -> assertThat(line).contains("org.codehaus.plexus")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("plexus-utils")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("3.4.1")); + } - @Test - void should_set_proxy_system_properties_when_env_variables_are_present() throws Exception { - List values = withEnvironmentVariable("HTTP_PROXY", "http://http.proxy.example.com:8080") - .and("HTTPS_PROXY", "http://https.proxy.example.com:8484") - .execute(() -> { - App.doMain(command, new SystemPropertyLoader(), "info.picocli:picocli"); - - return asList( - System.getProperty("http.proxyHost"), - System.getProperty("http.proxyPort"), - System.getProperty("https.proxyHost"), - System.getProperty("https.proxyPort")); - }); - - assertThat(values).isEqualTo(asList("http.proxy.example.com", "8080", "https.proxy.example.com", "8484")); + @StdIo + @Test + void should_find_multiple_jreleaser_maven_plugin(StdOut out) { + App.doMain("search", "org.jreleaser:jreleaser-maven-plugin"); + + var output = out.capturedLines(); + + assertThat(output).anySatisfy(line -> assertThat(line).matches("Found (\\d*) results \\(showing 20\\)")); + assertThat(output) + .anySatisfy(line -> assertThat(line).contains("org.jreleaser:jreleaser-maven-plugin:1.16.0")); + } + + @StdIo + @Test + void should_find_many_artifacts_for_JAX_WS_Handler(StdOut out) { + App.doMain("class-search", "-f", "javax.xml.ws.handler.Handler", "-l", "250"); + + var output = out.capturedLines(); + + assertThat(output).hasSizeGreaterThan(250); + assertThat(output).anySatisfy(line -> assertThat(line).contains("jakarta.xml.ws:jakarta.xml.ws-api:2.3.3")); + } + + @StdIo + @Test + void should_find_artifacts_for_Clocky_class(StdOut out) { + App.doMain("class-search", "AdvanceableTime"); + + var output = out.capturedLines(); + + assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4.1")); + } + + @StdIo + @Test + void should_find_artifacts_for_Clocky_full_class_name(StdOut out) { + App.doMain("class-search", "-f", "it.mulders.clocky.AdvanceableTime"); + + var output = out.capturedLines(); + + assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4")); + assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4.1")); + } } } diff --git a/src/test/java/it/mulders/mcs/AppTest.java b/src/test/java/it/mulders/mcs/AppTest.java index a8ea481d..b11f74b5 100644 --- a/src/test/java/it/mulders/mcs/AppTest.java +++ b/src/test/java/it/mulders/mcs/AppTest.java @@ -1,7 +1,7 @@ package it.mulders.mcs; import it.mulders.mcs.cli.Cli; -import it.mulders.mcs.cli.CommandClassFactory; +import it.mulders.mcs.cli.MockitoFactory; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -25,7 +25,7 @@ void should_prepend_search_to_command_line_args() { @Nested class IsInvocationWithoutSearchCommand { private final Cli cli = new Cli(); - private final CommandLine program = new CommandLine(cli, new CommandClassFactory(cli)); + private final CommandLine program = new CommandLine(cli, MockitoFactory.INSTANCE); @Test void should_detect_when_search_command_is_not_present() { diff --git a/src/test/java/it/mulders/mcs/cli/ClassSearchCommandTest.java b/src/test/java/it/mulders/mcs/cli/ClassSearchCommandTest.java new file mode 100644 index 00000000..d990fc7a --- /dev/null +++ b/src/test/java/it/mulders/mcs/cli/ClassSearchCommandTest.java @@ -0,0 +1,54 @@ +package it.mulders.mcs.cli; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import it.mulders.mcs.search.Constants; +import it.mulders.mcs.search.SearchCommandHandler; +import it.mulders.mcs.search.SearchQuery; +import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ClassSearchCommandTest implements WithAssertions { + private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); + + @Test + void delegates_to_handler() { + // Arrange + var command = new ClassSearchCommand(searchCommandHandler, "test", null, false); + + // Act + command.call(); + + // Assert + var query = SearchQuery.classSearch("test").build(); + verifyHandlerInvocation("maven", false, query); + } + + @Test + void accepts_full_name_parameter() { + // Arrange + var command = new ClassSearchCommand(searchCommandHandler, "test", null, true); + + // Act + command.call(); + + // Assert + var query = SearchQuery.classSearch("test") + .isFullyQualified(true) + .withLimit(Constants.DEFAULT_MAX_SEARCH_RESULTS) + .build(); + verifyHandlerInvocation("maven", false, query); + } + + private void verifyHandlerInvocation(String outputFormat, boolean reportVulnerabilities, SearchQuery query) { + var captor = ArgumentCaptor.forClass(SearchQuery.class); + verify(searchCommandHandler).search(captor.capture(), eq(outputFormat), eq(reportVulnerabilities)); + assertThat(captor.getValue()).isEqualTo(query); + } +} diff --git a/src/test/java/it/mulders/mcs/cli/CliTest.java b/src/test/java/it/mulders/mcs/cli/CliTest.java index 361ba509..64b234e5 100644 --- a/src/test/java/it/mulders/mcs/cli/CliTest.java +++ b/src/test/java/it/mulders/mcs/cli/CliTest.java @@ -1,90 +1,126 @@ package it.mulders.mcs.cli; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.mock; -import it.mulders.mcs.search.Constants; import it.mulders.mcs.search.SearchCommandHandler; -import it.mulders.mcs.search.SearchQuery; +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.MockedConstruction; -import org.mockito.Mockito; import picocli.CommandLine; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class CliTest implements WithAssertions { - - private final Cli cli = new Cli(); - private final CommandLine program = new CommandLine(cli, new CommandClassFactory(cli)); - - @Nested - class SearchCommandTest { - @Test - void delegates_to_handler() { - var query = SearchQuery.search("test").build(); - - verifySearchExecution(query, "search", "test"); - } - - @Test - void accepts_space_separated_terms() { - SearchQuery query = SearchQuery.search("jakarta rs").build(); - - verifySearchExecution(query, "search", "jakarta", "rs"); + private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); + private final SearchCommand searchCommand = new SearchCommand(searchCommandHandler); + private final ClassSearchCommand classSearchCommand = new ClassSearchCommand(searchCommandHandler); + + private final CommandLine.IFactory commandLineFactory = new CommandLine.IFactory() { + @Override + public K create(Class cls) throws Exception { + if (SearchCommand.class.equals(cls)) { + return (K) searchCommand; + } else if (ClassSearchCommand.class.equals(cls)) { + return (K) classSearchCommand; + } else { + return mock(cls); + } } + }; + private final CommandLine program = new CommandLine(new Cli(), commandLineFactory); + + @Test + void should_invoke_search_command() { + // Arrange + + // Act + program.execute("search", "plexus-utils"); + + // Assert + assertThat(searchCommand) + .extracting("query", InstanceOfAssertFactories.ARRAY) + .isEqualTo(new String[] {"plexus-utils"}); + assertThat(searchCommand).extracting("responseFormat").isNull(); + assertThat(classSearchCommand).extracting("limit").isNull(); + } - @Test - void accepts_limit_results_parameter() { - var query = SearchQuery.search("test").withLimit(3).build(); + @Test + void should_invoke_search_command_with_limit() { + // Arrange - verifySearchExecution(query, "search", "--limit", "3", "test"); - } + // Act + program.execute("search", "-l", "3", "plexus-utils"); - @Test - void accepts_output_type_parameter() { - var query = SearchQuery.search("test").build(); + // Assert + assertThat(searchCommand) + .extracting("limit", InstanceOfAssertFactories.INTEGER) + .isEqualTo(3); + } - verifySearchExecution(query, "search", "--format", "gradle-short", "test"); - } + @Test + void should_invoke_search_command_with_format() { + // Arrange - @Test - void accepts_show_vulnerabilities_parameter() { - var query = SearchQuery.search("test").build(); + // Act + program.execute("search", "-f", "gradle", "plexus-utils"); - verifySearchExecution(query, "search", "--show-vulnerabilities", "test"); - } + // Assert + assertThat(searchCommand) + .extracting("responseFormat", InstanceOfAssertFactories.STRING) + .isEqualTo("gradle"); } - @Nested - class ClassSearchCommandTest { - - @Test - void delegates_to_handler() { - var query = SearchQuery.classSearch("test").build(); + @Test + void should_invoke_class_search_command() { + // Arrange + + // Act + program.execute("class-search", "WithAssertions"); + + // Assert + assertThat(classSearchCommand) + .extracting("query", InstanceOfAssertFactories.STRING) + .isEqualTo("WithAssertions"); + assertThat(classSearchCommand) + .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) + .isFalse(); + assertThat(classSearchCommand).extracting("limit").isNull(); + } - verifySearchExecution(query, "class-search", "test"); - } + @Test + void should_invoke_class_search_command_with_limit() { + // Arrange + + // Act + program.execute("class-search", "-l", "3", "WithAssertions"); + + // Assert + assertThat(classSearchCommand) + .extracting("query", InstanceOfAssertFactories.STRING) + .isEqualTo("WithAssertions"); + assertThat(classSearchCommand) + .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) + .isFalse(); + assertThat(classSearchCommand) + .extracting("limit", InstanceOfAssertFactories.INTEGER) + .isEqualTo(3); + } - @Test - void accepts_full_name_parameter() { - var query = SearchQuery.classSearch("test") - .isFullyQualified(true) - .withLimit(Constants.DEFAULT_MAX_SEARCH_RESULTS) - .build(); + @Test + void should_invoke_class_search_command_with_full_classname() { + // Arrange - verifySearchExecution(query, "class-search", "--full-name", "test"); - } - } + // Act + program.execute("class-search", "-f", "org.assertj.core.api.WithAssertions"); - private void verifySearchExecution(SearchQuery query, String... args) { - try (MockedConstruction mocked = Mockito.mockConstruction(SearchCommandHandler.class)) { - program.execute(args); - assertThat(mocked.constructed()).hasSize(1); - SearchCommandHandler searchCommandHandler = mocked.constructed().get(0); - verify(searchCommandHandler).search(query); - } + // Assert + assertThat(classSearchCommand) + .extracting("query", InstanceOfAssertFactories.STRING) + .isEqualTo("org.assertj.core.api.WithAssertions"); + assertThat(classSearchCommand) + .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) + .isTrue(); } + // } diff --git a/src/test/java/it/mulders/mcs/cli/CommandClassFactoryTest.java b/src/test/java/it/mulders/mcs/cli/CommandClassFactoryTest.java deleted file mode 100644 index 3d1a231b..00000000 --- a/src/test/java/it/mulders/mcs/cli/CommandClassFactoryTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package it.mulders.mcs.cli; - -import org.assertj.core.api.WithAssertions; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; - -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class CommandClassFactoryTest implements WithAssertions { - private final Cli cli = new Cli(); - private final CommandClassFactory factory = new CommandClassFactory(cli); - - @Test - void can_construct_search_command_instance() throws Exception { - assertThat(factory.create(Cli.SearchCommand.class)).isNotNull(); - } - - @Test - void can_construct_arbitrary_other_class() throws Exception { - assertThat(factory.create(Dummy.class)).isNotNull(); - } - - static class Dummy {} -} diff --git a/src/test/java/it/mulders/mcs/cli/DaggerFactoryTest.java b/src/test/java/it/mulders/mcs/cli/DaggerFactoryTest.java new file mode 100644 index 00000000..0776eb74 --- /dev/null +++ b/src/test/java/it/mulders/mcs/cli/DaggerFactoryTest.java @@ -0,0 +1,34 @@ +package it.mulders.mcs.cli; + +import it.mulders.mcs.dagger.DaggerFactory; +import jakarta.inject.Provider; +import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DaggerFactoryTest implements WithAssertions { + private final SearchCommand searchCommand = new SearchCommand(null); + private final ClassSearchCommand classSearchCommand = new ClassSearchCommand(null); + private final Provider searchCommandProvider = () -> searchCommand; + private final Provider classSearchCommandProvider = () -> classSearchCommand; + private final DaggerFactory factory = new DaggerFactory(classSearchCommandProvider, searchCommandProvider); + + @Test + void can_construct_ClassSearchCommand_instance() throws Exception { + assertThat(factory.create(ClassSearchCommand.class)).isEqualTo(classSearchCommand); + } + + @Test + void can_construct_SearchCommand_instance() throws Exception { + assertThat(factory.create(SearchCommand.class)).isEqualTo(searchCommand); + } + + @Test + void can_construct_arbitrary_other_class() throws Exception { + assertThat(factory.create(Dummy.class)).isNotNull(); + } + + static class Dummy {} +} diff --git a/src/test/java/it/mulders/mcs/cli/MockitoFactory.java b/src/test/java/it/mulders/mcs/cli/MockitoFactory.java new file mode 100644 index 00000000..57fa5f0d --- /dev/null +++ b/src/test/java/it/mulders/mcs/cli/MockitoFactory.java @@ -0,0 +1,13 @@ +package it.mulders.mcs.cli; + +import org.mockito.Mockito; +import picocli.CommandLine; + +public class MockitoFactory implements CommandLine.IFactory { + public static final CommandLine.IFactory INSTANCE = new MockitoFactory(); + + @Override + public K create(Class cls) throws Exception { + return Mockito.mock(cls); + } +} diff --git a/src/test/java/it/mulders/mcs/cli/SearchCommandTest.java b/src/test/java/it/mulders/mcs/cli/SearchCommandTest.java new file mode 100644 index 00000000..69fbf6ec --- /dev/null +++ b/src/test/java/it/mulders/mcs/cli/SearchCommandTest.java @@ -0,0 +1,89 @@ +package it.mulders.mcs.cli; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import it.mulders.mcs.search.SearchCommandHandler; +import it.mulders.mcs.search.SearchQuery; +import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SearchCommandTest implements WithAssertions { + private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); + + @Test + void delegates_to_handler() { + // Arrange + var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "maven", false); + + // Act + command.call(); + + // Assert + var query = SearchQuery.search("test").build(); + verifyHandlerInvocation("maven", false, query); + } + + @Test + void accepts_space_separated_terms() { + // Arrange + var command = new SearchCommand(searchCommandHandler, new String[] {"jakarta", "rs"}, null, "maven", false); + + // Act + command.call(); + + // Assert + var query = SearchQuery.search("jakarta rs").build(); + verifyHandlerInvocation("maven", false, query); + } + + @Test + void accepts_limit_results_parameter() { + // Arrange + var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, 3, "maven", false); + + // Act + command.call(); + + // Assert + var query = SearchQuery.search("test").withLimit(3).build(); + verifyHandlerInvocation("maven", false, query); + } + + @Test + void accepts_output_type_parameter() { + // Arrange + var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "gradle-short", false); + + // Act + command.call(); + + // Assert + var query = SearchQuery.search("test").build(); + verifyHandlerInvocation("gradle-short", false, query); + } + + @Test + void accepts_show_vulnerabilities_parameter() { + // Arrange + var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "maven", true); + + // Act + command.call(); + + // Assert + var query = SearchQuery.search("test").build(); + verifyHandlerInvocation("maven", true, query); + } + + private void verifyHandlerInvocation(String outputFormat, boolean reportVulnerabilities, SearchQuery query) { + var captor = ArgumentCaptor.forClass(SearchQuery.class); + verify(searchCommandHandler).search(captor.capture(), eq(outputFormat), eq(reportVulnerabilities)); + assertThat(captor.getValue()).isEqualTo(query); + } +} diff --git a/src/test/java/it/mulders/mcs/search/SearchClientIT.java b/src/test/java/it/mulders/mcs/search/SearchClientIT.java index 64ef8851..9400d36f 100644 --- a/src/test/java/it/mulders/mcs/search/SearchClientIT.java +++ b/src/test/java/it/mulders/mcs/search/SearchClientIT.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; +import java.net.http.HttpClient; import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.assertj.core.api.WithAssertions; @@ -25,6 +26,8 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SearchClientIT implements WithAssertions { + private final HttpClient httpClient = HttpClient.newHttpClient(); + @RegisterExtension static WireMockExtension wiremock = WireMockExtension.newInstance() .options(wireMockConfig() @@ -54,7 +57,7 @@ void should_parse_response() { .willReturn(ok(getResourceAsString("/wildcard-search-response.json")))); // Act - var result = new SearchClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(new WildcardSearchQuery( "plexus-utils", Constants.DEFAULT_MAX_SEARCH_RESULTS, Constants.DEFAULT_START)); @@ -80,7 +83,7 @@ void should_parse_response_groupId_artifactId() { .willReturn(ok(getResourceAsString("/group-artifact-search.json")))); // Act - var result = new SearchClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(SearchQuery.search("org.codehaus.plexus:plexus-utils") .build()); @@ -102,7 +105,7 @@ void should_parse_response_groupId_artifactId_version() { .willReturn(ok(getResourceAsString("/group-artifact-version-search.json")))); // Act - var result = new SearchClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(SearchQuery.search("org.codehaus.plexus:plexus-utils:3.4.1") .build()); @@ -128,7 +131,7 @@ void should_gracefully_handle_4xx_response() { .willReturn(badRequest().withBody("Solr returned 400, msg: "))); // Act - var result = new SearchClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(SearchQuery.search("org.codehaus.plexus:plexus-utils") .build()); @@ -141,7 +144,7 @@ void should_gracefully_handle_4xx_response() { @Test void should_gracefully_handle_connection_failure() { // Very unlikely there's an HTTP server running there... - var result = new SearchClient("http://localhost:21") + var result = new SearchClient(httpClient, "http://localhost:21") .search(new WildcardSearchQuery( "plexus-utils", Constants.DEFAULT_MAX_SEARCH_RESULTS, Constants.DEFAULT_START)); diff --git a/src/test/java/it/mulders/mcs/search/SearchCommandHandlerTest.java b/src/test/java/it/mulders/mcs/search/SearchCommandHandlerTest.java index 803d3011..eb468bf9 100644 --- a/src/test/java/it/mulders/mcs/search/SearchCommandHandlerTest.java +++ b/src/test/java/it/mulders/mcs/search/SearchCommandHandlerTest.java @@ -4,9 +4,12 @@ import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import it.mulders.mcs.common.Result; +import it.mulders.mcs.search.printer.OutputFactory; import it.mulders.mcs.search.printer.OutputPrinter; +import it.mulders.mcs.search.vulnerability.ComponentReportClient; import javax.net.ssl.SSLHandshakeException; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.DisplayName; @@ -17,46 +20,99 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SearchCommandHandlerTest implements WithAssertions { + private final ComponentReportClient componentReportClient = mock(ComponentReportClient.class); private final OutputPrinter outputPrinter = mock(OutputPrinter.class); - private final SearchResponse.Response wildcardResponse = - new SearchResponse.Response(0, 0, new SearchResponse.Response.Doc[] {}); - private final SearchResponse.Response twoPartCoordinateResponse = - new SearchResponse.Response(0, 0, new SearchResponse.Response.Doc[] {}); - private final SearchResponse.Response threePartCoordinateResponse = - new SearchResponse.Response(0, 0, new SearchResponse.Response.Doc[] {}); - private final SearchClient searchClient = new SearchClient() { + private final OutputFactory outputFactory = new OutputFactory() { @Override - public Result search(final SearchQuery query) { - if (query.toSolrQuery().contains("tls-error")) { - return new Result.Failure<>( - new SSLHandshakeException( - "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target")); - } - if (query instanceof WildcardSearchQuery) { - return new Result.Success<>(new SearchResponse(null, wildcardResponse)); - } else if (query instanceof CoordinateQuery cq && cq.version().isBlank()) { - return new Result.Success<>(new SearchResponse(null, twoPartCoordinateResponse)); - } else { - return new Result.Success<>(new SearchResponse(null, threePartCoordinateResponse)); - } + public OutputPrinter findOutputPrinter(String formatName) { + return outputPrinter; } }; - private final SearchCommandHandler handler = new SearchCommandHandler(outputPrinter, false, searchClient, null); + private final SearchResponse.Response wildcardResponse = + new SearchResponse.Response(0, 0, new SearchResponse.Response.Doc[] { + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "1.0", "1.0", "jar", System.currentTimeMillis()) + }); + private final SearchResponse.Response twoPartCoordinateResponse = + new SearchResponse.Response(2, 0, new SearchResponse.Response.Doc[] { + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "1.0", "2.0", "jar", System.currentTimeMillis()), + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "2.0", "2.0", "jar", System.currentTimeMillis()) + }); + private final SearchResponse.Response threePartCoordinateResponse = + new SearchResponse.Response(3, 0, new SearchResponse.Response.Doc[] { + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "1.0", "3.0", "jar", System.currentTimeMillis()), + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "2.0", "3.0", "jar", System.currentTimeMillis()), + new SearchResponse.Response.Doc( + "foo:bar", "foo", "bar", "3.0", "3.0", "jar", System.currentTimeMillis()), + }); + private final SearchResponse.Response singleArtifactResponse = + new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { + new SearchResponse.Response.Doc( + "org.codehaus.plexus:plexus-utils:3.4.1", + "org.codehaus.plexus", + "plexus-utils", + "3.4.1", + "3.4.1", + "jar", + System.currentTimeMillis()), + }); + private final SearchResponse.Response multipleArtifactResponse = + new SearchResponse.Response(2, 0, new SearchResponse.Response.Doc[] { + new SearchResponse.Response.Doc( + "org.codehaus.plexus:plexus-utils:3.4.0", + "org.codehaus.plexus", + "plexus-utils", + "3.4.0", + "3.4.1", + "jar", + System.currentTimeMillis()), + new SearchResponse.Response.Doc( + "org.codehaus.plexus:plexus-utils:3.4.1", + "org.codehaus.plexus", + "plexus-utils", + "3.4.1", + "3.4.1", + "jar", + System.currentTimeMillis()) + }); + + private final SearchClient searchClient = mock(SearchClient.class); + ; + + private final SearchCommandHandler handler = + new SearchCommandHandler(componentReportClient, outputFactory, searchClient); @Nested @DisplayName("Wildcard search") class WildcardSearchTest { @Test void should_invoke_search_client() { - handler.search(SearchQuery.search("plexus-utils").build()); - verify(outputPrinter).print(any(WildcardSearchQuery.class), eq(wildcardResponse), any()); + // Arrange + when(searchClient.search(any())) + .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); + + // Act + handler.search(SearchQuery.search("plexus-utils").build(), "maven", false); + + // Assert + verify(outputPrinter).print(any(WildcardSearchQuery.class), eq(singleArtifactResponse), any()); } @Test void should_propagate_tls_exception_to_runtime_exception() { + // Arrange + var result = new Result.Failure( + new SSLHandshakeException( + "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target")); + when(searchClient.search(any())).thenReturn(result); + assertThatThrownBy( - () -> handler.search(SearchQuery.search("tls-error").build())) + () -> handler.search(SearchQuery.search("tls-error").build(), "maven", false)) .isInstanceOf(RuntimeException.class); } } @@ -64,31 +120,43 @@ void should_propagate_tls_exception_to_runtime_exception() { @Nested @DisplayName("Coordinate search") class CoordinateSearchTest { - @Test - void should_reject_search_terms_in_wrong_format() { - assertThatThrownBy(() -> - handler.search(SearchQuery.search("foo:bar:baz:qux").build())) - .isInstanceOf(IllegalArgumentException.class); - } - @Test void should_invoke_search_client_with_groupId_and_artifactId() { - handler.search( - SearchQuery.search("org.codehaus.plexus:plexus-utils").build()); - verify(outputPrinter).print(any(CoordinateQuery.class), eq(twoPartCoordinateResponse), any()); + // Arrange + when(searchClient.search(any())) + .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); + var handler = new SearchCommandHandler(componentReportClient, outputFactory, searchClient); + var query = SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); + + // Act + handler.search(query, "maven", false); + + // Assert + verify(outputPrinter).print(eq(query), eq(singleArtifactResponse), any()); } @Test void should_invoke_search_client_with_groupId_and_artifactId_and_version() { + // Arrange + when(searchClient.search(any())) + .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); + var handler = new SearchCommandHandler(componentReportClient, outputFactory, searchClient); + + // Act handler.search( - SearchQuery.search("org.codehaus.plexus:plexus-utils:3.4.1").build()); - verify(outputPrinter).print(any(CoordinateQuery.class), eq(threePartCoordinateResponse), any()); + SearchQuery.search("org.codehaus.plexus:plexus-utils").build(), "maven", false); + + // Assert + verify(outputPrinter).print(any(CoordinateQuery.class), eq(singleArtifactResponse), any()); } @Test void should_propagate_tls_exception_to_runtime_exception() { - assertThatThrownBy(() -> handler.search(SearchQuery.search("org.codehaus.plexus:tls-error:3.4.1") - .build())) + assertThatThrownBy(() -> handler.search( + SearchQuery.search("org.codehaus.plexus:tls-error:3.4.1") + .build(), + "maven", + false)) .isInstanceOf(RuntimeException.class); } } diff --git a/src/test/java/it/mulders/mcs/search/SearchQueryTest.java b/src/test/java/it/mulders/mcs/search/SearchQueryTest.java index cfd784e4..401def62 100644 --- a/src/test/java/it/mulders/mcs/search/SearchQueryTest.java +++ b/src/test/java/it/mulders/mcs/search/SearchQueryTest.java @@ -55,6 +55,12 @@ void should_build_query_with_only_artifactId() { }); } + @Test + void should_reject_invalid_input() { + assertThatThrownBy(() -> SearchQuery.search("foo:bar:baz:qux").build()) + .isInstanceOf(IllegalArgumentException.class); + } + @ParameterizedTest @CsvSource( textBlock = diff --git a/src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportClientIT.java b/src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportClientIT.java index d875858b..07982616 100644 --- a/src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportClientIT.java +++ b/src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportClientIT.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; +import java.net.http.HttpClient; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -28,6 +29,8 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class ComponentReportClientIT implements WithAssertions { + private final HttpClient httpClient = HttpClient.newHttpClient(); + @RegisterExtension static WireMockExtension wiremock = WireMockExtension.newInstance() .options(wireMockConfig() @@ -63,7 +66,7 @@ void should_parse_response() { .willReturn(ok(getResourceAsString("/vulnerabilities-component-report-response.json")))); // Act - var result = new ComponentReportClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(List.of("pkg:maven/org.apache.shiro/shiro-web@1.9.0")); // Assert @@ -89,7 +92,7 @@ void should_parse_response() { .willReturn(ok(getResourceAsString("/no-vulnerabilities-component-report-response.json")))); // Act - var result = new ComponentReportClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); // Assert @@ -110,7 +113,7 @@ void should_gracefully_handle_4xx_response() { .willReturn(badRequest().withBody("Ossindex returned 400, msg: "))); // Act - var result = new ComponentReportClient(wmRuntimeInfo.getHttpBaseUrl()) + var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); // Assert @@ -122,7 +125,7 @@ void should_gracefully_handle_4xx_response() { @Test void should_gracefully_handle_connection_failure() { // Very unlikely there's an HTTP server running there... - var result = new ComponentReportClient("http://localhost:21") + var result = new ComponentReportClient(httpClient, "http://localhost:21") .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); assertThat(result).isInstanceOf(Result.Failure.class);