From 0f70f30b681904a85d6779c9d81ccb40ff120a01 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Thu, 30 Jul 2026 11:54:26 -0700 Subject: [PATCH 01/24] evaluating how to make composites workable --- pom.xml | 1 + testing/pom.xml | 191 ++++++++++++++++++ testing/src/test/java/mock/NoOpException.java | 21 ++ .../src/test/java/mock/OpensearchEngine.java | 65 ++++++ .../OpensearchSupportedFunctionality.java | 9 + .../test/java/mock/annotation/replace.java | 6 + 6 files changed, 293 insertions(+) create mode 100644 testing/pom.xml create mode 100644 testing/src/test/java/mock/NoOpException.java create mode 100644 testing/src/test/java/mock/OpensearchEngine.java create mode 100644 testing/src/test/java/mock/OpensearchSupportedFunctionality.java create mode 100644 testing/src/test/java/mock/annotation/replace.java diff --git a/pom.xml b/pom.xml index 8f5db02d..9e1c933c 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,7 @@ Go through this file line-by-line and replace the template values with your own. common harvest manager + testing diff --git a/testing/pom.xml b/testing/pom.xml new file mode 100644 index 00000000..0eb34784 --- /dev/null +++ b/testing/pom.xml @@ -0,0 +1,191 @@ + + + + 4.0.0 + + + + gov.nasa.pds + registry-loader + 1.4.0-SNAPSHOT + + + gov.nasa.pds.registry-loader + registry-loader-testing + jar + + PDS Registry Loader Testing + + Gherkin based testing - both integrated and regression + + + + Jet Propulsion Laboratory, California Institute of Technology + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + 21 + 21 + UTF-8 + + + + + + org.elasticsearch.client + elasticsearch-rest-client + 9.1.3 + + + + com.google.code.gson + gson + 2.13.2 + + + + org.json + json + 20250517 + + + + org.apache.logging.log4j + log4j-core + 2.25.3 + + + + org.apache.commons + commons-lang3 + 3.18.0 + + + + org.apache.tika + tika-core + 3.2.2 + + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.2 + + + com.sun.xml.bind + jaxb-impl + 4.0.5 + + + jakarta.activation + jakarta.activation-api + 2.1.4 + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.5 + + + org.opensearch.client + opensearch-java + 3.2.0 + + + software.amazon.awssdk + opensearch + 2.33.9 + + + software.amazon.awssdk + apache-client + 2.33.9 + + + + + + + + maven-site-plugin + + true + true + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + + + + src/main/resources + false + + + src/main/resources/app + true + + + + + src/test/resources + + + + ../harvest/src/main/resources + + + + + + https://github.com/NASA-PDS/pds-registry-loader-testing + scm:git:git@github.com/NASA-PDS/pds-registry-loader-testing.git + scm:git:git@github.com:NASA-PDS/pds-registry-loader-testing.git + v1.1.0 + + diff --git a/testing/src/test/java/mock/NoOpException.java b/testing/src/test/java/mock/NoOpException.java new file mode 100644 index 00000000..4bbe3eb3 --- /dev/null +++ b/testing/src/test/java/mock/NoOpException.java @@ -0,0 +1,21 @@ +package mock; + +public class NoOpException extends RuntimeException { + private static final long serialVersionUID = 8994806487926218572L; + public NoOpException() { + super(); + } + public NoOpException(String message) { + super(message); + } + public NoOpException(Throwable cause) { + super(cause); + } + public NoOpException(String message, Throwable cause) { + super(message, cause); + } + public NoOpException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java new file mode 100644 index 00000000..034bdbb3 --- /dev/null +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -0,0 +1,65 @@ +package mock; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; + +import mock.annotation.replace; + +public final class OpensearchEngine { + private record MethodTarget(Object instance, Method method) {} + private final Map redirect = new ConcurrentHashMap<>(); + private String functionArgs = null; + private String functionName = null; + /** + * Scans the composite for @mock.annotation.replace methods and updates the redirect map. + */ + public void add(@Nonnull OpensearchSupportedFunctionality composite) { + // @Nonnull handles static/IDE analysis. + // Objects.requireNonNull(composite) can be added here if strict runtime enforcement is required. + + Method[] methods = composite.getClass().getDeclaredMethods(); + + Arrays.stream(methods) + .filter(method -> method.isAnnotationPresent(replace.class)) + .forEach(method -> { + // Ensure private/protected annotated methods can be invoked + method.setAccessible(true); + + // Map the method name to its execution target + redirect.put(method.getName(), new MethodTarget(composite, method)); + }); + } + + public void initialize() { + // make socket + } + + public void listen() { + // listen to the socket and call this.process() + // keeps socket for whole test suite but allows exceptions to do their thing + } + /** + * Sits on socket like opensearch and converts socket requests to OpensearchSupportedFunctionality + * functionName and functionArgs. Current thought is functionArgs is JSON block but unknown right now. + */ + public void process() { + MethodTarget target = this.redirect.get(this.functionName); + if (target == null) { + throw new NoOpException(this.functionName + " is not implemented in this configuration "); + } + try { + target.method().invoke(target.instance(), functionArgs); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new RuntimeException("Error executing redirected " + this.functionName, e.getCause()); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security restriction prevented executing " + this.functionName, e); + } + } +} diff --git a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java new file mode 100644 index 00000000..a12768e7 --- /dev/null +++ b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java @@ -0,0 +1,9 @@ +package mock; + +import java.util.List; + +public interface OpensearchSupportedFunctionality { + public void fu(); + public int bar(List justSomeArg); + public Object snafu(); +} diff --git a/testing/src/test/java/mock/annotation/replace.java b/testing/src/test/java/mock/annotation/replace.java new file mode 100644 index 00000000..794957c2 --- /dev/null +++ b/testing/src/test/java/mock/annotation/replace.java @@ -0,0 +1,6 @@ +package mock.annotation; + +@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +@java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) +public @interface replace { +} From bc6293569841425522cbd99305dbf89cd127992b Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Thu, 30 Jul 2026 12:19:24 -0700 Subject: [PATCH 02/24] add some more concrete mocks --- .../src/test/java/mock/OpensearchEngine.java | 4 +-- .../annotation/{replace.java => Replace.java} | 2 +- testing/src/test/java/mock/osf/Git1377.java | 12 +++++++++ testing/src/test/java/mock/osf/Git1378.java | 16 ++++++++++++ testing/src/test/java/mock/osf/Git2755.java | 26 +++++++++++++++++++ testing/src/test/java/mock/osf/NoOp.java | 20 ++++++++++++++ 6 files changed, 77 insertions(+), 3 deletions(-) rename testing/src/test/java/mock/annotation/{replace.java => Replace.java} (86%) create mode 100644 testing/src/test/java/mock/osf/Git1377.java create mode 100644 testing/src/test/java/mock/osf/Git1378.java create mode 100644 testing/src/test/java/mock/osf/Git2755.java create mode 100644 testing/src/test/java/mock/osf/NoOp.java diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 034bdbb3..c5e81b6d 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -7,7 +7,7 @@ import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; -import mock.annotation.replace; +import mock.annotation.Replace; public final class OpensearchEngine { private record MethodTarget(Object instance, Method method) {} @@ -24,7 +24,7 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { Method[] methods = composite.getClass().getDeclaredMethods(); Arrays.stream(methods) - .filter(method -> method.isAnnotationPresent(replace.class)) + .filter(method -> method.isAnnotationPresent(Replace.class)) .forEach(method -> { // Ensure private/protected annotated methods can be invoked method.setAccessible(true); diff --git a/testing/src/test/java/mock/annotation/replace.java b/testing/src/test/java/mock/annotation/Replace.java similarity index 86% rename from testing/src/test/java/mock/annotation/replace.java rename to testing/src/test/java/mock/annotation/Replace.java index 794957c2..6048c9fb 100644 --- a/testing/src/test/java/mock/annotation/replace.java +++ b/testing/src/test/java/mock/annotation/Replace.java @@ -2,5 +2,5 @@ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) -public @interface replace { +public @interface Replace { } diff --git a/testing/src/test/java/mock/osf/Git1377.java b/testing/src/test/java/mock/osf/Git1377.java new file mode 100644 index 00000000..63833188 --- /dev/null +++ b/testing/src/test/java/mock/osf/Git1377.java @@ -0,0 +1,12 @@ +package mock.osf; + +import java.util.List; +import mock.annotation.Replace; + +public class Git1377 extends NoOp { + @Replace + @Override + public int bar(List justSomeArg) { + return justSomeArg.size(); + } +} diff --git a/testing/src/test/java/mock/osf/Git1378.java b/testing/src/test/java/mock/osf/Git1378.java new file mode 100644 index 00000000..27e89c62 --- /dev/null +++ b/testing/src/test/java/mock/osf/Git1378.java @@ -0,0 +1,16 @@ +package mock.osf; + +import mock.NoOpException; +import mock.annotation.Replace; + +public class Git1378 extends NoOp { + @Replace + @Override + public void fu() { + } + @Replace + @Override + public Object snafu() { + return new NoOpException("Placeholder"); + } +} diff --git a/testing/src/test/java/mock/osf/Git2755.java b/testing/src/test/java/mock/osf/Git2755.java new file mode 100644 index 00000000..35ce10d3 --- /dev/null +++ b/testing/src/test/java/mock/osf/Git2755.java @@ -0,0 +1,26 @@ +package mock.osf; + +import java.util.List; +import mock.OpensearchSupportedFunctionality; +import mock.annotation.Replace; + +public class Git2755 implements OpensearchSupportedFunctionality { + // this class is equivalent to mock.osf.Git1377,mock.osf.Git1378 in the feature file + private OpensearchSupportedFunctionality a = new Git1377(); + private OpensearchSupportedFunctionality b = new Git1378(); + @Replace + @Override + public void fu() { + b.fu(); + } + @Replace + @Override + public int bar(List justSomeArg) { + return a.bar(justSomeArg); + } + @Replace + @Override + public Object snafu() { + return b.snafu(); + } +} diff --git a/testing/src/test/java/mock/osf/NoOp.java b/testing/src/test/java/mock/osf/NoOp.java new file mode 100644 index 00000000..81c408a9 --- /dev/null +++ b/testing/src/test/java/mock/osf/NoOp.java @@ -0,0 +1,20 @@ +package mock.osf; + +import java.util.List; +import mock.NoOpException; +import mock.OpensearchSupportedFunctionality; + +public abstract class NoOp implements OpensearchSupportedFunctionality { + @Override + public void fu() { + throw new NoOpException("Placeholder"); + } + @Override + public int bar(List justSomeArg) { + throw new NoOpException("Placeholder"); + } + @Override + public Object snafu() { + throw new NoOpException("Placeholder"); + } +} From cdc55c15e16009cbeaee307aa45e1c2fe83d7315 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Sun, 9 Aug 2026 09:18:47 -0700 Subject: [PATCH 03/24] initial stab at javalin --- testing/pom.xml | 98 ++++------------ .../src/test/java/mock/OpensearchEngine.java | 105 ++++++++++++++---- .../OpensearchSupportedFunctionality.java | 29 ++++- 3 files changed, 128 insertions(+), 104 deletions(-) diff --git a/testing/pom.xml b/testing/pom.xml index 0eb34784..10c5f353 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -66,80 +66,30 @@ POSSIBILITY OF SUCH DAMAGE. UTF-8 - - - - org.elasticsearch.client - elasticsearch-rest-client - 9.1.3 - - - - com.google.code.gson - gson - 2.13.2 - - - - org.json - json - 20250517 - - - - org.apache.logging.log4j - log4j-core - 2.25.3 - - - - org.apache.commons - commons-lang3 - 3.18.0 - - - - org.apache.tika - tika-core - 3.2.2 - - - - jakarta.xml.bind - jakarta.xml.bind-api - 4.0.2 - + + + - com.sun.xml.bind - jaxb-impl - 4.0.5 - - - jakarta.activation - jakarta.activation-api - 2.1.4 - - - - org.apache.httpcomponents.client5 - httpclient5 - 5.5 - - - org.opensearch.client - opensearch-java - 3.2.0 - - - software.amazon.awssdk - opensearch - 2.33.9 - - - software.amazon.awssdk - apache-client - 2.33.9 + io.javalin + javalin-bom + 7.2.2 + pom + import + + + + + + + io.javalin + javalin + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + @@ -175,10 +125,6 @@ POSSIBILITY OF SUCH DAMAGE. src/test/resources - - - ../harvest/src/main/resources - diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index c5e81b6d..8a270c79 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -1,19 +1,24 @@ package mock; +import io.javalin.Javalin; +import io.javalin.http.Handler; +import io.javalin.http.HandlerType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nonnull; +import java.util.stream.Collectors; +import jakarta.annotation.Nonnull; +import mock.OpensearchSupportedFunctionality.Context; +import mock.OpensearchSupportedFunctionality.Response; import mock.annotation.Replace; public final class OpensearchEngine { private record MethodTarget(Object instance, Method method) {} private final Map redirect = new ConcurrentHashMap<>(); - private String functionArgs = null; - private String functionName = null; + private Javalin app; /** * Scans the composite for @mock.annotation.replace methods and updates the redirect map. */ @@ -34,32 +39,84 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { }); } - public void initialize() { - // make socket + /** + * Converts HTTP paths into safe, matching Java method names. + * Examples: + * GET / -> getRoot + * POST /_bulk -> postBulk + * POST /my-index/_search -> postMyIndexSearch + */ + private String determineMethodName(String method, String path) { + String sanitizedPath = path.replaceAll("[^a-zA-Z0-9/]", ""); + if (sanitizedPath.equals("/") || path.isEmpty()) { + return method.toLowerCase() + "Root"; + } + String camelCasePath = Arrays.stream(sanitizedPath.split("/")) + .filter(segment -> !segment.isEmpty()) + .map(segment -> Character.toUpperCase(segment.charAt(0)) + segment.substring(1)) + .collect(Collectors.joining()); + return method.toLowerCase() + camelCasePath; } - public void listen() { - // listen to the socket and call this.process() - // keeps socket for whole test suite but allows exceptions to do their thing + /** + * Spins up the fixed Javalin instance. Called once during Cucumber initialization. + */ + public void start(int port) { + this.app = Javalin.create(config -> { + Handler catchAll = ctx -> { + Context facadeContext = new Context( + ctx.body(), + ctx.headerMap(), + ctx.queryParamMap().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), + ctx.pathParamMap() + ); + + String targetMethodName = determineMethodName(ctx.method().name(), ctx.path()); + sendResponse (ctx, process (targetMethodName, facadeContext)); + }; + + for (HandlerType method : new HandlerType[]{ + HandlerType.GET, HandlerType.POST, HandlerType.PUT, + HandlerType.PATCH, HandlerType.DELETE, HandlerType.HEAD, HandlerType.OPTIONS}) { + config.routes.addHttpHandler(method, "/*", catchAll); + } + }); + this.app.start(port); } + /** - * Sits on socket like opensearch and converts socket requests to OpensearchSupportedFunctionality - * functionName and functionArgs. Current thought is functionArgs is JSON block but unknown right now. + * Resolves the target interface method by climbing the profile's class hierarchy, + * respecting your explicit @Replace annotation policies. */ - public void process() { - MethodTarget target = this.redirect.get(this.functionName); - if (target == null) { - throw new NoOpException(this.functionName + " is not implemented in this configuration "); - } - try { - target.method().invoke(target.instance(), functionArgs); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof RuntimeException runtimeException) { - throw runtimeException; + private Response process(String methodName, Context context) { + MethodTarget target = redirect.get(methodName); + + if (target == null) { + return new Response(501, + "{\"error\": \"Method '" + methodName + "' not found in active profile hierarchy.\"}", + "application/json"); + } + + try { + return (Response) target.method().invoke(target.instance(), context); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + return new Response(500, "{\"error\": \"Mock runtime error: " + cause.getMessage() + "\"}", "application/json"); + } catch (IllegalAccessException e) { + return new Response(500, "{\"error\": \"Security constraint executing mock method\"}", "application/json"); + } + } + + private void sendResponse(io.javalin.http.Context ctx, Response response) { + ctx.status(response.statusCode()); + ctx.contentType(response.contentType()); + ctx.result(response.body()); + } + + public void stop() { + if (this.app != null) { + this.app.stop(); } - throw new RuntimeException("Error executing redirected " + this.functionName, e.getCause()); - } catch (IllegalAccessException e) { - throw new RuntimeException("Security restriction prevented executing " + this.functionName, e); - } } } diff --git a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java index a12768e7..a373c56e 100644 --- a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java +++ b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java @@ -1,9 +1,30 @@ package mock; -import java.util.List; +import java.util.Map; public interface OpensearchSupportedFunctionality { - public void fu(); - public int bar(List justSomeArg); - public Object snafu(); + public record Context( + String body, + Map headers, + Map queryParams, + Map pathParams) { + } + + public record Response( + int statusCode, + String body, + String contentType) { + // Convenience factory for standard 200 OK JSON responses + public static Response json(String jsonBody) { + return new Response(200, jsonBody, "application/json"); + } + // Convenience factory for standard empty success responses + public static Response empty(int statusCode) { + return new Response(statusCode, "{}", "application/json"); + } + } + + public Response authorize(Context ctx); + public Response getRoot(Context ctx); + public Response postBulk(Context ctx); } From 0a3f135243a38ddfe15ddbf5e4866fd5edff45e7 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Sun, 9 Aug 2026 13:35:16 -0700 Subject: [PATCH 04/24] ready to add cucumber... --- testing/src/test/java/expect/Sane.java | 34 +++++ testing/src/test/java/mock/MockAware.java | 7 + .../src/test/java/mock/OpensearchEngine.java | 144 ++++++++++-------- testing/src/test/java/mock/osf/Git1377.java | 12 -- testing/src/test/java/mock/osf/Git1378.java | 16 -- testing/src/test/java/mock/osf/Git2755.java | 26 ---- testing/src/test/java/mock/osf/JUnitish.java | 29 ++++ testing/src/test/java/mock/osf/NoOp.java | 19 ++- testing/src/test/java/suite/CliAware.java | 5 + testing/src/test/java/suite/Sanity.java | 33 ++++ .../src/test/resources/features/1.4.x.feature | 11 ++ 11 files changed, 210 insertions(+), 126 deletions(-) create mode 100644 testing/src/test/java/expect/Sane.java create mode 100644 testing/src/test/java/mock/MockAware.java delete mode 100644 testing/src/test/java/mock/osf/Git1377.java delete mode 100644 testing/src/test/java/mock/osf/Git1378.java delete mode 100644 testing/src/test/java/mock/osf/Git2755.java create mode 100644 testing/src/test/java/mock/osf/JUnitish.java create mode 100644 testing/src/test/java/suite/CliAware.java create mode 100644 testing/src/test/java/suite/Sanity.java create mode 100644 testing/src/test/resources/features/1.4.x.feature diff --git a/testing/src/test/java/expect/Sane.java b/testing/src/test/java/expect/Sane.java new file mode 100644 index 00000000..e2beffcf --- /dev/null +++ b/testing/src/test/java/expect/Sane.java @@ -0,0 +1,34 @@ +package expect; + +import java.util.ArrayList; +import java.util.List; +import mock.MockAware; +import mock.OpensearchSupportedFunctionality; +import mock.osf.JUnitish; + +public final class Sane implements MockAware { + private final List knownMocks = new ArrayList(); + @Override + public void run() { + boolean all = true; + int count = 0; + for (OpensearchSupportedFunctionality osf : this.knownMocks) { + if (osf instanceof JUnitish) { + count++; + for (Boolean b : ((JUnitish) osf).results.values()) { + all &= b; + } + } + } + if (count == 0) { + assert false : "No results found. Cannot meet any expectation without results."; + } + if (!all) { + assert false: "All tests did not meet expectations."; + } + } + @Override + public void mocks(List mocks) { + this.knownMocks.addAll(mocks); + } +} diff --git a/testing/src/test/java/mock/MockAware.java b/testing/src/test/java/mock/MockAware.java new file mode 100644 index 00000000..dacaa68d --- /dev/null +++ b/testing/src/test/java/mock/MockAware.java @@ -0,0 +1,7 @@ +package mock; + +import java.util.List; + +public interface MockAware extends Runnable { + public void mocks (List mocks); +} diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 8a270c79..321d24bb 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -5,7 +5,10 @@ import io.javalin.http.HandlerType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -16,46 +19,63 @@ import mock.annotation.Replace; public final class OpensearchEngine { - private record MethodTarget(Object instance, Method method) {} + private record MethodTarget(Object instance, Method method) { + } + private final Map redirect = new ConcurrentHashMap<>(); private Javalin app; + /** * Scans the composite for @mock.annotation.replace methods and updates the redirect map. */ public void add(@Nonnull OpensearchSupportedFunctionality composite) { - // @Nonnull handles static/IDE analysis. - // Objects.requireNonNull(composite) can be added here if strict runtime enforcement is required. - - Method[] methods = composite.getClass().getDeclaredMethods(); - - Arrays.stream(methods) - .filter(method -> method.isAnnotationPresent(Replace.class)) - .forEach(method -> { - // Ensure private/protected annotated methods can be invoked - method.setAccessible(true); - - // Map the method name to its execution target - redirect.put(method.getName(), new MethodTarget(composite, method)); - }); + Map interfaceMethods = + Arrays.stream(OpensearchSupportedFunctionality.class.getMethods()) + .collect(Collectors.toMap(Method::getName, m -> m)); + + // Walk the class hierarchy top (most ancestral, excluding Object) to bottom (composite's own + // class). + List> hierarchy = new ArrayList<>(); + for (Class c = composite.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { + hierarchy.add(c); + } + Collections.reverse(hierarchy); + for (Class clazz : hierarchy) { + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Replace.class)) { + Method interfaceMethod = interfaceMethods.get(method.getName()); + if (interfaceMethod == null + || !Arrays.equals(interfaceMethod.getParameterTypes(), method.getParameterTypes())) { + continue; // @Replace on something that isn't an interface override — not our concern + } + method.setAccessible(true); + redirect.put(method.getName(), new MethodTarget(composite, method)); + } + } + } + } + + /** + * For cucumber to signal the beginning or ending of a scenerio by clearing all of the test functions. + */ + public void clear() { + this.redirect.clear(); } - + /** - * Converts HTTP paths into safe, matching Java method names. - * Examples: - * GET / -> getRoot - * POST /_bulk -> postBulk - * POST /my-index/_search -> postMyIndexSearch + * Converts HTTP paths into safe, matching Java method names. Examples: GET / -> getRoot POST + * /_bulk -> postBulk POST /my-index/_search -> postMyIndexSearch */ private String determineMethodName(String method, String path) { - String sanitizedPath = path.replaceAll("[^a-zA-Z0-9/]", ""); - if (sanitizedPath.equals("/") || path.isEmpty()) { - return method.toLowerCase() + "Root"; - } - String camelCasePath = Arrays.stream(sanitizedPath.split("/")) - .filter(segment -> !segment.isEmpty()) + String sanitizedPath = path.replaceAll("[^a-zA-Z0-9/]", ""); + if (sanitizedPath.equals("/") || path.isEmpty()) { + return method.toLowerCase() + "Root"; + } + String camelCasePath = + Arrays.stream(sanitizedPath.split("/")).filter(segment -> !segment.isEmpty()) .map(segment -> Character.toUpperCase(segment.charAt(0)) + segment.substring(1)) - .collect(Collectors.joining()); - return method.toLowerCase() + camelCasePath; + .collect(Collectors.joining()); + return method.toLowerCase() + camelCasePath; } /** @@ -64,21 +84,19 @@ private String determineMethodName(String method, String path) { public void start(int port) { this.app = Javalin.create(config -> { Handler catchAll = ctx -> { - Context facadeContext = new Context( - ctx.body(), - ctx.headerMap(), - ctx.queryParamMap().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), - ctx.pathParamMap() - ); + Context facadeContext = + new Context(ctx.body(), ctx.headerMap(), + ctx.queryParamMap().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), + ctx.pathParamMap()); String targetMethodName = determineMethodName(ctx.method().name(), ctx.path()); - sendResponse (ctx, process (targetMethodName, facadeContext)); + sendResponse(ctx, process(targetMethodName, facadeContext)); }; - for (HandlerType method : new HandlerType[]{ - HandlerType.GET, HandlerType.POST, HandlerType.PUT, - HandlerType.PATCH, HandlerType.DELETE, HandlerType.HEAD, HandlerType.OPTIONS}) { + for (HandlerType method : new HandlerType[] {HandlerType.GET, HandlerType.POST, + HandlerType.PUT, HandlerType.PATCH, HandlerType.DELETE, HandlerType.HEAD, + HandlerType.OPTIONS}) { config.routes.addHttpHandler(method, "/*", catchAll); } }); @@ -86,37 +104,39 @@ public void start(int port) { } /** - * Resolves the target interface method by climbing the profile's class hierarchy, - * respecting your explicit @Replace annotation policies. + * Resolves the target interface method by climbing the profile's class hierarchy, respecting your + * explicit @Replace annotation policies. */ private Response process(String methodName, Context context) { - MethodTarget target = redirect.get(methodName); + MethodTarget target = redirect.get(methodName); - if (target == null) { - return new Response(501, - "{\"error\": \"Method '" + methodName + "' not found in active profile hierarchy.\"}", - "application/json"); - } + if (target == null) { + return new Response(501, + "{\"error\": \"Method '" + methodName + "' not found in active profile hierarchy.\"}", + "application/json"); + } - try { - return (Response) target.method().invoke(target.instance(), context); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - return new Response(500, "{\"error\": \"Mock runtime error: " + cause.getMessage() + "\"}", "application/json"); - } catch (IllegalAccessException e) { - return new Response(500, "{\"error\": \"Security constraint executing mock method\"}", "application/json"); - } + try { + return (Response) target.method().invoke(target.instance(), context); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + return new Response(500, "{\"error\": \"Mock runtime error: " + cause.getMessage() + "\"}", + "application/json"); + } catch (IllegalAccessException e) { + return new Response(500, "{\"error\": \"Security constraint executing mock method\"}", + "application/json"); + } } private void sendResponse(io.javalin.http.Context ctx, Response response) { - ctx.status(response.statusCode()); - ctx.contentType(response.contentType()); - ctx.result(response.body()); + ctx.status(response.statusCode()); + ctx.contentType(response.contentType()); + ctx.result(response.body()); } public void stop() { - if (this.app != null) { - this.app.stop(); - } + if (this.app != null) { + this.app.stop(); + } } } diff --git a/testing/src/test/java/mock/osf/Git1377.java b/testing/src/test/java/mock/osf/Git1377.java deleted file mode 100644 index 63833188..00000000 --- a/testing/src/test/java/mock/osf/Git1377.java +++ /dev/null @@ -1,12 +0,0 @@ -package mock.osf; - -import java.util.List; -import mock.annotation.Replace; - -public class Git1377 extends NoOp { - @Replace - @Override - public int bar(List justSomeArg) { - return justSomeArg.size(); - } -} diff --git a/testing/src/test/java/mock/osf/Git1378.java b/testing/src/test/java/mock/osf/Git1378.java deleted file mode 100644 index 27e89c62..00000000 --- a/testing/src/test/java/mock/osf/Git1378.java +++ /dev/null @@ -1,16 +0,0 @@ -package mock.osf; - -import mock.NoOpException; -import mock.annotation.Replace; - -public class Git1378 extends NoOp { - @Replace - @Override - public void fu() { - } - @Replace - @Override - public Object snafu() { - return new NoOpException("Placeholder"); - } -} diff --git a/testing/src/test/java/mock/osf/Git2755.java b/testing/src/test/java/mock/osf/Git2755.java deleted file mode 100644 index 35ce10d3..00000000 --- a/testing/src/test/java/mock/osf/Git2755.java +++ /dev/null @@ -1,26 +0,0 @@ -package mock.osf; - -import java.util.List; -import mock.OpensearchSupportedFunctionality; -import mock.annotation.Replace; - -public class Git2755 implements OpensearchSupportedFunctionality { - // this class is equivalent to mock.osf.Git1377,mock.osf.Git1378 in the feature file - private OpensearchSupportedFunctionality a = new Git1377(); - private OpensearchSupportedFunctionality b = new Git1378(); - @Replace - @Override - public void fu() { - b.fu(); - } - @Replace - @Override - public int bar(List justSomeArg) { - return a.bar(justSomeArg); - } - @Replace - @Override - public Object snafu() { - return b.snafu(); - } -} diff --git a/testing/src/test/java/mock/osf/JUnitish.java b/testing/src/test/java/mock/osf/JUnitish.java new file mode 100644 index 00000000..3ca82aaf --- /dev/null +++ b/testing/src/test/java/mock/osf/JUnitish.java @@ -0,0 +1,29 @@ +package mock.osf; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import org.junit.jupiter.api.Test; +import mock.OpensearchSupportedFunctionality; + +public final class JUnitish extends NoOp implements OpensearchSupportedFunctionality { + public final HashMap results = new HashMap(); + public void runTests(Object from) { + int count = 0; + for (Method method : from.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(Test.class)) { + try { + count++; + method.invoke(from); + this.results.put(method.getName(), true); + } catch (AssertionError | IllegalAccessException | InvocationTargetException e) { + // FIXME: print/log e nicely + this.results.put(method.getName(), false); + } + } + } + if (count == 0) { + results.put("found tests", false); + } + } +} diff --git a/testing/src/test/java/mock/osf/NoOp.java b/testing/src/test/java/mock/osf/NoOp.java index 81c408a9..3eec9825 100644 --- a/testing/src/test/java/mock/osf/NoOp.java +++ b/testing/src/test/java/mock/osf/NoOp.java @@ -1,20 +1,19 @@ package mock.osf; -import java.util.List; +import java.lang.invoke.MethodHandles; import mock.NoOpException; import mock.OpensearchSupportedFunctionality; public abstract class NoOp implements OpensearchSupportedFunctionality { - @Override - public void fu() { - throw new NoOpException("Placeholder"); + private Response placeholder() { + String methodName = new Throwable().getStackTrace()[1].getMethodName(); + String className = MethodHandles.lookup().lookupClass().getSimpleName(); + throw new NoOpException("Placeholder: " + className + "." + methodName + "()"); } @Override - public int bar(List justSomeArg) { - throw new NoOpException("Placeholder"); - } + public Response authorize(Context ctx) { return placeholder(); } @Override - public Object snafu() { - throw new NoOpException("Placeholder"); - } + public Response getRoot(Context ctx) { return placeholder(); } + @Override + public Response postBulk(Context ctx) { return placeholder(); } } diff --git a/testing/src/test/java/suite/CliAware.java b/testing/src/test/java/suite/CliAware.java new file mode 100644 index 00000000..8878025a --- /dev/null +++ b/testing/src/test/java/suite/CliAware.java @@ -0,0 +1,5 @@ +package suite; + +public interface CliAware extends Runnable { + public void arguments(String args); +} diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java new file mode 100644 index 00000000..a2d30a82 --- /dev/null +++ b/testing/src/test/java/suite/Sanity.java @@ -0,0 +1,33 @@ +package suite; + +import java.util.ArrayList; +import java.util.List; +import mock.MockAware; +import mock.OpensearchSupportedFunctionality; +import mock.osf.JUnitish; + +public final class Sanity implements MockAware { + private final List knownMocks = new ArrayList(); + @Override + public void run() { + for (OpensearchSupportedFunctionality osf : this.knownMocks) { + if (osf instanceof JUnitish) { + ((JUnitish) osf).runTests(this); + } + } + } + @Override + public void mocks(List mocks) { + this.knownMocks.addAll(mocks); + } + + @Test + public void test_authorize() { + assert false; + } + + @Test + public void test_root() { + assert true; + } +} diff --git a/testing/src/test/resources/features/1.4.x.feature b/testing/src/test/resources/features/1.4.x.feature new file mode 100644 index 00000000..97cdca02 --- /dev/null +++ b/testing/src/test/resources/features/1.4.x.feature @@ -0,0 +1,11 @@ +Feature: 1.4.x + Scenario Outline: NASA-PDS/registry-loader#- + Given registry-loader issue , test , opensearch mocks , test suite , CLI arguments , expectation + When test suite executed + Then compared to the expected outcome . + @1.4.x + Examples: + | issueNumber | subtest | mocks | suite | args | expectation | + | 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | + +| 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | From a9d4aab7c067d88a46d9de4c5cf7bf619c1b38fd Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Sun, 9 Aug 2026 14:55:29 -0700 Subject: [PATCH 05/24] should have cucumber set up and ready --- src/test/resources/junit-platform.properties | 5 ++ testing/pom.xml | 24 +++++- .../src/test/java/cucumber/CucumberTest.java | 12 +++ .../test/java/cucumber/SingleScenerio.java | 54 +++++++++++++ testing/src/test/java/cucumber/StepDefs.java | 79 +++++++++++++++++++ testing/src/test/java/suite/Sanity.java | 1 + .../src/test/resources/features/1.4.x.feature | 12 +-- 7 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/junit-platform.properties create mode 100644 testing/src/test/java/cucumber/CucumberTest.java create mode 100644 testing/src/test/java/cucumber/SingleScenerio.java create mode 100644 testing/src/test/java/cucumber/StepDefs.java diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 00000000..bcb0125c --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,5 @@ +cucumber.features=classpath:features/ +cucumber.glue=cucumber +cucumber.plugin=pretty,summary,html:target/cucumber.html +cucumber.publish.quiet=true + diff --git a/testing/pom.xml b/testing/pom.xml index 10c5f353..034c92f1 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -68,7 +68,13 @@ POSSIBILITY OF SUCH DAMAGE. - + + io.cucumber + cucumber-bom + 7.34.6 + pom + import + io.javalin javalin-bom @@ -80,7 +86,21 @@ POSSIBILITY OF SUCH DAMAGE. - + + io.cucumber + cucumber-java + test + + + io.cucumber + cucumber-junit-platform-engine + test + + + org.junit.platform + junit-platform-suite-api + test + io.javalin javalin diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java new file mode 100644 index 00000000..34771f96 --- /dev/null +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -0,0 +1,12 @@ +package cucumber; + +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.Suite; + +@Suite +@IncludeEngines("cucumber") + +public class CucumberTest { + public CucumberTest() {} +} + diff --git a/testing/src/test/java/cucumber/SingleScenerio.java b/testing/src/test/java/cucumber/SingleScenerio.java new file mode 100644 index 00000000..55d25160 --- /dev/null +++ b/testing/src/test/java/cucumber/SingleScenerio.java @@ -0,0 +1,54 @@ +package cucumber; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class SingleScenerio { + public static void main(String[] args) throws NumberFormatException, IOException { + if (args.length > 2 || args.length == 0) { + System.out.println ("usage: "); + System.out.println (" is the first column in the feature file like 1066"); + System.out.println (" is the second column and should be omitted if there are no subtests for the "); + return; + } + + for (File file : Paths.get(TestConstants.TEST_DATA_DIR, "features").toFile().listFiles((dir, name) -> name.endsWith(".feature"))) { + for (String line : Files.readAllLines(file.toPath())) { + line = line.strip(); + if (line.startsWith("|")) { + String[] scenerio = line.split("\\|"); + if (scenerio[1].strip().equals(args[0])) { + if (args.length == 1 && !scenerio[2].strip().isBlank()) { + System.out.println ("Scenerio " + args[0] + " requires a subtest value too."); + return; + } + if (args.length == 2 && scenerio[2].strip().isBlank()) { + System.out.println ("Scenerio " + args[0] + " does not require a subtest value."); + return; + } + if (args.length == 2 && !args[1].equals(scenerio[2].strip())) continue; + StepDefs engine = new StepDefs(); + System.out.println("an_and"); + engine.an_and( + Integer.valueOf(scenerio[1].strip()), + args.length == 1 ? null : Integer.valueOf(scenerio[2].strip()), + scenerio[3].strip().substring(1, scenerio[3].strip().length()-1)); + System.out.println("execute_validate"); + engine.execute_validate (scenerio[4].strip().substring(1,scenerio[4].strip().length()-1)); + System.out.println ("compare_to_the"); + engine.compare_to_the(scenerio[5].strip().isBlank() ? "" : + scenerio[5].strip().substring(1, scenerio[5].strip().length()-1)); + System.out.println ("success"); + return; + } + } + } + } + System.out.print ("Could not find issue number " + args[0]); + if (args.length == 2) System.out.print (" and subtest " + args[1]); + System.out.println(); + return; + } +} diff --git a/testing/src/test/java/cucumber/StepDefs.java b/testing/src/test/java/cucumber/StepDefs.java new file mode 100644 index 00000000..6c34b69f --- /dev/null +++ b/testing/src/test/java/cucumber/StepDefs.java @@ -0,0 +1,79 @@ +package cucumber; + +import java.util.ArrayList; +import io.cucumber.java.After; +import io.cucumber.java.AfterAll; +import io.cucumber.java.BeforeAll; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import mock.MockAware; +import mock.OpensearchEngine; +import mock.OpensearchSupportedFunctionality; +import suite.CliAware; + +public class StepDefs { + private final ArrayList mocks = new ArrayList(); + private final OpensearchEngine motor = new OpensearchEngine(); + + @After + public void reset() { + this.mocks.clear(); + this.motor.clear(); + } + + @AfterAll + public void stop() { + motor.stop(); + } + + @BeforeAll + public void start() { + motor.start(19022); + } + + @Given("registry-loader issue {int}, test {int}, and opensearch mocks {string}") + public void construct(Integer issueNumber, Integer count, String mocks) { + for (String mock : mocks.split(",")) { + this.mocks.add((OpensearchSupportedFunctionality)classForName(mock)); + } + } + + @When("test suite {string} is executed with CLI arguments {string}") + public void execute(String suite, String cliargline) { + Object obj = classForName(suite); + if (obj instanceof CliAware) { + ((CliAware)obj).arguments(cliargline); + } else if (cliargline != null && !cliargline.isBlank()) { + throw new IllegalStateException("An argument line was given to the suite " + suite + " that is not suite.CliAware"); + } + if (obj instanceof Runnable) { + ((Runnable)obj).run(); + } else { + throw new IllegalStateException("The suite " + suite + " is not Runnable."); + } + } + + @Then("compare to the expected outcome {string}.") + public void compare(String expectation) { + Object obj = this.classForName(expectation); + if (obj instanceof Runnable) { + ((Runnable)obj).run(); + } + } + + private Object classForName(String className) { + try { + Class clazz = Class.forName(className.trim()); + Object obj = clazz.getDeclaredConstructor().newInstance(); + if (obj instanceof MockAware) { + ((MockAware)obj).mocks(this.mocks); + } + return obj; + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to instantiate mock class '" + className + "' — check spelling/package in the feature file's mocks column.", e + ); + } + } +} diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index a2d30a82..acff23a1 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Test; import mock.MockAware; import mock.OpensearchSupportedFunctionality; import mock.osf.JUnitish; diff --git a/testing/src/test/resources/features/1.4.x.feature b/testing/src/test/resources/features/1.4.x.feature index 97cdca02..69627f33 100644 --- a/testing/src/test/resources/features/1.4.x.feature +++ b/testing/src/test/resources/features/1.4.x.feature @@ -1,11 +1,11 @@ Feature: 1.4.x Scenario Outline: NASA-PDS/registry-loader#- - Given registry-loader issue , test , opensearch mocks , test suite , CLI arguments , expectation - When test suite executed - Then compared to the expected outcome . + Given registry-loader issue , test , and opensearch mocks + When test suite is executed with CLI arguments + Then compare to the expected outcome . @1.4.x Examples: - | issueNumber | subtest | mocks | suite | args | expectation | - | 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | + | issueNumber | subtest | mocks | suite | cliargline | expectation | + | 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | -| 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | +| 139 | 0 | "mocks.osf.Standard,mocks.osf.JUnitish" | "suites.Sanity" | "" | "expect.Sane" | From 7d2007c63c5905d9b571ce27497aaa609c01bd7f Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 10:16:44 -0700 Subject: [PATCH 06/24] working cucumber --- testing/pom.xml | 8 ++++ testing/src/test/java/cucumber/StepDefs.java | 47 ++++++++----------- testing/src/test/java/mock/osf/Standard.java | 5 ++ testing/src/test/java/suite/Sanity.java | 1 + .../src/test/resources/features/1.4.x.feature | 4 +- 5 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 testing/src/test/java/mock/osf/Standard.java diff --git a/testing/pom.xml b/testing/pom.xml index 034c92f1..69fc0f25 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -130,6 +130,14 @@ POSSIBILITY OF SUCH DAMAGE. false + + org.apache.maven.plugins + maven-surefire-plugin + + true + false + + diff --git a/testing/src/test/java/cucumber/StepDefs.java b/testing/src/test/java/cucumber/StepDefs.java index 6c34b69f..650d35b6 100644 --- a/testing/src/test/java/cucumber/StepDefs.java +++ b/testing/src/test/java/cucumber/StepDefs.java @@ -1,8 +1,8 @@ package cucumber; import java.util.ArrayList; -import io.cucumber.java.After; import io.cucumber.java.AfterAll; +import io.cucumber.java.Before; import io.cucumber.java.BeforeAll; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; @@ -14,62 +14,55 @@ public class StepDefs { private final ArrayList mocks = new ArrayList(); - private final OpensearchEngine motor = new OpensearchEngine(); + private static final OpensearchEngine motor = new OpensearchEngine(); - @After - public void reset() { - this.mocks.clear(); - this.motor.clear(); + @AfterAll + public static void stop() { + StepDefs.motor.stop(); } - @AfterAll - public void stop() { - motor.stop(); + @Before + public void reset() { + StepDefs.motor.clear(); } @BeforeAll - public void start() { - motor.start(19022); + public static void start() { + StepDefs.motor.start(19022); } @Given("registry-loader issue {int}, test {int}, and opensearch mocks {string}") public void construct(Integer issueNumber, Integer count, String mocks) { for (String mock : mocks.split(",")) { - this.mocks.add((OpensearchSupportedFunctionality)classForName(mock)); + this.mocks.add(classForName(mock)); } } @When("test suite {string} is executed with CLI arguments {string}") public void execute(String suite, String cliargline) { - Object obj = classForName(suite); - if (obj instanceof CliAware) { - ((CliAware)obj).arguments(cliargline); + Runnable task = this.classForName(suite); + if (task instanceof CliAware) { + ((CliAware)task).arguments(cliargline); } else if (cliargline != null && !cliargline.isBlank()) { throw new IllegalStateException("An argument line was given to the suite " + suite + " that is not suite.CliAware"); } - if (obj instanceof Runnable) { - ((Runnable)obj).run(); - } else { - throw new IllegalStateException("The suite " + suite + " is not Runnable."); - } + task.run(); } @Then("compare to the expected outcome {string}.") public void compare(String expectation) { - Object obj = this.classForName(expectation); - if (obj instanceof Runnable) { - ((Runnable)obj).run(); - } + this.classForName(expectation).run(); } - - private Object classForName(String className) { + + @SuppressWarnings("unchecked") + private T classForName(String className) { try { Class clazz = Class.forName(className.trim()); Object obj = clazz.getDeclaredConstructor().newInstance(); if (obj instanceof MockAware) { ((MockAware)obj).mocks(this.mocks); } - return obj; + return (T)obj; } catch (ReflectiveOperationException e) { throw new IllegalStateException( "Failed to instantiate mock class '" + className + "' — check spelling/package in the feature file's mocks column.", e diff --git a/testing/src/test/java/mock/osf/Standard.java b/testing/src/test/java/mock/osf/Standard.java new file mode 100644 index 00000000..7195cdbf --- /dev/null +++ b/testing/src/test/java/mock/osf/Standard.java @@ -0,0 +1,5 @@ +package mock.osf; + +public class Standard extends NoOp { + +} diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index acff23a1..87ea8494 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -17,6 +17,7 @@ public void run() { } } } + @Override public void mocks(List mocks) { this.knownMocks.addAll(mocks); diff --git a/testing/src/test/resources/features/1.4.x.feature b/testing/src/test/resources/features/1.4.x.feature index 69627f33..31484fad 100644 --- a/testing/src/test/resources/features/1.4.x.feature +++ b/testing/src/test/resources/features/1.4.x.feature @@ -6,6 +6,6 @@ Feature: 1.4.x @1.4.x Examples: | issueNumber | subtest | mocks | suite | cliargline | expectation | - | 139 | 0 | "mocks.osf.Standard" | "suites.Sanity" | "" | "expect.Sane" | +# | 139 | 0 | "mock.osf.Standard" | "suite.Sanity" | "" | "expect.Sane" | -| 139 | 0 | "mocks.osf.Standard,mocks.osf.JUnitish" | "suites.Sanity" | "" | "expect.Sane" | +| 139 | 0 | "mock.osf.Standard,mock.osf.JUnitish" | "suite.Sanity" | "" | "expect.Sane" | From c3e83bac91b39203c25b5340d427fd6fa380f92d Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 10:21:13 -0700 Subject: [PATCH 07/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java index 34771f96..44605728 100644 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -7,6 +7,10 @@ @IncludeEngines("cucumber") public class CucumberTest { - public CucumberTest() {} + public CucumberTest() { + // this function is here to make cucumber happy + // these comments are here to make sonarcube happy + // both are useless to humans + } } From 11813ef52a2aac2123370f8bb333a998ba6c314e Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 10:24:02 -0700 Subject: [PATCH 08/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java index 44605728..ae7ec56a 100644 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -8,9 +8,11 @@ public class CucumberTest { public CucumberTest() { - // this function is here to make cucumber happy - // these comments are here to make sonarcube happy - // both are useless to humans + /* + * this function is here to make cucumber happy + * these comments are here to make sonarcube happy + * both are useless to humans + */ } } From e5a722f4d0a90dbb95e3a4b76b048e993ab09559 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 10:29:02 -0700 Subject: [PATCH 09/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java index ae7ec56a..f9bedd3f 100644 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -9,9 +9,10 @@ public class CucumberTest { public CucumberTest() { /* - * this function is here to make cucumber happy - * these comments are here to make sonarcube happy - * both are useless to humans + * intentionally empty: + * this function is here to make cucumber happy + * these comments are here to make sonarqube happy + * both are useless to humans */ } } From 0c895e97be29dcc08b347869cc2c94a5bed39fc3 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 10:34:35 -0700 Subject: [PATCH 10/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java index f9bedd3f..b3500754 100644 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -9,7 +9,7 @@ public class CucumberTest { public CucumberTest() { /* - * intentionally empty: + * Intentionally empty; * this function is here to make cucumber happy * these comments are here to make sonarqube happy * both are useless to humans From 69de696ea7d71b069cba1506f9a00518c44fff86 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 11:01:26 -0700 Subject: [PATCH 11/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java index b3500754..1bc500c7 100644 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ b/testing/src/test/java/cucumber/CucumberTest.java @@ -7,13 +7,5 @@ @IncludeEngines("cucumber") public class CucumberTest { - public CucumberTest() { - /* - * Intentionally empty; - * this function is here to make cucumber happy - * these comments are here to make sonarqube happy - * both are useless to humans - */ - } } From 0cc1624822889d51009a2c531b4a2ad9cfbbfebd Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 11:03:56 -0700 Subject: [PATCH 12/24] make sonar happy --- testing/src/test/java/cucumber/CucumberTest.java | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 testing/src/test/java/cucumber/CucumberTest.java diff --git a/testing/src/test/java/cucumber/CucumberTest.java b/testing/src/test/java/cucumber/CucumberTest.java deleted file mode 100644 index 1bc500c7..00000000 --- a/testing/src/test/java/cucumber/CucumberTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package cucumber; - -import org.junit.platform.suite.api.IncludeEngines; -import org.junit.platform.suite.api.Suite; - -@Suite -@IncludeEngines("cucumber") - -public class CucumberTest { -} - From 87049277263c40028f2e4b17d73b633c32b461f7 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 13:36:35 -0700 Subject: [PATCH 13/24] good enough - now need to implement tests --- testing/pom.xml | 5 +++++ testing/src/test/java/expect/Sane.java | 16 ++++++++++----- .../src/test/java/mock/OpensearchEngine.java | 11 +++++++--- testing/src/test/java/mock/osf/JUnitish.java | 20 ++++++++++++++++++- testing/src/test/java/suite/Sanity.java | 2 +- .../test/resources/junit-platform.properties | 0 6 files changed, 44 insertions(+), 10 deletions(-) rename {src => testing/src}/test/resources/junit-platform.properties (100%) diff --git a/testing/pom.xml b/testing/pom.xml index 69fc0f25..5cec48e7 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -110,6 +110,11 @@ POSSIBILITY OF SUCH DAMAGE. jakarta.annotation-api 3.0.0 + + org.slf4j + slf4j-simple + 2.0.16 + diff --git a/testing/src/test/java/expect/Sane.java b/testing/src/test/java/expect/Sane.java index e2beffcf..a4490dbe 100644 --- a/testing/src/test/java/expect/Sane.java +++ b/testing/src/test/java/expect/Sane.java @@ -10,21 +10,27 @@ public final class Sane implements MockAware { private final List knownMocks = new ArrayList(); @Override public void run() { - boolean all = true; int count = 0; + int failed = 0; + int passed = 0; for (OpensearchSupportedFunctionality osf : this.knownMocks) { if (osf instanceof JUnitish) { count++; for (Boolean b : ((JUnitish) osf).results.values()) { - all &= b; + if (b) { + passed++; + } else { + failed++; + } } } } if (count == 0) { assert false : "No results found. Cannot meet any expectation without results."; - } - if (!all) { - assert false: "All tests did not meet expectations."; + } else { + if (failed > 0 || passed == 0) { + assert false: passed + " tests passed but " + failed + " tests failed"; + } } } @Override diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 321d24bb..06f14cd3 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -12,6 +12,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jakarta.annotation.Nonnull; import mock.OpensearchSupportedFunctionality.Context; @@ -22,6 +24,7 @@ public final class OpensearchEngine { private record MethodTarget(Object instance, Method method) { } + private final Logger log = LoggerFactory.getLogger(OpensearchEngine.class); private final Map redirect = new ConcurrentHashMap<>(); private Javalin app; @@ -46,7 +49,8 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { Method interfaceMethod = interfaceMethods.get(method.getName()); if (interfaceMethod == null || !Arrays.equals(interfaceMethod.getParameterTypes(), method.getParameterTypes())) { - continue; // @Replace on something that isn't an interface override — not our concern + log.warn("Method {} in class {} is not a valid override of OpensearchSupportedFunctionality", method.getName(), composite.getClass().getName()); + continue; } method.setAccessible(true); redirect.put(method.getName(), new MethodTarget(composite, method)); @@ -119,10 +123,11 @@ private Response process(String methodName, Context context) { try { return (Response) target.method().invoke(target.instance(), context); } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - return new Response(500, "{\"error\": \"Mock runtime error: " + cause.getMessage() + "\"}", + log.error("Invocation problem processing the testing request", e); + return new Response(500, "{\"error\": \"Mock runtime error: " + e.getCause().getMessage() + "\"}", "application/json"); } catch (IllegalAccessException e) { + log.error("Access is to testing functioon is wrong", e); return new Response(500, "{\"error\": \"Security constraint executing mock method\"}", "application/json"); } diff --git a/testing/src/test/java/mock/osf/JUnitish.java b/testing/src/test/java/mock/osf/JUnitish.java index 3ca82aaf..7b714cc1 100644 --- a/testing/src/test/java/mock/osf/JUnitish.java +++ b/testing/src/test/java/mock/osf/JUnitish.java @@ -4,10 +4,13 @@ import java.lang.reflect.Method; import java.util.HashMap; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import mock.OpensearchSupportedFunctionality; public final class JUnitish extends NoOp implements OpensearchSupportedFunctionality { public final HashMap results = new HashMap(); + private final Logger log = LoggerFactory.getLogger(JUnitish.class); public void runTests(Object from) { int count = 0; for (Method method : from.getClass().getDeclaredMethods()) { @@ -17,7 +20,22 @@ public void runTests(Object from) { method.invoke(from); this.results.put(method.getName(), true); } catch (AssertionError | IllegalAccessException | InvocationTargetException e) { - // FIXME: print/log e nicely + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + if (t instanceof AssertionError) { + StackTraceElement element = java.util.Arrays.stream(t.getStackTrace()) + .findFirst() + .orElse(null); + + log.warn((element != null) + ? String.format("assertion failed at %s.%s():%d - {}", + element.getClassName(), element.getMethodName(), element.getLineNumber()) + : "assertion failed at unknown source - {}", t.getMessage()); + } else { + log.error("Test failed due to implementation error", t); + } this.results.put(method.getName(), false); } } diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index 87ea8494..b20e4f90 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -25,7 +25,7 @@ public void mocks(List mocks) { @Test public void test_authorize() { - assert false; + assert false: "implement me"; } @Test diff --git a/src/test/resources/junit-platform.properties b/testing/src/test/resources/junit-platform.properties similarity index 100% rename from src/test/resources/junit-platform.properties rename to testing/src/test/resources/junit-platform.properties From ccd6f0cc52be9b7dd23ebb23d3da6bc6b04e9041 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Tue, 11 Aug 2026 15:08:56 -0700 Subject: [PATCH 14/24] adding in ssl --- testing/pom.xml | 4 ++++ testing/src/test/java/mock/OpensearchEngine.java | 15 +++++++++++++++ testing/src/test/java/suite/Sanity.java | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/testing/pom.xml b/testing/pom.xml index 5cec48e7..2cba6b77 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -105,6 +105,10 @@ POSSIBILITY OF SUCH DAMAGE. io.javalin javalin + + io.javalin.community.ssl + javalin-ssl + jakarta.annotation jakarta.annotation-api diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 06f14cd3..49974c4c 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -1,6 +1,7 @@ package mock; import io.javalin.Javalin; +import io.javalin.community.ssl.SslPlugin; import io.javalin.http.Handler; import io.javalin.http.HandlerType; import java.lang.reflect.InvocationTargetException; @@ -87,6 +88,12 @@ private String determineMethodName(String method, String path) { */ public void start(int port) { this.app = Javalin.create(config -> { + config.registerPlugin(new SslPlugin(ssl -> { + ssl.host = "0.0.0.0"; + ssl.securePort = port; + ssl.pemFromPath("/path/to/my.pem", null); + })); + Handler catchAll = ctx -> { Context facadeContext = new Context(ctx.body(), ctx.headerMap(), @@ -94,6 +101,7 @@ public void start(int port) { .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), ctx.pathParamMap()); + System.out.println(ctx); String targetMethodName = determineMethodName(ctx.method().name(), ctx.path()); sendResponse(ctx, process(targetMethodName, facadeContext)); }; @@ -144,4 +152,11 @@ public void stop() { this.app.stop(); } } + + public static void main(String argv[]) throws InterruptedException { + OpensearchEngine me = new OpensearchEngine(); + me.start(9200); + Thread.sleep(1000*1000); + me.stop(); + } } diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index b20e4f90..6dbbb056 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -30,6 +30,6 @@ public void test_authorize() { @Test public void test_root() { - assert true; + assert false: "implement me"; } } From b613b219e489208a318833b3aa9c9b94524619a9 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Thu, 13 Aug 2026 15:07:29 -0700 Subject: [PATCH 15/24] the basic tests have begun --- .gitignore | 1 - testing/pom.xml | 6 + .../src/test/java/mock/OpensearchEngine.java | 123 +++++++++++++----- .../OpensearchSupportedFunctionality.java | 11 +- testing/src/test/java/mock/osf/NoOp.java | 4 +- testing/src/test/java/mock/osf/Standard.java | 7 +- 6 files changed, 109 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index b5b2349b..b99c8390 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,3 @@ target/ # macOS specific stuff .DS_Store ._* - diff --git a/testing/pom.xml b/testing/pom.xml index 2cba6b77..b5e19590 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -86,6 +86,12 @@ POSSIBILITY OF SUCH DAMAGE. + + com.fasterxml.jackson.core + jackson-databind + 2.22.1 + compile + io.cucumber cucumber-java diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 49974c4c..28144322 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -1,32 +1,36 @@ package mock; -import io.javalin.Javalin; -import io.javalin.community.ssl.SslPlugin; -import io.javalin.http.Handler; -import io.javalin.http.HandlerType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.javalin.Javalin; +import io.javalin.community.ssl.SslPlugin; +import io.javalin.http.Handler; +import io.javalin.http.HandlerType; import jakarta.annotation.Nonnull; - import mock.OpensearchSupportedFunctionality.Context; import mock.OpensearchSupportedFunctionality.Response; import mock.annotation.Replace; +import mock.osf.Standard; public final class OpensearchEngine { private record MethodTarget(Object instance, Method method) { } - private final Logger log = LoggerFactory.getLogger(OpensearchEngine.class); private final Map redirect = new ConcurrentHashMap<>(); + private final ObjectMapper mapper = new ObjectMapper(); private Javalin app; /** @@ -50,7 +54,9 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { Method interfaceMethod = interfaceMethods.get(method.getName()); if (interfaceMethod == null || !Arrays.equals(interfaceMethod.getParameterTypes(), method.getParameterTypes())) { - log.warn("Method {} in class {} is not a valid override of OpensearchSupportedFunctionality", method.getName(), composite.getClass().getName()); + log.warn( + "Method {} in class {} is not a valid override of OpensearchSupportedFunctionality", + method.getName(), composite.getClass().getName()); continue; } method.setAccessible(true); @@ -61,12 +67,35 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { } /** - * For cucumber to signal the beginning or ending of a scenerio by clearing all of the test functions. + * For cucumber to signal the beginning or ending of a scenerio by clearing all of the test + * functions. */ public void clear() { this.redirect.clear(); } + /** + * Same as decodeTopLevel, but tolerant of bodies that aren't a JSON object at all (not valid + * JSON, or a JSON array/scalar instead of an object). Returns empty map in that case instead of + * throwing, so callers on a mixed-content-type pipeline can call this unconditionally. + */ + private Map decodeTopLevel(String body) { + if (body == null || body.isBlank()) + return Map.of(); + try { + JsonNode root = this.mapper.readTree(body); + Map result = new LinkedHashMap<>(); + if (!root.isObject()) + return Map.of(); // not a JSON object at top level + for (Map.Entry e : root.properties()) { + result.put(e.getKey(), this.mapper.writeValueAsString(e.getValue())); + } + return result; + } catch (Exception e) { + return Map.of(); // not valid JSON at all + } + } + /** * Converts HTTP paths into safe, matching Java method names. Examples: GET / -> getRoot POST * /_bulk -> postBulk POST /my-index/_search -> postMyIndexSearch @@ -83,15 +112,53 @@ private String determineMethodName(String method, String path) { return method.toLowerCase() + camelCasePath; } + /** + * Resolves the target interface method by climbing the profile's class hierarchy, respecting your + * explicit @Replace annotation policies. + */ + private Response process(String methodName, Context context) { + final String registry = "Devregistrystructured"; + log.info("Method name: {}", methodName); + log.info("Context:"); + log.info(" body: {}", context.body()); + log.info(" hearder: {}", context.headers()); + log.info(" query: {}", context.queryParams()); + log.info(" params: {}", context.pathParams()); + if (methodName.endsWith(registry)) { + methodName = methodName.substring(0, methodName.length() - registry.length()); + Map endpoint = decodeTopLevel(context.body()); + for (String name : endpoint.keySet().stream().map(String::toLowerCase).sorted().toList()) { + methodName = methodName + Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + } + return subprocess(methodName, context); + } + + private void sendResponse(io.javalin.http.Context ctx, Response response) { + ctx.status(response.statusCode()); + ctx.contentType(response.contentType()); + ctx.result(response.body()); + } + /** * Spins up the fixed Javalin instance. Called once during Cucumber initialization. */ public void start(int port) { this.app = Javalin.create(config -> { config.registerPlugin(new SslPlugin(ssl -> { - ssl.host = "0.0.0.0"; + ssl.host = "127.0.0.1"; + ssl.insecure = false; ssl.securePort = port; - ssl.pemFromPath("/path/to/my.pem", null); + try { + Process process = new ProcessBuilder("sh", "-c", + "openssl req -x509 -newkey rsa:2048 -keyout /dev/stdout -out /dev/stdout -sha256 -days 1 -nodes -subj '/CN=localhost' -addext 'subjectAltName = DNS:localhost' 2>/dev/null") + .start(); + String openSslOutput = + new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + ssl.pemFromString(openSslOutput, openSslOutput); + } catch (Exception e) { + throw new RuntimeException("Failed to auto-generate localhost cert via openssl", e); + } })); Handler catchAll = ctx -> { @@ -100,9 +167,8 @@ public void start(int port) { ctx.queryParamMap().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), ctx.pathParamMap()); - - System.out.println(ctx); String targetMethodName = determineMethodName(ctx.method().name(), ctx.path()); + log.info("request path: {}", ctx.path()); sendResponse(ctx, process(targetMethodName, facadeContext)); }; @@ -112,14 +178,17 @@ public void start(int port) { config.routes.addHttpHandler(method, "/*", catchAll); } }); - this.app.start(port); + this.app.start(); } - /** - * Resolves the target interface method by climbing the profile's class hierarchy, respecting your - * explicit @Replace annotation policies. - */ - private Response process(String methodName, Context context) { + public void stop() { + if (this.app != null) { + this.app.stop(); + } + } + + private Response subprocess(String methodName, Context context) { + // have a more appropriate name for the endpoint and just its data (maybe) MethodTarget target = redirect.get(methodName); if (target == null) { @@ -132,7 +201,8 @@ private Response process(String methodName, Context context) { return (Response) target.method().invoke(target.instance(), context); } catch (InvocationTargetException e) { log.error("Invocation problem processing the testing request", e); - return new Response(500, "{\"error\": \"Mock runtime error: " + e.getCause().getMessage() + "\"}", + return new Response(500, + "{\"error\": \"Mock runtime error: " + e.getCause().getMessage() + "\"}", "application/json"); } catch (IllegalAccessException e) { log.error("Access is to testing functioon is wrong", e); @@ -141,22 +211,11 @@ private Response process(String methodName, Context context) { } } - private void sendResponse(io.javalin.http.Context ctx, Response response) { - ctx.status(response.statusCode()); - ctx.contentType(response.contentType()); - ctx.result(response.body()); - } - - public void stop() { - if (this.app != null) { - this.app.stop(); - } - } - public static void main(String argv[]) throws InterruptedException { OpensearchEngine me = new OpensearchEngine(); me.start(9200); - Thread.sleep(1000*1000); + me.add(new Standard()); + Thread.sleep(1000 * 1000); me.stop(); } } diff --git a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java index a373c56e..358660d5 100644 --- a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java +++ b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java @@ -4,10 +4,10 @@ public interface OpensearchSupportedFunctionality { public record Context( - String body, - Map headers, - Map queryParams, - Map pathParams) { + String body, // original headers sent to the socket + Map headers, // original headers sent to the socket + Map queryParams, // original query parameters sent to the socket + Map pathParams) { // original path parameters sent to the socket } public record Response( @@ -25,6 +25,5 @@ public static Response empty(int statusCode) { } public Response authorize(Context ctx); - public Response getRoot(Context ctx); - public Response postBulk(Context ctx); + public Response putMappingsSettings(Context ctx); } diff --git a/testing/src/test/java/mock/osf/NoOp.java b/testing/src/test/java/mock/osf/NoOp.java index 3eec9825..a6e5559d 100644 --- a/testing/src/test/java/mock/osf/NoOp.java +++ b/testing/src/test/java/mock/osf/NoOp.java @@ -13,7 +13,5 @@ private Response placeholder() { @Override public Response authorize(Context ctx) { return placeholder(); } @Override - public Response getRoot(Context ctx) { return placeholder(); } - @Override - public Response postBulk(Context ctx) { return placeholder(); } + public Response putMappingsSettings(Context ctx) { return placeholder(); } } diff --git a/testing/src/test/java/mock/osf/Standard.java b/testing/src/test/java/mock/osf/Standard.java index 7195cdbf..5bcb1852 100644 --- a/testing/src/test/java/mock/osf/Standard.java +++ b/testing/src/test/java/mock/osf/Standard.java @@ -1,5 +1,10 @@ package mock.osf; -public class Standard extends NoOp { +import mock.annotation.Replace; +public class Standard extends NoOp { + @Override @Replace + public Response putMappingsSettings (Context ctx) { + return super.putMappingsSettings(ctx); + } } From ad9b554a8c6bfe5afedc179b2dbcef5349809219 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Thu, 20 Aug 2026 14:29:12 -0700 Subject: [PATCH 16/24] working tests that mimic JUnit but use OpensearchSpy to reverse engineer SDK v2. --- testing/src/test/java/expect/Sane.java | 4 +- .../src/test/java/mock/OpensearchEngine.java | 12 +- testing/src/test/java/mock/OpensearchSpy.java | 348 ++++++++++++++++++ testing/src/test/java/mock/osf/Standard.java | 9 +- .../test/java/suite/ArtificialComposite.java | 35 ++ testing/src/test/java/suite/Sanity.java | 34 +- 6 files changed, 426 insertions(+), 16 deletions(-) create mode 100644 testing/src/test/java/mock/OpensearchSpy.java create mode 100644 testing/src/test/java/suite/ArtificialComposite.java diff --git a/testing/src/test/java/expect/Sane.java b/testing/src/test/java/expect/Sane.java index a4490dbe..50073e73 100644 --- a/testing/src/test/java/expect/Sane.java +++ b/testing/src/test/java/expect/Sane.java @@ -15,7 +15,7 @@ public void run() { int passed = 0; for (OpensearchSupportedFunctionality osf : this.knownMocks) { if (osf instanceof JUnitish) { - count++; + count += ((JUnitish) osf).results.values().size(); for (Boolean b : ((JUnitish) osf).results.values()) { if (b) { passed++; @@ -29,7 +29,7 @@ public void run() { assert false : "No results found. Cannot meet any expectation without results."; } else { if (failed > 0 || passed == 0) { - assert false: passed + " tests passed but " + failed + " tests failed"; + assert false: "out of " + count + " tests, " + passed + " tests passed and " + failed + " tests failed"; } } } diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 28144322..482db9dd 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -26,7 +26,7 @@ import mock.osf.Standard; public final class OpensearchEngine { - private record MethodTarget(Object instance, Method method) { + public record MethodTarget(Object instance, Method method) { } private final Logger log = LoggerFactory.getLogger(OpensearchEngine.class); private final Map redirect = new ConcurrentHashMap<>(); @@ -65,6 +65,14 @@ public void add(@Nonnull OpensearchSupportedFunctionality composite) { } } } + + public Map redirectDeepCopy() { + ConcurrentHashMap dcp = new ConcurrentHashMap<>(); + for (Map.Entry item : this.redirect.entrySet()) { + dcp.put(item.getKey(), new MethodTarget(item.getValue().instance(), item.getValue().method())); + } + return dcp; + } /** * For cucumber to signal the beginning or ending of a scenerio by clearing all of the test @@ -116,7 +124,7 @@ private String determineMethodName(String method, String path) { * Resolves the target interface method by climbing the profile's class hierarchy, respecting your * explicit @Replace annotation policies. */ - private Response process(String methodName, Context context) { + public Response process(String methodName, Context context) { final String registry = "Devregistrystructured"; log.info("Method name: {}", methodName); log.info("Context:"); diff --git a/testing/src/test/java/mock/OpensearchSpy.java b/testing/src/test/java/mock/OpensearchSpy.java new file mode 100644 index 00000000..3164f12b --- /dev/null +++ b/testing/src/test/java/mock/OpensearchSpy.java @@ -0,0 +1,348 @@ +package mock; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.javalin.Javalin; +import io.javalin.community.ssl.SslPlugin; +import io.javalin.http.Context; +import io.javalin.http.Handler; +import io.javalin.http.HandlerType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Sits on 9200 exactly like OpensearchEngine, but instead of dispatching to mock + * {@code @Replace} methods, forwards every request byte-for-byte to a real OpenSearch + * (e.g. running in docker), captures the upstream response, relays it back to the SDK, + * and records the request/response pair to disk for later mock-building analysis. + * + *

