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);