Deliberately does NOT reuse OpensearchEngine's {@code Context}/{@code Response} + * facade or the {@code redirect} reflection machinery — none of that applies to a + * transparent proxy, and staying on raw Javalin {@code Context} + byte arrays avoids + * losing anything the SDK sends (in particular NDJSON bulk bodies, which a + * parse-as-one-JSON-doc facade would mangle). + */ +public final class OpensearchSpy { + + private final Logger log = LoggerFactory.getLogger(OpensearchSpy.class); + private final ObjectMapper mapper = new ObjectMapper(); + private final AtomicLong sequence = new AtomicLong(); + private final Object indexWriteLock = new Object(); + private Javalin app; + + private final HttpClient upstreamClient; + private final URI upstreamBase; + private final Path captureDir; + + // Headers we do NOT re-set on the *outgoing* upstream request or the *outgoing* + // client response, because the HTTP stack (HttpClient / Jetty) computes and sets + // these itself based on the actual bytes being sent — copying stale values across + // would corrupt the framing. Everything is still captured in the JSON record + // unfiltered; this list only affects what gets blindly re-set on the wire. + private static final Set HOP_BY_HOP_HEADERS = Set.of( + "host", "content-length", "connection", "transfer-encoding", "expect"); + + public OpensearchSpy(URI upstreamBase, Path captureDir) { + this.upstreamBase = upstreamBase; + this.captureDir = captureDir; + this.upstreamClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NEVER) + .sslContext(trustAllSslContext()) // local docker cluster w/ demo self-signed certs + .build(); + try { + Files.createDirectories(captureDir); + } catch (IOException e) { + throw new RuntimeException("Could not create capture directory " + captureDir, e); + } + } + + /** + * Trust-all SSLContext used ONLY for talking to the local docker OpenSearch upstream. + * Never applied to the listener the SDK connects to on 9200 (that keeps its normal + * openssl-generated cert via SslPlugin below). Fine for a local test double; do not + * reuse this pattern anywhere that touches a real network. + */ + private static SSLContext trustAllSslContext() { + try { + TrustManager[] trustAll = new TrustManager[] {new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + }}; + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustAll, new SecureRandom()); + return ctx; + } catch (Exception e) { + throw new RuntimeException("Failed building trust-all SSLContext for upstream client", e); + } + } + + public void start(int port) { + this.app = Javalin.create(config -> { + config.registerPlugin(new SslPlugin(ssl -> { + ssl.host = "127.0.0.1"; + ssl.insecure = false; + ssl.securePort = port; + try { + Process process = new ProcessBuilder("sh", "-c", + "openssl req -x509 -newkey rsa:2048 -keyout /dev/stdout -out /dev/stdout -sha256 -days 1 -nodes -subj '/CN=localhost' -addext 'subjectAltName = DNS:localhost' 2>/dev/null") + .start(); + String openSslOutput = + new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + ssl.pemFromString(openSslOutput, openSslOutput); + } catch (Exception e) { + throw new RuntimeException("Failed to auto-generate localhost cert via openssl", e); + } + })); + + Handler catchAll = this::handle; + + for (HandlerType method : new HandlerType[] {HandlerType.GET, HandlerType.POST, + HandlerType.PUT, HandlerType.PATCH, HandlerType.DELETE, HandlerType.HEAD, + HandlerType.OPTIONS}) { + config.routes.addHttpHandler(method, "/*", catchAll); + } + }); + this.app.start(); + log.info("OpensearchSpy listening on 9200, forwarding to {}, capturing to {}", + upstreamBase, captureDir.toAbsolutePath()); + } + + public void stop() { + if (this.app != null) { + this.app.stop(); + } + } + + private void handle(Context ctx) { + long id = sequence.incrementAndGet(); + byte[] requestBody = ctx.bodyAsBytes(); + String rawPathAndQuery = ctx.path() + (ctx.queryString() != null ? "?" + ctx.queryString() : ""); + + // Full, unfiltered, multi-valued incoming headers, pulled from the raw servlet + // request rather than ctx.headerMap() (which is single-valued / last-wins and + // would silently drop a repeated header like a second Set-Cookie or X-Forwarded-For). + Map> incomingHeaders = multiValuedRequestHeaders(ctx); + + HttpRequest.Builder upstreamReq = HttpRequest.newBuilder() + .uri(upstreamBase.resolve(rawPathAndQuery)) + .method(ctx.method().name(), + requestBody.length == 0 + ? HttpRequest.BodyPublishers.noBody() + : HttpRequest.BodyPublishers.ofByteArray(requestBody)); + + // Real SDK auth is forwarded as-is (docker was started with matching credentials) -- + // no substitution. We only skip headers HttpClient manages itself (see comment above). + for (Map.Entry> header : incomingHeaders.entrySet()) { + if (HOP_BY_HOP_HEADERS.contains(header.getKey().toLowerCase())) { + continue; + } + for (String value : header.getValue()) { + try { + upstreamReq.header(header.getKey(), value); + } catch (IllegalArgumentException restrictedHeader) { + // A handful of headers (e.g. Host) are restricted even outside our explicit + // skip list depending on JDK version; HttpClient sets the wire equivalent + // itself, so this is safe to ignore. + } + } + } + + HttpResponse upstreamResp; + Instant start = Instant.now(); + try { + upstreamResp = upstreamClient.send(upstreamReq.build(), HttpResponse.BodyHandlers.ofByteArray()); + } catch (Exception e) { + log.error("Failed forwarding {} {} to upstream", ctx.method(), rawPathAndQuery, e); + ctx.status(502); + ctx.contentType("application/json"); + ctx.result("{\"error\": \"OpensearchSpy failed to reach upstream: " + e.getMessage() + "\"}"); + recordFailure(id, ctx, rawPathAndQuery, incomingHeaders, requestBody, e); + return; + } + long tookMillis = Instant.now().toEpochMilli() - start.toEpochMilli(); + + ctx.status(upstreamResp.statusCode()); + upstreamResp.headers().map().forEach((name, values) -> { + if (HOP_BY_HOP_HEADERS.contains(name.toLowerCase())) { + return; // still captured below in full -- just not blindly re-set on the wire + } + for (String value : values) { + ctx.header(name, value); + } + }); + ctx.result(upstreamResp.body()); + + record(id, ctx, rawPathAndQuery, incomingHeaders, requestBody, + upstreamResp, tookMillis); + } + + /** Pulls every header value (not just the last one per name) off the raw servlet request. */ + private Map> multiValuedRequestHeaders(Context ctx) { + Map> result = new LinkedHashMap<>(); + var servletRequest = ctx.req(); + Enumeration names = servletRequest.getHeaderNames(); + while (names != null && names.hasMoreElements()) { + String name = names.nextElement(); + List values = new ArrayList<>(); + Enumeration valueEnum = servletRequest.getHeaders(name); + while (valueEnum.hasMoreElements()) { + values.add(valueEnum.nextElement()); + } + result.put(name, values); + } + return result; + } + + private void record(long id, Context ctx, String pathAndQuery, Map> reqHeaders, + byte[] reqBody, HttpResponse upstreamResp, long tookMillis) { + try { + ObjectNode root = mapper.createObjectNode(); + root.put("id", id); + root.put("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now())); + root.put("tookMillis", tookMillis); + + populateRequest(root.putObject("request"), ctx, pathAndQuery, reqHeaders, reqBody); + + ObjectNode response = root.putObject("response"); + response.put("status", upstreamResp.statusCode()); + response.put("httpVersion", upstreamResp.version().name()); + response.put("upstreamUri", upstreamResp.uri().toString()); + ObjectNode respHeadersNode = response.putObject("headers"); + upstreamResp.headers().map().forEach((k, v) -> { + var arr = respHeadersNode.putArray(k); + v.forEach(arr::add); + }); + attachBody(response, "body", upstreamResp.body()); + + writeCaptureFile(id, ctx.method().name(), pathAndQuery, root); + appendToIndex(root); + } catch (IOException e) { + log.error("Failed to record capture #{}", id, e); + } + } + + private void recordFailure(long id, Context ctx, String pathAndQuery, + Map> reqHeaders, byte[] reqBody, Exception failure) { + try { + ObjectNode root = mapper.createObjectNode(); + root.put("id", id); + root.put("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now())); + root.put("error", failure.toString()); + + populateRequest(root.putObject("request"), ctx, pathAndQuery, reqHeaders, reqBody); + + writeCaptureFile(id, ctx.method().name(), pathAndQuery + "-FAILED", root); + appendToIndex(root); + } catch (IOException e) { + log.error("Failed to record failure capture #{}", id, e); + } + } + + /** Captures everything we can cheaply pull off the incoming request, not just body+headers. */ + private void populateRequest(ObjectNode request, Context ctx, String pathAndQuery, + Map> reqHeaders, byte[] reqBody) { + request.put("method", ctx.method().name()); + request.put("path", ctx.path()); + request.put("pathAndQuery", pathAndQuery); + request.put("queryString", ctx.queryString()); + request.put("endpoint", ctx.endpoint().path); + request.put("protocol", ctx.protocol()); + request.put("scheme", ctx.scheme()); + request.put("host", ctx.host()); + request.put("contentType", ctx.contentType()); + ObjectNode reqHeadersNode = request.putObject("headers"); + reqHeaders.forEach((k, v) -> { + var arr = reqHeadersNode.putArray(k); + v.forEach(arr::add); + }); + attachBody(request, "body", reqBody); + } + + private void writeCaptureFile(long id, String method, String pathAndQuery, ObjectNode root) + throws IOException { + String filename = "%05d-%s-%s.json".formatted(id, method, sanitize(pathAndQuery)); + Files.writeString(captureDir.resolve(filename), + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + /** Append-only NDJSON index across all captures, for quick grepping / batch analysis. */ + private void appendToIndex(ObjectNode root) throws IOException { + String line = mapper.writeValueAsString(root) + System.lineSeparator(); + synchronized (indexWriteLock) { + Files.writeString(captureDir.resolve("_index.ndjson"), line, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + } + + /** + * Bulk/NDJSON and ordinary JSON bodies both round-trip fine as text; falls back to + * base64 only if the payload isn't valid UTF-8. Note: plain + * {@code new String(bytes, UTF_8)} never throws -- it silently swaps bad bytes for + * U+FFFD -- so we decode strictly with CodingErrorAction.REPORT to actually detect + * that case instead of quietly corrupting the capture. + */ + private void attachBody(ObjectNode parent, String field, byte[] body) { + if (body == null || body.length == 0) { + parent.putNull(field); + return; + } + try { + String text = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(body)) + .toString(); + parent.put(field, text); + parent.put(field + "Encoding", "utf8"); + } catch (CharacterCodingException notUtf8) { + parent.put(field, java.util.Base64.getEncoder().encodeToString(body)); + parent.put(field + "Encoding", "base64"); + } + } + + private String sanitize(String pathAndQuery) { + String pathOnly = pathAndQuery.split("\\?")[0]; + String s = pathOnly.replaceAll("[^a-zA-Z0-9]", "_"); + return s.length() > 80 ? s.substring(0, 80) : s; + } + + public static void main(String[] argv) throws InterruptedException { + // Point this at wherever docker-compose exposes OpenSearch, e.g. https://localhost:19200 + URI upstream = URI.create(System.getProperty("spy.upstream", "https://localhost:19200")); + Path captures = Path.of(System.getProperty("spy.captureDir", "/tmp/captures")); + + OpensearchSpy spy = new OpensearchSpy(upstream, captures); + spy.start(9200); + Thread.sleep(1000L * 1000); + spy.stop(); + } +} \ No newline at end of file diff --git a/testing/src/test/java/mock/osf/Standard.java b/testing/src/test/java/mock/osf/Standard.java index 5bcb1852..56687e52 100644 --- a/testing/src/test/java/mock/osf/Standard.java +++ b/testing/src/test/java/mock/osf/Standard.java @@ -3,8 +3,15 @@ import mock.annotation.Replace; public class Standard extends NoOp { + @Override @Replace + public Response authorize (Context ctc) { + if (ctc.headers().containsKey("authorization") && ctc.headers().get("authorization").startsWith("Basic ")) { + return Response.empty(200); + } + return Response.empty(401); + } @Override @Replace public Response putMappingsSettings (Context ctx) { - return super.putMappingsSettings(ctx); + return Response.json("{\"acknowledged\":true,\"shards_acknowledged\":true,\"index\":\"dev-registry-structured\"}"); } } diff --git a/testing/src/test/java/suite/ArtificialComposite.java b/testing/src/test/java/suite/ArtificialComposite.java new file mode 100644 index 00000000..a020bd0f --- /dev/null +++ b/testing/src/test/java/suite/ArtificialComposite.java @@ -0,0 +1,35 @@ +package suite; + +import java.util.List; +import mock.MockAware; +import mock.OpensearchEngine; +import mock.OpensearchSupportedFunctionality; + +abstract class ArtificialComposite implements OpensearchSupportedFunctionality, MockAware { + private final OpensearchEngine redirect = new OpensearchEngine(); + + @Override + public void mocks(List mocks) { + for (OpensearchSupportedFunctionality mock : mocks) { + redirect.add(mock); + } + } + + @Override + public final Response authorize(Context ctx) { + return this.redirect.process( + StackWalker.getInstance() + .walk(stream -> stream.findFirst().map(StackWalker.StackFrame::getMethodName)) + .orElse("unknown"), + ctx); + } + + @Override + public final Response putMappingsSettings(Context ctx) { + return this.redirect.process( + StackWalker.getInstance() + .walk(stream -> stream.findFirst().map(StackWalker.StackFrame::getMethodName)) + .orElse("unknown"), + ctx); + } +} diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index 6dbbb056..fbc50e18 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -1,35 +1,47 @@ package suite; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.Test; -import mock.MockAware; import mock.OpensearchSupportedFunctionality; import mock.osf.JUnitish; -public final class Sanity implements MockAware { +public final class Sanity extends ArtificialComposite { private final List knownMocks = new ArrayList(); + + @Override + public void mocks(List mocks) { + super.mocks(mocks); + this.knownMocks.addAll(mocks); + } + @Override public void run() { + System.out.println("running sanity"); + System.out.println("mocks: " + this.knownMocks); for (OpensearchSupportedFunctionality osf : this.knownMocks) { if (osf instanceof JUnitish) { + System.out.println("running junitish"); ((JUnitish) osf).runTests(this); } } } - @Override - public void mocks(List mocks) { - this.knownMocks.addAll(mocks); - } - @Test - public void test_authorize() { - assert false: "implement me"; + public void testAuthorize() { + HashMap headers = new HashMap(); + Context ctx = new Context("body", headers, null, null); + headers.put("user-agent", "opensearch-java/3.2.0 (Java/21.0.11)"); + headers.put("accept", "application/json; charset=UTF-8"); + headers.put("authorization", "Basic fakekey"); + headers.put("content-type", "application/json; charset=UTF-8"); + assert this.authorize(ctx).statusCode() == 200 : "authorization was not requested"; } @Test - public void test_root() { - assert false: "implement me"; + public void testMappingSettings() { + Context ctx = new Context("body", null, null, null); + assert this.putMappingsSettings(ctx).statusCode() == 200 : "did not return a success status code"; } } From 5e841ec157fbe7c40af5458ca25042c2e7f73756 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 21 Aug 2026 08:30:13 -0700 Subject: [PATCH 17/24] spy is fully functional --- testing/src/test/java/mock/OpensearchSpy.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/testing/src/test/java/mock/OpensearchSpy.java b/testing/src/test/java/mock/OpensearchSpy.java index 3164f12b..e68e9155 100644 --- a/testing/src/test/java/mock/OpensearchSpy.java +++ b/testing/src/test/java/mock/OpensearchSpy.java @@ -66,8 +66,15 @@ public final class OpensearchSpy { // these itself based on the actual bytes being sent — copying stale values across // would corrupt the framing. Everything is still captured in the JSON record // unfiltered; this list only affects what gets blindly re-set on the wire. + // + // The h2-specific entries (keep-alive, upgrade, proxy-connection, te) matter even + // though the listener now runs HTTP/1.1-only (see start(), ssl.http2 = false): + // OpenSearch's HTTP/1.1 upstream commonly sends "Keep-Alive: timeout=5" etc, and + // these are meaningless/stale once relayed over a fresh connection, so we drop them + // on principle rather than only when strictly required by the protocol in use. private static final Set HOP_BY_HOP_HEADERS = Set.of( - "host", "content-length", "connection", "transfer-encoding", "expect"); + "host", "content-length", "connection", "transfer-encoding", "expect", + "keep-alive", "upgrade", "proxy-connection", "te"); public OpensearchSpy(URI upstreamBase, Path captureDir) { this.upstreamBase = upstreamBase; @@ -110,6 +117,14 @@ public void start(int port) { ssl.host = "127.0.0.1"; ssl.insecure = false; ssl.securePort = port; + // Disable HTTP/2 (SslPlugin defaults this on via ALPN). A raw proxy is exactly + // the kind of thing that trips HTTP/2's stricter framing rules -- a header + // that's merely sloppy over HTTP/1.1 (e.g. a stray Keep-Alive from the + // upstream, a duplicate) can get the whole stream RST_STREAM'd by the SDK's h2 + // client ("Stream reset (8)" / CANCEL). We don't need h2 for a test double, so + // pin HTTP/1.1 to remove that entire bug class rather than chase every header + // h2 happens to be strict about. + ssl.http2 = false; try { Process process = new ProcessBuilder("sh", "-c", "openssl req -x509 -newkey rsa:2048 -keyout /dev/stdout -out /dev/stdout -sha256 -days 1 -nodes -subj '/CN=localhost' -addext 'subjectAltName = DNS:localhost' 2>/dev/null") @@ -273,7 +288,7 @@ private void populateRequest(ObjectNode request, Context ctx, String pathAndQuer request.put("path", ctx.path()); request.put("pathAndQuery", pathAndQuery); request.put("queryString", ctx.queryString()); - request.put("endpoint", ctx.endpoint().path); + request.put("matchedPath", ctx.endpoint() != null ? ctx.endpoint().path : null); request.put("protocol", ctx.protocol()); request.put("scheme", ctx.scheme()); request.put("host", ctx.host()); @@ -336,13 +351,13 @@ private String sanitize(String pathAndQuery) { } public static void main(String[] argv) throws InterruptedException { - // Point this at wherever docker-compose exposes OpenSearch, e.g. https://localhost:19200 + // Point this at wherever docker-compose exposes OpenSearch, e.g. https://localhost:9201 URI upstream = URI.create(System.getProperty("spy.upstream", "https://localhost:19200")); - Path captures = Path.of(System.getProperty("spy.captureDir", "/tmp/captures")); + Path captures = Path.of(System.getProperty("spy.captureDir", "target/captures")); OpensearchSpy spy = new OpensearchSpy(upstream, captures); spy.start(9200); Thread.sleep(1000L * 1000); spy.stop(); } -} \ No newline at end of file +} From 637959b0c094ccc533cee318cac9d3a6f2b10d3b Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 21 Aug 2026 11:12:19 -0700 Subject: [PATCH 18/24] create the indices now passes --- testing/src/test/java/mock/JsonHelper.java | 58 +++++++++++++++++++ .../src/test/java/mock/OpensearchEngine.java | 57 ++++++------------ .../OpensearchSupportedFunctionality.java | 2 + testing/src/test/java/mock/osf/NoOp.java | 4 +- testing/src/test/java/mock/osf/Standard.java | 31 +++++++++- .../test/java/suite/ArtificialComposite.java | 27 +++++++-- testing/src/test/java/suite/Sanity.java | 4 +- 7 files changed, 134 insertions(+), 49 deletions(-) create mode 100644 testing/src/test/java/mock/JsonHelper.java diff --git a/testing/src/test/java/mock/JsonHelper.java b/testing/src/test/java/mock/JsonHelper.java new file mode 100644 index 00000000..e5c21330 --- /dev/null +++ b/testing/src/test/java/mock/JsonHelper.java @@ -0,0 +1,58 @@ +package mock; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonHelper { + public record BulkCreateRequest (BulkCreateRequestLine1 create) {}; + public record BulkCreateRequestLine1(String _id, String _index) {}; + public record BulkCreateResponse(int took, boolean errors, List items) {}; + public record BulkCreateResponseItem(BulkCreateResponseItemResult create) {}; + public record BulkCreateResponseItemResult(String _index, String _id, int _version, String result, Shards _shards, int _seq_no, int _primary_term, int status) {}; + public record Shards(int total, int successful, int failed) {}; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + private final ObjectMapper mapper = new ObjectMapper(); + + public T decode (String body, Class target) { + try { + return this.mapper.readValue(body, target); + } catch (JsonProcessingException e) { + log.error("Could not convert body to the desired record {}", body, e); + throw new NoOpException("invalid json body for conversion"); + } + } + + public Map decodeTopLevel(String body) { + if (body == null || body.isBlank()) + return Map.of(); + try { + JsonNode root = this.mapper.readTree(body); + Map result = new LinkedHashMap<>(); + if (!root.isObject()) + return Map.of(); // not a JSON object at top level + for (Map.Entry e : root.properties()) { + result.put(e.getKey(), this.mapper.writeValueAsString(e.getValue())); + } + return result; + } catch (Exception e) { + return Map.of(); // not valid JSON at all + } + } + + public String encode (T object) { + try { + return this.mapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + log.error("invalid object (not a record) for encoding to json", e); + throw new NoOpException("invalid object for conversion to json"); + } + } + +} diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 482db9dd..639b5a1a 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -6,15 +6,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import io.javalin.community.ssl.SslPlugin; import io.javalin.http.Handler; @@ -28,9 +25,10 @@ public final class OpensearchEngine { public record MethodTarget(Object instance, Method method) { } - private final Logger log = LoggerFactory.getLogger(OpensearchEngine.class); + + private final JsonHelper json = new JsonHelper(); + private final Logger log = LoggerFactory.getLogger(this.getClass()); private final Map redirect = new ConcurrentHashMap<>(); - private final ObjectMapper mapper = new ObjectMapper(); private Javalin app; /** @@ -82,37 +80,12 @@ public void clear() { this.redirect.clear(); } - /** - * Same as decodeTopLevel, but tolerant of bodies that aren't a JSON object at all (not valid - * JSON, or a JSON array/scalar instead of an object). Returns empty map in that case instead of - * throwing, so callers on a mixed-content-type pipeline can call this unconditionally. - */ - private Map decodeTopLevel(String body) { - if (body == null || body.isBlank()) - return Map.of(); - try { - JsonNode root = this.mapper.readTree(body); - Map result = new LinkedHashMap<>(); - if (!root.isObject()) - return Map.of(); // not a JSON object at top level - for (Map.Entry e : root.properties()) { - result.put(e.getKey(), this.mapper.writeValueAsString(e.getValue())); - } - return result; - } catch (Exception e) { - return Map.of(); // not valid JSON at all - } - } - /** * Converts HTTP paths into safe, matching Java method names. Examples: GET / -> getRoot POST * /_bulk -> postBulk POST /my-index/_search -> postMyIndexSearch */ private String determineMethodName(String method, String path) { String sanitizedPath = path.replaceAll("[^a-zA-Z0-9/]", ""); - if (sanitizedPath.equals("/") || path.isEmpty()) { - return method.toLowerCase() + "Root"; - } String camelCasePath = Arrays.stream(sanitizedPath.split("/")).filter(segment -> !segment.isEmpty()) .map(segment -> Character.toUpperCase(segment.charAt(0)) + segment.substring(1)) @@ -120,24 +93,25 @@ private String determineMethodName(String method, String path) { return method.toLowerCase() + camelCasePath; } + private String indexFrom(String path) { + return path.split("/")[1]; + } + /** * Resolves the target interface method by climbing the profile's class hierarchy, respecting your * explicit @Replace annotation policies. */ public Response process(String methodName, Context context) { - final String registry = "Devregistrystructured"; log.info("Method name: {}", methodName); log.info("Context:"); + log.info(" index: {}", context.index()); log.info(" body: {}", context.body()); log.info(" hearder: {}", context.headers()); log.info(" query: {}", context.queryParams()); log.info(" params: {}", context.pathParams()); - if (methodName.endsWith(registry)) { - methodName = methodName.substring(0, methodName.length() - registry.length()); - Map endpoint = decodeTopLevel(context.body()); - for (String name : endpoint.keySet().stream().map(String::toLowerCase).sorted().toList()) { - methodName = methodName + Character.toUpperCase(name.charAt(0)) + name.substring(1); - } + Map endpoint = json.decodeTopLevel(context.body()); + for (String name : endpoint.keySet().stream().map(String::toLowerCase).sorted().toList()) { + methodName = methodName + Character.toUpperCase(name.charAt(0)) + name.substring(1); } return subprocess(methodName, context); } @@ -171,12 +145,17 @@ public void start(int port) { Handler catchAll = ctx -> { Context facadeContext = - new Context(ctx.body(), ctx.headerMap(), + + new Context( + indexFrom(ctx.path()), + ctx.body(), ctx.headerMap(), ctx.queryParamMap().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))), ctx.pathParamMap()); - String targetMethodName = determineMethodName(ctx.method().name(), ctx.path()); + + String targetMethodName = determineMethodName(ctx.method().name(), ctx.path().substring(facadeContext.index().length()+1)); log.info("request path: {}", ctx.path()); + // FIXME: need to always check authorize() here??? sendResponse(ctx, process(targetMethodName, facadeContext)); }; diff --git a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java index 358660d5..1aa14d54 100644 --- a/testing/src/test/java/mock/OpensearchSupportedFunctionality.java +++ b/testing/src/test/java/mock/OpensearchSupportedFunctionality.java @@ -4,6 +4,7 @@ public interface OpensearchSupportedFunctionality { public record Context( + String index, // the index that the JSON body applies String body, // original headers sent to the socket Map headers, // original headers sent to the socket Map queryParams, // original query parameters sent to the socket @@ -25,5 +26,6 @@ public static Response empty(int statusCode) { } public Response authorize(Context ctx); + public Response postBulkCreate(Context ctx); public Response putMappingsSettings(Context ctx); } diff --git a/testing/src/test/java/mock/osf/NoOp.java b/testing/src/test/java/mock/osf/NoOp.java index a6e5559d..69ce2e50 100644 --- a/testing/src/test/java/mock/osf/NoOp.java +++ b/testing/src/test/java/mock/osf/NoOp.java @@ -4,7 +4,7 @@ import mock.NoOpException; import mock.OpensearchSupportedFunctionality; -public abstract class NoOp implements OpensearchSupportedFunctionality { +public class NoOp implements OpensearchSupportedFunctionality { private Response placeholder() { String methodName = new Throwable().getStackTrace()[1].getMethodName(); String className = MethodHandles.lookup().lookupClass().getSimpleName(); @@ -13,5 +13,7 @@ private Response placeholder() { @Override public Response authorize(Context ctx) { return placeholder(); } @Override + public Response postBulkCreate(Context ctx) { return placeholder(); } + @Override public Response putMappingsSettings(Context ctx) { return placeholder(); } } diff --git a/testing/src/test/java/mock/osf/Standard.java b/testing/src/test/java/mock/osf/Standard.java index 56687e52..3f283147 100644 --- a/testing/src/test/java/mock/osf/Standard.java +++ b/testing/src/test/java/mock/osf/Standard.java @@ -1,8 +1,19 @@ package mock.osf; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import mock.JsonHelper; +import mock.JsonHelper.BulkCreateRequest; +import mock.JsonHelper.BulkCreateResponse; +import mock.JsonHelper.BulkCreateResponseItem; +import mock.JsonHelper.BulkCreateResponseItemResult; +import mock.JsonHelper.Shards; import mock.annotation.Replace; public class Standard extends NoOp { + private final JsonHelper json = new JsonHelper(); + @Override @Replace public Response authorize (Context ctc) { if (ctc.headers().containsKey("authorization") && ctc.headers().get("authorization").startsWith("Basic ")) { @@ -11,7 +22,25 @@ public Response authorize (Context ctc) { return Response.empty(401); } @Override @Replace + public Response postBulkCreate (Context ctx) { + int seq = 0; + Iterator requestsText = List.of(ctx.body().split("\\R")).iterator(); + List items = new LinkedList<>(); + while (requestsText.hasNext()) { + BulkCreateRequest request = json.decode(requestsText.next(), BulkCreateRequest.class); + items.add (new BulkCreateResponseItem( + new BulkCreateResponseItemResult( + request.create()._index(), + request.create()._id(), + 1, "created", + new Shards(1, 1, 0), + seq, 1, 201))); + requestsText.next(); // throw away the body of the message + } + return Response.json(json.encode(new BulkCreateResponse(11, false, items))); + } + @Override @Replace public Response putMappingsSettings (Context ctx) { - return Response.json("{\"acknowledged\":true,\"shards_acknowledged\":true,\"index\":\"dev-registry-structured\"}"); + return Response.json("{\"acknowledged\":true,\"shards_acknowledged\":true,\"index\":\"" + ctx.index() + "\"}"); } } diff --git a/testing/src/test/java/suite/ArtificialComposite.java b/testing/src/test/java/suite/ArtificialComposite.java index a020bd0f..45a3d57f 100644 --- a/testing/src/test/java/suite/ArtificialComposite.java +++ b/testing/src/test/java/suite/ArtificialComposite.java @@ -2,28 +2,38 @@ import java.util.List; import mock.MockAware; +import mock.NoOpException; import mock.OpensearchEngine; import mock.OpensearchSupportedFunctionality; -abstract class ArtificialComposite implements OpensearchSupportedFunctionality, MockAware { +class ArtificialComposite implements OpensearchSupportedFunctionality, MockAware { private final OpensearchEngine redirect = new OpensearchEngine(); + @Override + public final Response authorize(Context ctx) { + return this.redirect.process( + StackWalker.getInstance() + .walk(stream -> stream.findFirst().map(StackWalker.StackFrame::getMethodName)) + .orElse("unknown"), + ctx); + } + @Override public void mocks(List mocks) { for (OpensearchSupportedFunctionality mock : mocks) { redirect.add(mock); } - } - + } + @Override - public final Response authorize(Context ctx) { + public Response postBulkCreate(Context ctx) { return this.redirect.process( StackWalker.getInstance() .walk(stream -> stream.findFirst().map(StackWalker.StackFrame::getMethodName)) .orElse("unknown"), ctx); - } - + } + @Override public final Response putMappingsSettings(Context ctx) { return this.redirect.process( @@ -32,4 +42,9 @@ public final Response putMappingsSettings(Context ctx) { .orElse("unknown"), ctx); } + + @Override + public void run() { + throw new NoOpException("This should be overriden by suites"); + } } diff --git a/testing/src/test/java/suite/Sanity.java b/testing/src/test/java/suite/Sanity.java index fbc50e18..5b23fbfe 100644 --- a/testing/src/test/java/suite/Sanity.java +++ b/testing/src/test/java/suite/Sanity.java @@ -31,7 +31,7 @@ public void run() { @Test public void testAuthorize() { HashMap headers = new HashMap(); - Context ctx = new Context("body", headers, null, null); + Context ctx = new Context("test", "body", headers, null, null); headers.put("user-agent", "opensearch-java/3.2.0 (Java/21.0.11)"); headers.put("accept", "application/json; charset=UTF-8"); headers.put("authorization", "Basic fakekey"); @@ -41,7 +41,7 @@ public void testAuthorize() { @Test public void testMappingSettings() { - Context ctx = new Context("body", null, null, null); + Context ctx = new Context("test", "body", null, null, null); assert this.putMappingsSettings(ctx).statusCode() == 200 : "did not return a success status code"; } } From 51de93cb37f0b1d91617b3b4da653b43cc25b2e8 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 21 Aug 2026 11:18:00 -0700 Subject: [PATCH 19/24] sonar fix --- testing/src/test/java/mock/OpensearchEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index 639b5a1a..b7cb635d 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -198,7 +198,7 @@ private Response subprocess(String methodName, Context context) { } } - public static void main(String argv[]) throws InterruptedException { + public static void main(String[] argv) throws InterruptedException { OpensearchEngine me = new OpensearchEngine(); me.start(9200); me.add(new Standard()); From 0d556da115ababf65077ee915bf7e3cdcdfce368 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 21 Aug 2026 11:39:32 -0700 Subject: [PATCH 20/24] clean up to be able to do one step in all of the feature files --- .../test/java/cucumber/SingleScenerio.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/testing/src/test/java/cucumber/SingleScenerio.java b/testing/src/test/java/cucumber/SingleScenerio.java index 55d25160..f88f6076 100644 --- a/testing/src/test/java/cucumber/SingleScenerio.java +++ b/testing/src/test/java/cucumber/SingleScenerio.java @@ -6,6 +6,13 @@ import java.nio.file.Paths; public class SingleScenerio { + public static final String TEST_DATA_DIR = + System.getProperty("user.dir") + File.separator + "src/test/resources"; + + private static String clean(String arg) { + arg = arg.strip(); + return arg.substring(1,arg.length()-1); + } public static void main(String[] args) throws NumberFormatException, IOException { if (args.length > 2 || args.length == 0) { System.out.println ("usage: "); @@ -14,7 +21,7 @@ public static void main(String[] args) throws NumberFormatException, IOException return; } - for (File file : Paths.get(TestConstants.TEST_DATA_DIR, "features").toFile().listFiles((dir, name) -> name.endsWith(".feature"))) { + for (File file : Paths.get(TEST_DATA_DIR, "features").toFile().listFiles((dir, name) -> name.endsWith(".feature"))) { for (String line : Files.readAllLines(file.toPath())) { line = line.strip(); if (line.startsWith("|")) { @@ -30,16 +37,15 @@ public static void main(String[] args) throws NumberFormatException, IOException } if (args.length == 2 && !args[1].equals(scenerio[2].strip())) continue; StepDefs engine = new StepDefs(); - System.out.println("an_and"); - engine.an_and( + System.out.println("construct"); + engine.construct( Integer.valueOf(scenerio[1].strip()), args.length == 1 ? null : Integer.valueOf(scenerio[2].strip()), - scenerio[3].strip().substring(1, scenerio[3].strip().length()-1)); - System.out.println("execute_validate"); - engine.execute_validate (scenerio[4].strip().substring(1,scenerio[4].strip().length()-1)); - System.out.println ("compare_to_the"); - engine.compare_to_the(scenerio[5].strip().isBlank() ? "" : - scenerio[5].strip().substring(1, scenerio[5].strip().length()-1)); + clean(scenerio[3])); + System.out.println(""); + engine.execute (clean(scenerio[4]), clean(scenerio[5])); + System.out.println ("compare"); + engine.compare (clean(scenerio[6])); System.out.println ("success"); return; } From ece1f3c71c5ee94b971f926afad750ab8bf29956 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Thu, 27 Aug 2026 10:08:57 -0700 Subject: [PATCH 21/24] hook cucumber to mvn test --- pom.xml | 584 +++++++++--------- testing/pom.xml | 286 +++++---- testing/src/test/java/cucumber/RunTest.java | 15 + .../src/test/java/mock/OpensearchEngine.java | 9 +- 4 files changed, 453 insertions(+), 441 deletions(-) create mode 100644 testing/src/test/java/cucumber/RunTest.java diff --git a/pom.xml b/pom.xml index 9e1c933c..fd2811ac 100644 --- a/pom.xml +++ b/pom.xml @@ -1,117 +1,117 @@ + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + --> + Go through this file line-by-line and replace the template values with your own. + --> - - 4.0.0 + + 4.0.0 - - - gov.nasa.pds - parent - 1.20.0 - + + gov.nasa.pds + parent + 1.20.0 + - - - - registry-loader - - - UTF-8 - + + + + registry-loader + + + UTF-8 + - - gov.nasa.pds - - registry-loader - - 1.4.0-SNAPSHOT - - pom + registry-loader + + 1.4.0-SNAPSHOT + + pom - ${project.groupId}:${project-name} - - Registry loader tools, from creation of the registry, to loading new products in it. - - - 2021 + ${project.groupId}:${project-name} + + Registry loader tools, from creation of the registry, to loading new products in it. + + + 2021 - + https://nasa-pds.github.io/registry-loader + + GitHub + https://github.com/NASA-PDS/registry-loader/issues + + + scm:git:https://github.com/NASA-PDS/registry-loader.git + scm:git:ssh://github.com/NASA-PDS/registry-loader.git https://nasa-pds.github.io/registry-loader - - GitHub - https://github.com/NASA-PDS/registry-loader/issues - - - scm:git:https://github.com/NASA-PDS/registry-loader.git - scm:git:ssh://github.com/NASA-PDS/registry-loader.git - https://nasa-pds.github.io/registry-loader - main - - - common - harvest - manager - testing - + main + + + common + harvest + manager + testing + OSGeo @@ -119,41 +119,41 @@ Go through this file line-by-line and replace the template values with your own. https://repo.osgeo.org/repository/release/ - - - - - org.junit.jupiter - junit-jupiter - test - - + + + + org.junit.jupiter + junit-jupiter + test + + - - org.apache.commons - commons-numbers-primes - 1.0 - jar - compile - false - - - - + --> - org.junit - junit-bom - 5.13.4 - pom - import + org.apache.commons + commons-numbers-primes + 1.0 + jar + compile + false - + + + + org.junit + junit-bom + 5.13.4 + pom + import + + + - + org.sonatype.central @@ -166,191 +166,191 @@ Go through this file line-by-line and replace the template values with your own. published - - org.apache.maven.plugins - maven-site-plugin - 4.0.0-M13 - - + + org.apache.maven.plugins + maven-site-plugin + 4.0.0-M13 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + v@{project.version} + + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + + default-install + install + + install + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + false + + + + maven-failsafe-plugin + 3.0.0-M3 + + + + + + + release + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + true + published + 5400 + 7200 + + + + org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - + maven-source-plugin + 3.3.1 + + + make-source-jar + prepare-package + + jar-no-fork + test-jar-no-fork + + + true + + + + + org.apache.maven.plugins - maven-release-plugin - 3.1.1 + maven-javadoc-plugin + 3.3.0 - v@{project.version} + 1.8 + false - - - org.apache.maven.plugins - maven-install-plugin - 3.0.0-M1 - - default-install - install - - install - - + + attach-javadocs + + jar + + - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - - false - - - - maven-failsafe-plugin - 3.0.0-M3 - - - - - - - release - - - - - org.sonatype.central - central-publishing-maven-plugin - 0.9.0 - true - - central - true - published - 5400 - 7200 - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - make-source-jar - prepare-package - - jar-no-fork - test-jar-no-fork - - - true - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.3.0 - - 1.8 - false - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - - - - - - - - - - - - - - - org.apache.maven.plugins - maven-project-info-reports-plugin - 3.1.2 - - - - dependencies - issue-management - licenses - mailing-lists - team - summary - scm - - - - - - org.apache.maven.plugins - maven-changes-plugin - 2.12.1 - - - - changes-report - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.3.0 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + - 1.8 - false + + --pinentry-mode + loopback + - - - org.apache.maven.plugins - maven-linkcheck-plugin - 1.2 - + + + - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - + + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.1.2 + + + + dependencies + issue-management + licenses + mailing-lists + team + summary + scm + + + + + + org.apache.maven.plugins + maven-changes-plugin + 2.12.1 + + + + changes-report + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.0 + + 1.8 + false + + + + org.apache.maven.plugins + maven-linkcheck-plugin + 1.2 + + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + - - Planetary Data System - https://pds.nasa.gov/ - + + Planetary Data System + https://pds.nasa.gov/ + diff --git a/testing/pom.xml b/testing/pom.xml index b5e19590..831a9ddd 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -1,175 +1,167 @@ + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + --> - 4.0.0 + 4.0.0 - + gov.nasa.pds registry-loader 1.4.0-SNAPSHOT - gov.nasa.pds.registry-loader - registry-loader-testing - jar - - PDS Registry Loader Testing - - Gherkin based testing - both integrated and regression - - - - Jet Propulsion Laboratory, California Institute of Technology - - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - + gov.nasa.pds.registry-loader + registry-loader-testing + jar + + PDS Registry Loader Testing + + Gherkin based testing - both integrated and regression + + + + Jet Propulsion Laboratory, California Institute of Technology + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + - - 21 - 21 - UTF-8 - - - - - - io.cucumber - cucumber-bom - 7.34.6 - pom - import - - - io.javalin - javalin-bom - 7.2.2 - pom - import - - - + + 21 + 21 + UTF-8 + + - - com.fasterxml.jackson.core - jackson-databind - 2.22.1 - compile - - - io.cucumber - cucumber-java - test - io.cucumber - cucumber-junit-platform-engine - test - - - org.junit.platform - junit-platform-suite-api - test + cucumber-bom + 7.34.6 + pom + import io.javalin - javalin - - - io.javalin.community.ssl - javalin-ssl - - - jakarta.annotation - jakarta.annotation-api - 3.0.0 - - - org.slf4j - slf4j-simple - 2.0.16 + javalin-bom + 7.2.2 + pom + import + + + + + com.fasterxml.jackson.core + jackson-databind + 2.22.1 + compile + + + io.cucumber + cucumber-java + test + + + io.cucumber + cucumber-junit-platform-engine + test + + + org.junit.platform + junit-platform-suite-api + test + + + io.javalin + javalin + + + io.javalin.community.ssl + javalin-ssl + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + org.slf4j + slf4j-simple + 2.0.16 + + - - - - - maven-site-plugin - - true - true - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - false - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - false - - - - - - src/main/resources - false - - - src/main/resources/app - true - - - - - src/test/resources - - - + + + + + maven-site-plugin + + true + true + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + + + + src/main/resources + false + + + src/main/resources/app + true + + + + + src/test/resources + + + https://github.com/NASA-PDS/pds-registry-loader-testing diff --git a/testing/src/test/java/cucumber/RunTest.java b/testing/src/test/java/cucumber/RunTest.java new file mode 100644 index 00000000..1377d46e --- /dev/null +++ b/testing/src/test/java/cucumber/RunTest.java @@ -0,0 +1,15 @@ +package cucumber; + +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; + +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("features") // Points to src/test/resources/features +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example") // Points to your step definition package +public class RunTest { + // intentionally left blank but surefire has to find this file +} diff --git a/testing/src/test/java/mock/OpensearchEngine.java b/testing/src/test/java/mock/OpensearchEngine.java index b7cb635d..9bd72f81 100644 --- a/testing/src/test/java/mock/OpensearchEngine.java +++ b/testing/src/test/java/mock/OpensearchEngine.java @@ -106,7 +106,7 @@ public Response process(String methodName, Context context) { log.info("Context:"); log.info(" index: {}", context.index()); log.info(" body: {}", context.body()); - log.info(" hearder: {}", context.headers()); + log.info(" header: {}", context.headers()); log.info(" query: {}", context.queryParams()); log.info(" params: {}", context.pathParams()); Map endpoint = json.decodeTopLevel(context.body()); @@ -135,6 +135,7 @@ public void start(int port) { Process process = new ProcessBuilder("sh", "-c", "openssl req -x509 -newkey rsa:2048 -keyout /dev/stdout -out /dev/stdout -sha256 -days 1 -nodes -subj '/CN=localhost' -addext 'subjectAltName = DNS:localhost' 2>/dev/null") .start(); + process.waitFor(); // ignoring exceptions and bad waits as this is a test harness and developer can deal with it. String openSslOutput = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); ssl.pemFromString(openSslOutput, openSslOutput); @@ -156,7 +157,11 @@ public void start(int port) { String targetMethodName = determineMethodName(ctx.method().name(), ctx.path().substring(facadeContext.index().length()+1)); log.info("request path: {}", ctx.path()); // FIXME: need to always check authorize() here??? - sendResponse(ctx, process(targetMethodName, facadeContext)); + Response aResponse = subprocess("authorize", facadeContext); + if (200 <= aResponse.statusCode() && aResponse.statusCode() < 300) { + aResponse = process(targetMethodName, facadeContext); + } + sendResponse(ctx, aResponse); }; for (HandlerType method : new HandlerType[] {HandlerType.GET, HandlerType.POST, From ad6e8d28e6347fc4eb7a469c2c1131b4f410f3c2 Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 28 Aug 2026 08:41:55 -0700 Subject: [PATCH 22/24] move to java 21 --- .github/workflows/branch-cicd.yaml.off | 2 +- .github/workflows/legacy-branch-cicd.yaml | 2 +- common/.github/workflows/branch-cicd.yaml | 2 +- common/.github/workflows/stable-cicd.yaml | 2 +- common/.github/workflows/unstable-cicd.yaml | 2 +- harvest/.github/workflows/branch-cicd.yaml | 2 +- harvest/.github/workflows/stable-cicd.yaml | 2 +- harvest/.github/workflows/unstable-cicd.yaml | 2 +- manager/.github/workflows/branch-cicd.yaml | 2 +- manager/.github/workflows/stable-cicd.yaml | 2 +- manager/.github/workflows/unstable-cicd.yaml | 2 +- manager/pom.xml | 6 ++--- pom.xml | 27 +++++--------------- 13 files changed, 20 insertions(+), 35 deletions(-) diff --git a/.github/workflows/branch-cicd.yaml.off b/.github/workflows/branch-cicd.yaml.off index 34cea1e7..ca66d9cd 100644 --- a/.github/workflows/branch-cicd.yaml.off +++ b/.github/workflows/branch-cicd.yaml.off @@ -31,7 +31,7 @@ jobs: strategy: matrix: - java-version: [17] + java-version: [21, 25] steps: - diff --git a/.github/workflows/legacy-branch-cicd.yaml b/.github/workflows/legacy-branch-cicd.yaml index 48b6d9a9..3db10e79 100644 --- a/.github/workflows/legacy-branch-cicd.yaml +++ b/.github/workflows/legacy-branch-cicd.yaml @@ -31,7 +31,7 @@ jobs: strategy: matrix: - java-version: [17] + java-version: [21, 25] steps: - diff --git a/common/.github/workflows/branch-cicd.yaml b/common/.github/workflows/branch-cicd.yaml index b3398822..dc764311 100644 --- a/common/.github/workflows/branch-cicd.yaml +++ b/common/.github/workflows/branch-cicd.yaml @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - java-version: [11,18] + java-version: [21, 25] steps: - diff --git a/common/.github/workflows/stable-cicd.yaml b/common/.github/workflows/stable-cicd.yaml index 9ea6f365..646a042c 100644 --- a/common/.github/workflows/stable-cicd.yaml +++ b/common/.github/workflows/stable-cicd.yaml @@ -72,7 +72,7 @@ jobs: uses: NASA-PDS/roundup-action@stable with: assembly: stable - packages: openjdk11-jdk + packages: openjdk21-jdk env: central_portal_username: ${{secrets.CENTRAL_REPOSITORY_USERNAME}} central_portal_token: ${{secrets.CENTRAL_REPOSITORY_TOKEN}} diff --git a/common/.github/workflows/unstable-cicd.yaml b/common/.github/workflows/unstable-cicd.yaml index 09b07ca2..fe883cc9 100644 --- a/common/.github/workflows/unstable-cicd.yaml +++ b/common/.github/workflows/unstable-cicd.yaml @@ -72,7 +72,7 @@ jobs: uses: NASA-PDS/roundup-action@stable with: assembly: unstable - packages: openjdk11-jdk + packages: openjdk21-jdk env: central_portal_username: ${{secrets.CENTRAL_REPOSITORY_USERNAME}} central_portal_token: ${{secrets.CENTRAL_REPOSITORY_TOKEN}} diff --git a/harvest/.github/workflows/branch-cicd.yaml b/harvest/.github/workflows/branch-cicd.yaml index 09d5426f..95e9c9b8 100644 --- a/harvest/.github/workflows/branch-cicd.yaml +++ b/harvest/.github/workflows/branch-cicd.yaml @@ -36,7 +36,7 @@ jobs: # 👉 For the `harvest` repo, we only need to use JDK 11 for some reason. (In the branch # workflow pre-template-rollout-summer-2021, there was no matrix strategy and JDK 11 # was "specifically specified".) - java-version: [17, 21] + java-version: [21, 25] steps: - diff --git a/harvest/.github/workflows/stable-cicd.yaml b/harvest/.github/workflows/stable-cicd.yaml index 59887d31..773a12ac 100644 --- a/harvest/.github/workflows/stable-cicd.yaml +++ b/harvest/.github/workflows/stable-cicd.yaml @@ -72,7 +72,7 @@ jobs: uses: NASA-PDS/roundup-action@stable with: assembly: stable - packages: openjdk17-jdk + packages: openjdk21-jdk maven-build-phases: install maven-doc-phases: clean,site,site:stage maven-stable-artifact-phases: clean,site,site:stage,deploy diff --git a/harvest/.github/workflows/unstable-cicd.yaml b/harvest/.github/workflows/unstable-cicd.yaml index d1f074b8..c2b8b67d 100644 --- a/harvest/.github/workflows/unstable-cicd.yaml +++ b/harvest/.github/workflows/unstable-cicd.yaml @@ -72,7 +72,7 @@ jobs: name: 🤠 Roundup uses: NASA-PDS/roundup-action@stable with: - packages: openjdk17-jdk + packages: openjdk21-jdk assembly: unstable env: central_portal_username: ${{secrets.CENTRAL_REPOSITORY_USERNAME}} diff --git a/manager/.github/workflows/branch-cicd.yaml b/manager/.github/workflows/branch-cicd.yaml index bf751b53..e7bea5de 100644 --- a/manager/.github/workflows/branch-cicd.yaml +++ b/manager/.github/workflows/branch-cicd.yaml @@ -33,7 +33,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - java-version: [17] + java-version: [21, 25] steps: - diff --git a/manager/.github/workflows/stable-cicd.yaml b/manager/.github/workflows/stable-cicd.yaml index e13b6ca3..c9a8d6f6 100644 --- a/manager/.github/workflows/stable-cicd.yaml +++ b/manager/.github/workflows/stable-cicd.yaml @@ -72,7 +72,7 @@ jobs: uses: NASA-PDS/roundup-action@stable with: assembly: stable - packages: openjdk17-jdk + packages: openjdk21-jdk maven-build-phases: install maven-doc-phases: clean,site,site:stage maven-stable-artifact-phases: clean,site,site:stage,deploy diff --git a/manager/.github/workflows/unstable-cicd.yaml b/manager/.github/workflows/unstable-cicd.yaml index e1097bc5..ec548f8f 100644 --- a/manager/.github/workflows/unstable-cicd.yaml +++ b/manager/.github/workflows/unstable-cicd.yaml @@ -61,7 +61,7 @@ jobs: uses: NASA-PDS/roundup-action@stable with: assembly: unstable - packages: openjdk17-jdk + packages: openjdk21-jdk env: central_portal_username: ${{secrets.CENTRAL_REPOSITORY_USERNAME}} central_portal_token: ${{secrets.CENTRAL_REPOSITORY_TOKEN}} diff --git a/manager/pom.xml b/manager/pom.xml index 7988c770..5a3b2ca0 100644 --- a/manager/pom.xml +++ b/manager/pom.xml @@ -61,9 +61,9 @@ POSSIBILITY OF SUCH DAMAGE. - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 diff --git a/pom.xml b/pom.xml index 308bf02f..dd59774d 100644 --- a/pom.xml +++ b/pom.xml @@ -131,28 +131,7 @@ Here's an example dependency; you can find dependencies at https://search.maven.org. This isn't actually used by this template, but is shown full-fleshed out because chances are your project will need at least one if not many dependencies. -<<<<<<< HEAD --> -======= - --> - - org.apache.commons - commons-numbers-primes - 1.0 - jar - compile - false - - - - org.slf4j - slf4j-nop - 2.0.18 - - - - ->>>>>>> 068f3d465a41a9b3d2e8ed2b762190ff1eb29f32 org.apache.commons commons-numbers-primes @@ -161,6 +140,12 @@ compile false + + + org.slf4j + slf4j-nop + 2.0.18 + From e86612da9b8b4ec2c0f6082174d6fe2f6955f0ca Mon Sep 17 00:00:00 2001 From: Al Niessner Date: Fri, 28 Aug 2026 08:53:03 -0700 Subject: [PATCH 23/24] consolidate poms a bit --- common/pom.xml | 6 ------ harvest/pom.xml | 6 ------ manager/pom.xml | 7 ------- pom.xml | 7 +++++-- testing/pom.xml | 6 ------ 5 files changed, 5 insertions(+), 27 deletions(-) diff --git a/common/pom.xml b/common/pom.xml index 1ae1a3b4..d7732676 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -60,12 +60,6 @@ POSSIBILITY OF SUCH DAMAGE. - - 11 - 11 - UTF-8 - - diff --git a/harvest/pom.xml b/harvest/pom.xml index 3691887a..ea456271 100644 --- a/harvest/pom.xml +++ b/harvest/pom.xml @@ -65,12 +65,6 @@ scm:git:ssh://git@github.com/NASA-PDS/harvest.git - - 11 - 11 - UTF-8 - - diff --git a/manager/pom.xml b/manager/pom.xml index 5a3b2ca0..966d7b56 100644 --- a/manager/pom.xml +++ b/manager/pom.xml @@ -60,13 +60,6 @@ POSSIBILITY OF SUCH DAMAGE. - - 21 - 21 - 21 - UTF-8 - - diff --git a/pom.xml b/pom.xml index dd59774d..b42f16c6 100644 --- a/pom.xml +++ b/pom.xml @@ -60,9 +60,12 @@ + + 21 + 21 + 21 - registry-loader - + registry-loader UTF-8 diff --git a/testing/pom.xml b/testing/pom.xml index 831a9ddd..bca2634c 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -60,12 +60,6 @@ - - 21 - 21 - UTF-8 - - From 4a71f5923e0dcf491c9a643d050a697291ee2815 Mon Sep 17 00:00:00 2001 From: al-niessner <1130658+al-niessner@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:42:39 -0700 Subject: [PATCH 24/24] Update RunTest.java --- testing/src/test/java/cucumber/RunTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/src/test/java/cucumber/RunTest.java b/testing/src/test/java/cucumber/RunTest.java index 1377d46e..5b42c344 100644 --- a/testing/src/test/java/cucumber/RunTest.java +++ b/testing/src/test/java/cucumber/RunTest.java @@ -9,7 +9,7 @@ @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") // Points to src/test/resources/features -@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example") // Points to your step definition package +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "cucumber") // Points to your step definition package public class RunTest { // intentionally left blank but surefire has to find this file }