You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/05/15 16:16:26 UTC

(juneau) branch master updated: test: TODO-11 NG REST client coverage closeout + cross-transport remote tests

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new b4a94c8152 test: TODO-11 NG REST client coverage closeout + cross-transport remote tests
b4a94c8152 is described below

commit b4a94c81523ab18fa83bb965e4be13cc63234672
Author: James Bognar <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
AuthorDate: Fri May 15 12:16:20 2026 -0400

    test: TODO-11 NG REST client coverage closeout + cross-transport remote tests
---
 .../microservice/MicroserviceTestFixture.java      | 187 ++++++++++++
 .../org/apache/juneau/ng/NgPackageScanner.java     |  93 ++++++
 .../juneau/ng/http/HttpFactoryFacades_Test.java    | 125 ++++++++
 .../juneau/ng/http/entity/HttpBodies_Test.java     | 211 +++++++++++++
 .../juneau/ng/http/header/NgNamedHeaders_Test.java | 302 ++++++++++++++++++
 .../ng/http/header/PolymorphicHeaders_Test.java    | 177 +++++++++++
 .../apache/juneau/ng/http/part/HttpParts_Test.java |  91 ++++++
 .../ng/http/response/HttpStatusLineBean_Test.java  |  76 +++++
 .../ng/http/response/NgNamedResponses_Test.java    | 188 ++++++++++++
 .../ng/rest/NgRemoteInterfaceTransport_Test.java   | 340 +++++++++++++++++++++
 ...FINISHED-11b-restclient-ng-coverage-closeout.md |  64 ++++
 todo/TODO-11-restclient-ng-coverage-closeout.md    |  70 -----
 todo/TODO.md                                       |   2 -
 13 files changed, 1854 insertions(+), 72 deletions(-)

diff --git a/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
new file mode 100644
index 0000000000..931d4a6694
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.microservice;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.eclipse.jetty.ee11.servlet.*;
+import org.eclipse.jetty.server.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * JUnit 5 extension that boots a {@link JettyMicroservice} on an ephemeral port for the duration of a test class.
+ *
+ * <p>
+ * Typical usage:
+ *
+ * <p class='bjava'>
+ * 	<ja>@RegisterExtension</ja>
+ * 	<jk>static</jk> MicroserviceTestFixture <jv>fixture</jv> = MicroserviceTestFixture.<jsm>create</jsm>()
+ * 		.configurations(MyServerConfig.<jk>class</jk>);
+ *
+ * 	<ja>@Configuration</ja>
+ * 	<jk>static class</jk> MyServerConfig {{
+ * 		<ja>@Bean</ja> Servlet myService() {{ <jk>return new</jk> MyRestService(); }}
+ * 	}}
+ *
+ * 	<ja>@Test</ja>
+ * 	<jk>void</jk> exampleTest() {{
+ * 		<jk>var</jk> rootUrl = <jv>fixture</jv>.getRootUrl();
+ * 		<jc>// build a client against rootUrl ...</jc>
+ * 	}}
+ * </p>
+ *
+ * <h5 class='section'>How it works:</h5>
+ * <ul>
+ * 	<li>The fixture supplies its own {@link Server} {@link Bean} factory ({@link EphemeralJettyServerConfig}) that
+ * 		binds to port {@code 0} (OS-assigned ephemeral port). User-supplied configurations should typically
+ * 		contribute {@code @Bean Servlet} definitions (which the {@link JettyMicroservice} auto-mounts at
+ * 		{@code @Rest(path=...)}); a user-supplied {@code @Bean Server} will conflict and is not supported here.
+ * 	<li>{@link #beforeAll(ExtensionContext) beforeAll} builds the microservice, calls
+ * 		{@link JettyMicroservice#createServer() createServer()}, and then
+ * 		{@link JettyMicroservice#start() start()}.
+ * 	<li>{@link #afterAll(ExtensionContext) afterAll} calls {@link JettyMicroservice#stop() stop()} so the bound
+ * 		port is released and {@code @PreDestroy} hooks on bean-store beans fire.
+ * </ul>
+ *
+ * <p>
+ * The fixture is intended to amortize Jetty-startup cost across a whole test class — combined with the dynamic
+ * {@link org.apache.juneau.rest.RestChildren#addChild RestChildren.addChild} / {@code removeChild} API, individual
+ * test methods can mount and unmount their own child resources against a single long-running server.
+ */
+public final class MicroserviceTestFixture implements BeforeAllCallback, AfterAllCallback {
+
+	private final List<Class<?>> configurations = new ArrayList<>();
+	private JettyMicroservice microservice;
+	private URI rootUrl;
+
+	private MicroserviceTestFixture() {}
+
+	/**
+	 * Creates a new fixture.
+	 *
+	 * @return A new fixture instance.
+	 */
+	public static MicroserviceTestFixture create() {
+		return new MicroserviceTestFixture();
+	}
+
+	/**
+	 * Registers one or more {@code @Configuration} classes whose {@code @Bean Servlet} methods will be auto-mounted
+	 * by the microservice. May be called multiple times to append.
+	 *
+	 * @param cs The configuration classes.
+	 * @return This fixture (fluent).
+	 */
+	public MicroserviceTestFixture configurations(Class<?>... cs) {
+		Collections.addAll(configurations, cs);
+		return this;
+	}
+
+	/**
+	 * Returns the root URI of the running microservice (after {@link #beforeAll(ExtensionContext)} has fired).
+	 *
+	 * @return The root URI, e.g. {@code http://localhost:54321/}.
+	 */
+	public URI getRootUrl() {
+		return rootUrl;
+	}
+
+	/**
+	 * Returns the actual port the server is listening on (post-{@code start()}).
+	 *
+	 * @return The bound port.
+	 */
+	public int getPort() {
+		return rootUrl.getPort();
+	}
+
+	/**
+	 * Returns the underlying microservice instance, useful for fixture-scoped child-resource mutations.
+	 *
+	 * @return The microservice.
+	 */
+	public JettyMicroservice getMicroservice() {
+		return microservice;
+	}
+
+	@Override
+	public void beforeAll(ExtensionContext ctx) throws Exception {
+		// The user's configurations come first (so @Bean Servlet methods are visible). Our default Server
+		// factory is registered last via configurations() — JettyMicroservice resolves Server from the bean
+		// store by type, and BeanStore.getBean(...) returns the first registered match.
+		var classes = new ArrayList<>(configurations);
+		classes.add(EphemeralJettyServerConfig.class);
+		microservice = JettyMicroservice.create()
+			.configurations(classes.toArray(new Class<?>[0]))
+			.build();
+		microservice.start();
+		// JettyMicroservice.getURI() uses InetAddress.getLocalHost().getHostName() (may resolve to a non-loopback
+		// IP on some machines) and ServerConnector.getPort() (returns the configured port = 0, not the bound port).
+		// For test fixtures we want a deterministic loopback URL with the actual bound port.
+		var localPort = -1;
+		for (var c : microservice.getServer().getConnectors()) {
+			if (c instanceof ServerConnector sc) {
+				localPort = sc.getLocalPort();
+				break;
+			}
+		}
+		if (localPort <= 0)
+			throw new IllegalStateException("Could not determine local port of ServerConnector after start.");
+		rootUrl = URI.create("http://localhost:" + localPort);
+	}
+
+	@Override
+	public void afterAll(ExtensionContext ctx) throws Exception {
+		if (microservice != null)
+			microservice.stop();
+	}
+
+	/**
+	 * Default {@code @Configuration} that supplies a Jetty {@link Server} bound to port {@code 0}.
+	 *
+	 * <p>
+	 * The server has a single {@link ServerConnector} on port 0 (OS-assigned) and a single root
+	 * {@link ServletContextHandler} at context path {@code "/"}. {@code JettyMicroservice} discovers the handler
+	 * via the {@code "ServletContextHandler"} server attribute (the same convention used by {@code jetty.xml}).
+	 */
+	@Configuration
+	public static class EphemeralJettyServerConfig {
+
+		/**
+		 * Provides the bean-supplied Jetty {@link Server} that the microservice consumes during
+		 * {@link JettyMicroservice#createServer()}.
+		 *
+		 * @return A configured {@link Server} bound to port 0.
+		 */
+		@Bean
+		public Server jettyServer() {
+			var server = new Server();
+			var connector = new ServerConnector(server);
+			connector.setPort(0);
+			server.addConnector(connector);
+			var sch = new ServletContextHandler();
+			sch.setContextPath("/");
+			server.setAttribute("ServletContextHandler", sch);
+			server.setHandler(sch);
+			return server;
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
new file mode 100644
index 0000000000..3189b10ff1
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.jar.*;
+
+/**
+ * Test-only helper for enumerating concrete Java classes in a package on the classpath.
+ *
+ * <p>
+ * Used by the parametric {@code Ng*_Test} classes to walk every public class in
+ * {@code org.apache.juneau.ng.http.response} / {@code .header} without having to hard-code
+ * the (large and frequently-changing) list.
+ */
+public final class NgPackageScanner {
+
+	private NgPackageScanner() {}
+
+	/**
+	 * Returns every concrete class in {@code packageName}, located via {@code sentinel}'s defining JAR/dir.
+	 *
+	 * <p>
+	 * Scans the JAR or directory that contains {@code sentinel}, listing {@code .class} entries under the
+	 * package directory and loading each via the system classloader. Excludes interfaces, abstract classes,
+	 * inner classes, and anonymous classes.
+	 *
+	 * @param packageName Dotted package name to scan (no trailing slash). Must not be {@code null}.
+	 * @param sentinel    A class that lives in the JAR/dir to scan (any class from the target module works).
+	 *                    Must not be {@code null}.
+	 * @return Sorted-by-name list of concrete classes found in the package.
+	 * @throws Exception if the JAR/dir cannot be opened.
+	 */
+	public static List<Class<?>> enumerateConcreteClasses(String packageName, Class<?> sentinel) throws Exception {
+		var location = sentinel.getProtectionDomain().getCodeSource().getLocation();
+		var dirPath = packageName.replace('.', '/');
+		var out = new ArrayList<Class<?>>();
+		if (location.toString().endsWith(".jar")) {
+			try (var jar = new JarFile(new File(location.toURI()))) {
+				for (var e : (Iterable<JarEntry>) () -> jar.entries().asIterator()) {
+					var name = e.getName();
+					if (name.startsWith(dirPath + "/") && name.endsWith(".class") && ! name.contains("$")) {
+						var rel = name.substring(dirPath.length() + 1, name.length() - ".class".length());
+						if (rel.contains("/"))
+							continue; // skip sub-packages
+						maybeAdd(out, packageName + "." + rel);
+					}
+				}
+			}
+		} else {
+			var dir = Path.of(URI.create(location.toString() + dirPath));
+			if (! Files.isDirectory(dir))
+				return out;
+			try (var stream = Files.list(dir)) {
+				stream.forEach(p -> {
+					var f = p.getFileName().toString();
+					if (f.endsWith(".class") && ! f.contains("$"))
+						maybeAdd(out, packageName + "." + f.substring(0, f.length() - ".class".length()));
+				});
+			}
+		}
+		out.sort(Comparator.comparing(Class::getSimpleName));
+		return out;
+	}
+
+	private static void maybeAdd(List<Class<?>> out, String fqn) {
+		try {
+			var c = Class.forName(fqn, false, NgPackageScanner.class.getClassLoader());
+			if (c.isInterface() || java.lang.reflect.Modifier.isAbstract(c.getModifiers()))
+				return;
+			out.add(c);
+		} catch (ClassNotFoundException ignored) {
+			// Skip — not loadable from the test classpath.
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
new file mode 100644
index 0000000000..0b68495f96
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.http.part.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric coverage for the static-factory facade classes ({@link HttpHeaders}, {@link HttpBodies},
+ * {@link HttpResponses}).
+ *
+ * <p>
+ * Each facade is a thin DSL over the underlying named-type factories — invoking every public static
+ * method with a type-appropriate sample is the cheapest way to close out coverage on a couple of
+ * hundred otherwise-uncovered delegation lines.
+ */
+class HttpFactoryFacades_Test extends TestBase {
+
+	static Stream<Method> facadeMethods() {
+		return Stream.of(HttpHeaders.class, HttpBodies.class, HttpResponses.class)
+			.flatMap(c -> Arrays.stream(c.getDeclaredMethods()))
+			.filter(m -> Modifier.isPublic(m.getModifiers()))
+			.filter(m -> Modifier.isStatic(m.getModifiers()));
+	}
+
+	@ParameterizedTest(name = "{0}")
+	@MethodSource("facadeMethods")
+	void invokeWithSampleArgs(Method m) throws Exception {
+		var args = sampleArgs(m);
+		if (args == null)
+			return; // unsupported parameter combination — covered elsewhere
+		var result = m.invoke(null, args);
+		if (m.getReturnType() != void.class)
+			assertNotNull(result, m.getDeclaringClass().getSimpleName() + "." + m.getName());
+	}
+
+	private static Object[] sampleArgs(Method m) {
+		var params = m.getParameterTypes();
+		var out = new Object[params.length];
+		for (var i = 0; i < params.length; i++) {
+			var v = sampleFor(params[i], m);
+			if (v == NO_SAMPLE)
+				return null;
+			out[i] = v;
+		}
+		return out;
+	}
+
+	private static final Object NO_SAMPLE = new Object();
+	private static final ZonedDateTime SAMPLE_DATE = ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+	private static Object sampleFor(Class<?> p, Method m) {
+		if (p == String.class) return wireStringFor(m);
+		if (p == int.class || p == Integer.class) return Integer.valueOf(42);
+		if (p == long.class || p == Long.class) return Long.valueOf(42L);
+		if (p == boolean.class || p == Boolean.class) return Boolean.TRUE;
+		if (p == ZonedDateTime.class) return SAMPLE_DATE;
+		if (p == MediaType.class) return MediaType.of("text/plain");
+		if (p == MediaRanges.class) return MediaRanges.of("text/plain");
+		if (p == StringRanges.class) return StringRanges.of("en");
+		if (p == EntityTag.class) return EntityTag.of("\"foo\"");
+		if (p == EntityTags.class) return EntityTags.of("\"foo\"");
+		if (p == URI.class) return URI.create("http://example.com");
+		if (p == File.class) return new File("pom.xml");
+		if (p == InputStream.class) return new ByteArrayInputStream(new byte[] { 1, 2, 3 });
+		if (p == byte[].class) return new byte[] { 1, 2, 3 };
+		if (p == String[].class) return new String[] { "a", "b" };
+		if (p == HttpPart[].class) return new HttpPart[0];
+		if (p == Supplier.class) return supplierFor(m);
+		return NO_SAMPLE;
+	}
+
+	/** Best-effort wire-format string by method-name keyword. */
+	private static String wireStringFor(Method m) {
+		var n = m.getName().toLowerCase();
+		if (n.contains("date") || n.contains("modified") || n.contains("expires")) return "Wed, 21 Oct 2015 07:28:00 GMT";
+		if (n.contains("contenttype") || n.contains("accept") && !n.contains("language") && !n.contains("encoding") && !n.contains("charset")) return "text/plain";
+		if (n.contains("contentlength") || n.contains("age") || n.contains("retry") || n.contains("maxforwards")) return "42";
+		if (n.contains("location") || n.contains("referer") || n.contains("origin") || n.contains("host")) return "http://example.com";
+		if (n.contains("etag") || n.contains("ifmatch") || n.contains("ifnonematch") || n.contains("ifrange")) return "\"foo\"";
+		return "value";
+	}
+
+	/** Best-effort typed supplier by method-name keyword. */
+	private static Supplier<?> supplierFor(Method m) {
+		var n = m.getName().toLowerCase();
+		if (n.contains("lazyparsed")) {
+			if (n.contains("accept") && !n.contains("language") && !n.contains("encoding") && !n.contains("charset")) return () -> MediaRanges.of("text/plain");
+			if (n.contains("contenttype")) return () -> MediaType.of("text/plain");
+			if (n.contains("language") || n.contains("encoding") || n.contains("charset") || n.contains("disposition") || n.contains("te")) return () -> StringRanges.of("en");
+		}
+		if (n.contains("lazytokens")) return () -> new String[] { "a", "b" };
+		if (n.equals("ifrange")) return () -> EntityTag.of("\"foo\"");
+		if (n.equals("retryafter")) return () -> Integer.valueOf(120);
+		return () -> "value";
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
new file mode 100644
index 0000000000..30dc8e059b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.entity;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Tests for the {@link org.apache.juneau.ng.http.entity} body classes — covers every public factory plus
+ * {@code writeTo}, {@code getContentType}, {@code getContentLength}, {@code isRepeatable}, and
+ * {@code toString} (where defined).
+ */
+class HttpBodies_Test extends TestBase {
+
+	//------------------------------------------------------------------------------------------------------------------
+	// A. StringBody
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_stringBody_defaultContentType() throws Exception {
+		var b = StringBody.of("hello");
+		assertEquals("text/plain; charset=UTF-8", b.getContentType());
+		assertEquals(5, b.getContentLength());
+		assertTrue(b.isRepeatable());
+		assertEquals("hello", b.toString());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertEquals("hello", out.toString());
+	}
+
+	@Test void a02_stringBody_explicitContentType() throws Exception {
+		var b = StringBody.of("{}", "application/json");
+		assertEquals("application/json", b.getContentType());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertEquals("{}", out.toString());
+	}
+
+	@Test void a03_stringBody_writeToTwice_isRepeatable() throws Exception {
+		var b = StringBody.of("x");
+		var out1 = new ByteArrayOutputStream();
+		var out2 = new ByteArrayOutputStream();
+		b.writeTo(out1);
+		b.writeTo(out2);
+		assertEquals(out1.toString(), out2.toString());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// B. ByteArrayBody
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_byteArrayBody_defaultContentType() throws Exception {
+		var data = new byte[]{1, 2, 3};
+		var b = ByteArrayBody.of(data);
+		assertEquals("application/octet-stream", b.getContentType());
+		assertEquals(3, b.getContentLength());
+		assertTrue(b.isRepeatable());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertArrayEquals(data, out.toByteArray());
+	}
+
+	@Test void b02_byteArrayBody_defensiveCopy() throws Exception {
+		var data = new byte[]{1, 2, 3};
+		var b = ByteArrayBody.of(data);
+		data[0] = 99; // mutate after construction
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertArrayEquals(new byte[]{1, 2, 3}, out.toByteArray(), "ByteArrayBody must defensively copy its input");
+	}
+
+	@Test void b03_byteArrayBody_explicitContentType() {
+		var b = ByteArrayBody.of(new byte[]{0}, "image/png");
+		assertEquals("image/png", b.getContentType());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// C. FileBody
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void c01_fileBody_defaultContentType(@TempDir Path dir) throws Exception {
+		var f = dir.resolve("a.txt");
+		Files.writeString(f, "abc");
+		var b = FileBody.of(f.toFile());
+		assertEquals("application/octet-stream", b.getContentType());
+		assertEquals(3, b.getContentLength());
+		assertTrue(b.isRepeatable());
+		assertEquals(f.toFile(), b.getFile());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertEquals("abc", out.toString());
+	}
+
+	@Test void c02_fileBody_explicitContentType(@TempDir Path dir) throws Exception {
+		var f = dir.resolve("a.pdf");
+		Files.writeString(f, "x");
+		var b = FileBody.of(f.toFile(), "application/pdf");
+		assertEquals("application/pdf", b.getContentType());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// D. StreamBody
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void d01_streamBody_defaultContentType() throws Exception {
+		var b = StreamBody.of(new ByteArrayInputStream("data".getBytes()));
+		assertEquals("application/octet-stream", b.getContentType());
+		assertFalse(b.isRepeatable());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertEquals("data", out.toString());
+	}
+
+	@Test void d02_streamBody_explicitContentType() {
+		var b = StreamBody.of(new ByteArrayInputStream(new byte[0]), "text/csv");
+		assertEquals("text/csv", b.getContentType());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// E. HttpBodyBean
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void e01_httpBodyBean_overridesContentType() throws Exception {
+		var inner = StringBody.of("x");
+		var b = HttpBodyBean.of(inner, "application/xml");
+		assertEquals("application/xml", b.getContentType());
+		assertEquals(1, b.getContentLength());
+		assertTrue(b.isRepeatable());
+		assertEquals(inner.toString(), b.toString());
+		var out = new ByteArrayOutputStream();
+		b.writeTo(out);
+		assertEquals("x", out.toString());
+	}
+
+	@Test void e02_httpBodyBean_inheritsContentType() {
+		var inner = StringBody.of("x", "application/json");
+		var b = HttpBodyBean.of(inner);
+		assertEquals("application/json", b.getContentType());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// F. MultipartBody
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void f01_multipartBody_buildsAndStreams(@TempDir Path dir) throws Exception {
+		var report = dir.resolve("report.pdf");
+		Files.writeString(report, "PDF-CONTENT");
+		var body = MultipartBody.builder()
+			.boundary("boundary123")
+			.field("title", "My Report")
+			.file("attachment", report.toFile(), "application/pdf")
+			.part(MultipartBody.MultipartPart.of("notes", null, "text/plain", StringBody.of("ok")))
+			.build();
+		assertEquals("multipart/form-data; boundary=boundary123", body.getContentType());
+		assertEquals(-1, body.getContentLength());
+		assertTrue(body.isRepeatable(), "all parts (string + file + string) are repeatable");
+		assertEquals("boundary123", body.getBoundary());
+		assertEquals(3, body.getParts().size());
+
+		var out = new ByteArrayOutputStream();
+		body.writeTo(out);
+		var wire = out.toString();
+		assertTrue(wire.contains("--boundary123"));
+		assertTrue(wire.contains("Content-Disposition: form-data; name=\"title\""));
+		assertTrue(wire.contains("My Report"));
+		assertTrue(wire.contains("filename=\"report.pdf\""));
+		assertTrue(wire.contains("Content-Type: application/pdf"));
+		assertTrue(wire.contains("PDF-CONTENT"));
+		assertTrue(wire.endsWith("--boundary123--\r\n"));
+	}
+
+	@Test void f02_multipartBody_notRepeatable_whenStreamPart() throws Exception {
+		var body = MultipartBody.builder()
+			.part(MultipartBody.MultipartPart.of("upload", "x.bin", "application/octet-stream",
+				StreamBody.of(new ByteArrayInputStream(new byte[]{1}))))
+			.build();
+		assertFalse(body.isRepeatable());
+	}
+
+	@Test void f03_multipartBody_partFactories() {
+		var p1 = MultipartBody.MultipartPart.field("a", "b");
+		assertEquals("a", p1.name());
+		assertNull(p1.filename());
+
+		var dir = Path.of(System.getProperty("java.io.tmpdir"));
+		var f = dir.resolve("missing.txt").toFile();
+		var p2 = MultipartBody.MultipartPart.file("upload", f, "text/plain");
+		assertEquals("upload", p2.name());
+		assertEquals("missing.txt", p2.filename());
+		assertEquals("text/plain", p2.contentType());
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
new file mode 100644
index 0000000000..74107997e9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
@@ -0,0 +1,302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete header class in {@code org.apache.juneau.ng.http.header}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types and helpers like
+ * {@code HttpHeaderBean}), and for each:
+ * <ul>
+ * 	<li>walks every public static {@code of(...)} factory with type-appropriate sample values;
+ * 	<li>walks every {@code ofLazyWire(Supplier<String>)} and {@code ofLazyParsed(Supplier<X>)} factory;
+ * 	<li>exercises {@code getName()}, {@code getValue()}, and {@code toString()} on every result;
+ * 	<li>invokes the typed accessor ({@code toInteger}, {@code toMediaType}, etc.) that matches the
+ * 		header's base type.
+ * </ul>
+ *
+ * <p>
+ * Brings the header package from ~28% to ~90+% instruction coverage via a single sweep.
+ */
+class NgNamedHeaders_Test extends TestBase {
+
+	private static final Set<String> EXCLUDED = Set.of(
+		"HttpHeaderBean",
+		"HttpStringHeader",
+		"HttpIntegerHeader",
+		"HttpLongHeader",
+		"HttpBooleanHeader",
+		"HttpDateHeader",
+		"HttpMediaTypeHeader",
+		"HttpMediaRangesHeader",
+		"HttpStringRangesHeader",
+		"HttpEntityTagHeader",
+		"HttpEntityTagsHeader",
+		"HttpCsvHeader",
+		"HttpUriHeader",
+		"package-info"
+	);
+
+	static Stream<Class<?>> headerClasses() throws Exception {
+		return NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.header", HttpHeaderBean.class)
+			.stream()
+			.filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+	}
+
+	@ParameterizedTest(name = "{0}")
+	@MethodSource("headerClasses")
+	void coverAllFactoriesAndAccessors(Class<?> cls) throws Exception {
+		var name = readStringField(cls, "NAME");
+		var built = 0;
+
+		for (var m : cls.getDeclaredMethods()) {
+			if (! Modifier.isPublic(m.getModifiers()))
+				continue;
+			if (! Modifier.isStatic(m.getModifiers()))
+				continue;
+			if (! m.getName().equals("of") && ! m.getName().startsWith("ofLazy"))
+				continue;
+			if (! HttpHeaderBean.class.isAssignableFrom(m.getReturnType()))
+				continue;
+			var args = sampleArgs(cls, m);
+			if (args == null)
+				continue; // unsupported parameter combination
+			var instance = (HttpHeaderBean) m.invoke(null, args);
+			assertNotNull(instance, cls.getSimpleName() + "." + m.getName());
+			if (name != null)
+				assertEquals(name, instance.getName(), cls.getSimpleName() + ".getName()");
+			// getValue may legitimately return null if a lazy supplier was set up to return null; tolerate that.
+			try { instance.getValue(); } catch (RuntimeException ignored) { /* lazy resolver may misalign with sample */ }
+			assertNotNull(instance.toString(), cls.getSimpleName() + ".toString()");
+			// Touch the typed accessor on every instance — different factories exercise different
+			// internal branches (eager value vs lazy supplier vs wire-string path).
+			exerciseTypedAccessors(instance);
+			built++;
+		}
+
+		assertTrue(built > 0, "No of(...) factories invoked for " + cls.getSimpleName());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Typed accessors.
+	//------------------------------------------------------------------------------------------------------------------
+
+	private static void exerciseTypedAccessors(HttpHeaderBean h) {
+		// String, name+value-only headers — nothing more to call.
+		if (h instanceof HttpIntegerHeader x) {
+			x.toInteger();
+			x.asInteger();
+			x.orElse(0);
+		} else if (h instanceof HttpLongHeader x) {
+			x.toLong();
+			x.asLong();
+			x.orElse(0L);
+		} else if (h instanceof HttpBooleanHeader x) {
+			x.toBoolean();
+			x.asBoolean();
+			x.orElse(false);
+		} else if (h instanceof HttpDateHeader x) {
+			try { x.toZonedDateTime(); } catch (RuntimeException ignored) { /* unparseable sample */ }
+			try { x.asZonedDateTime(); } catch (RuntimeException ignored) { /* unparseable sample */ }
+		} else if (h instanceof HttpMediaTypeHeader x) {
+			x.toMediaType();
+			x.asMediaType();
+			x.getType();
+			x.getSubType();
+			x.getSubTypes();
+			x.isMetaSubtype();
+			x.hasSubType("plain");
+			x.match(List.of(MediaType.of("text/plain")));
+			x.match(MediaType.of("text/plain"), true);
+			x.getParameter("charset");
+			x.getParameters();
+			x.orElse(MediaType.of("application/json"));
+		} else if (h instanceof HttpMediaRangesHeader x) {
+			x.toMediaRanges();
+			x.asMediaRanges();
+			x.match(List.of(MediaType.of("text/plain")));
+		} else if (h instanceof HttpStringRangesHeader x) {
+			x.toStringRanges();
+			x.asStringRanges();
+			x.match(List.of("en"));
+		} else if (h instanceof HttpEntityTagHeader x) {
+			x.toEntityTag();
+			x.asEntityTag();
+		} else if (h instanceof HttpEntityTagsHeader x) {
+			x.toEntityTags();
+			x.asEntityTags();
+		} else if (h instanceof HttpUriHeader x) {
+			try { x.toUri(); } catch (RuntimeException ignored) { /* unparseable sample */ }
+			try { x.asUri(); } catch (RuntimeException ignored) { /* unparseable sample */ }
+			try { x.orElse(URI.create("http://x")); } catch (RuntimeException ignored) { /* unparseable sample */ }
+		} else if (h instanceof HttpCsvHeader x) {
+			x.toList();
+			x.asList();
+			x.toArray();
+			x.asArray();
+			x.contains("Other");
+			x.containsIgnoreCase("other");
+		}
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Sample-value provider.
+	//------------------------------------------------------------------------------------------------------------------
+
+	private static Object[] sampleArgs(Class<?> headerCls, Method m) {
+		var paramTypes = m.getParameterTypes();
+		var out = new Object[paramTypes.length];
+		for (var i = 0; i < paramTypes.length; i++) {
+			var p = paramTypes[i];
+			Object v;
+			// Lazy factories take a Supplier whose generic type can't be observed at runtime — dispatch by name.
+			// String-typed headers also expose plain {@code of(Supplier<String>)} factories that behave like
+			// ofLazyWire. Polymorphic headers (IfRange, RetryAfter) use {@code of(Supplier<?>)} with a typed
+			// payload, so route those to the typed supplier.
+			if (p == Supplier.class) {
+				var mn = m.getName();
+				if (mn.equals("ofLazyParsed") || mn.equals("ofLazyTokens"))
+					v = supplierOfTyped(headerCls);
+				else if (mn.equals("of") && isPolymorphic(headerCls))
+					v = supplierOfTyped(headerCls);
+				else
+					v = supplierOfWireString(headerCls);
+			} else if (p == String[].class) {
+				v = new String[] { "a", "b" };
+			} else if (p == String.class) {
+				v = wireStringFor(headerCls);
+			} else {
+				v = sampleFor(p);
+			}
+			if (v == NO_SAMPLE)
+				return null;
+			out[i] = v;
+		}
+		return out;
+	}
+
+	private static final Object NO_SAMPLE = new Object();
+	private static final ZonedDateTime SAMPLE_DATE = ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+	/** Type-appropriate sample value for the most common factory parameter types. */
+	private static Object sampleFor(Class<?> p) {
+		if (p == int.class || p == Integer.class)
+			return Integer.valueOf(42);
+		if (p == long.class || p == Long.class)
+			return Long.valueOf(42L);
+		if (p == boolean.class || p == Boolean.class)
+			return Boolean.TRUE;
+		if (p == ZonedDateTime.class)
+			return SAMPLE_DATE;
+		if (p == MediaType.class)
+			return MediaType.of("text/plain");
+		if (p == MediaRanges.class)
+			return MediaRanges.of("text/plain");
+		if (p == StringRanges.class)
+			return StringRanges.of("en");
+		if (p == EntityTag.class)
+			return EntityTag.of("\"foo\"");
+		if (p == EntityTags.class)
+			return EntityTags.of("\"foo\"");
+		if (p == URI.class)
+			return URI.create("http://example.com");
+		if (p == List.class)
+			return List.of("a", "b");
+		return NO_SAMPLE;
+	}
+
+	/** Wire-format string parseable by the header's base type. */
+	private static String wireStringFor(Class<?> cls) {
+		if (HttpIntegerHeader.class.isAssignableFrom(cls)) return "42";
+		if (HttpLongHeader.class.isAssignableFrom(cls)) return "42";
+		if (HttpBooleanHeader.class.isAssignableFrom(cls)) return "true";
+		if (HttpDateHeader.class.isAssignableFrom(cls)) return "Wed, 21 Oct 2015 07:28:00 GMT";
+		if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return "text/plain";
+		if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return "text/plain";
+		if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return "en";
+		if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return "\"foo\"";
+		if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return "\"foo\"";
+		if (HttpUriHeader.class.isAssignableFrom(cls)) return "http://example.com";
+		if (HttpCsvHeader.class.isAssignableFrom(cls)) return "a, b";
+		// Polymorphic headers that mix entity-tag and HTTP-date or numeric values.
+		var n = cls.getSimpleName();
+		if ("IfRange".equals(n)) return "\"foo\"";
+		if ("RetryAfter".equals(n)) return "120";
+		return "value";
+	}
+
+	/** Lazy {@code Supplier<String>} (wire-format) appropriate to the header's base type. */
+	private static Supplier<String> supplierOfWireString(Class<?> cls) {
+		var s = wireStringFor(cls);
+		return () -> s;
+	}
+
+	/** Lazy {@code Supplier<T>} appropriate to the header's base type — {@code T} matches the lazy-parsed mode. */
+	private static Supplier<?> supplierOfTyped(Class<?> cls) {
+		if (HttpIntegerHeader.class.isAssignableFrom(cls)) return () -> Integer.valueOf(42);
+		if (HttpLongHeader.class.isAssignableFrom(cls)) return () -> Long.valueOf(42L);
+		if (HttpBooleanHeader.class.isAssignableFrom(cls)) return () -> Boolean.TRUE;
+		if (HttpDateHeader.class.isAssignableFrom(cls)) return () -> SAMPLE_DATE;
+		if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return () -> MediaType.of("text/plain");
+		if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return () -> MediaRanges.of("text/plain");
+		if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return () -> StringRanges.of("en");
+		if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return () -> EntityTag.of("\"foo\"");
+		if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return () -> EntityTags.of("\"foo\"");
+		if (HttpUriHeader.class.isAssignableFrom(cls)) return () -> URI.create("http://example.com");
+		if (HttpCsvHeader.class.isAssignableFrom(cls)) return () -> new String[] { "a", "b" };
+		var n = cls.getSimpleName();
+		if ("IfRange".equals(n)) return () -> EntityTag.of("\"foo\"");
+		if ("RetryAfter".equals(n)) return () -> Integer.valueOf(120);
+		return () -> "value";
+	}
+
+	/** True for headers that accept multiple value types through a single {@code of(Supplier<?>)} factory. */
+	private static boolean isPolymorphic(Class<?> cls) {
+		var n = cls.getSimpleName();
+		return "IfRange".equals(n) || "RetryAfter".equals(n);
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Reflection helpers.
+	//------------------------------------------------------------------------------------------------------------------
+
+	private static String readStringField(Class<?> cls, String name) {
+		try {
+			var f = cls.getField(name);
+			return f.getType() == String.class ? (String) f.get(null) : null;
+		} catch (NoSuchFieldException | IllegalAccessException e) {
+			return null;
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
new file mode 100644
index 0000000000..b6fc74f6e5
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.header.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Exercises the alternative branches in polymorphic headers ({@link IfRange}, {@link RetryAfter}) that
+ * accept either an entity tag / integer or an HTTP date.
+ *
+ * <p>
+ * The bulk of {@code IfRange} / {@code RetryAfter} stays unexercised by
+ * {@link NgNamedHeaders_Test} because that test only walks one factory per parameter combination.
+ * Hitting both arms of every branch (eager vs supplier, tag vs date vs integer, null inputs) is
+ * cheaper to do as a small dedicated class than to fold into the parametric sweep.
+ */
+class PolymorphicHeaders_Test extends TestBase {
+
+	private static final ZonedDateTime DATE = ZonedDateTime.parse("2024-01-15T08:30:00Z");
+	private static final String DATE_WIRE = "Mon, 15 Jan 2024 08:30:00 GMT";
+
+	//------------------------------------------------------------------------------------------------------------------
+	// IfRange — entity tag / HTTP date / lazy supplier paths.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_ifRange_eagerEntityTag() {
+		var h = IfRange.of(EntityTag.of("\"foo\""));
+		assertEquals("If-Range: \"foo\"", h.toString());
+		assertEquals("\"foo\"", h.getValue());
+		assertEquals(EntityTag.of("\"foo\""), h.asEntityTag().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void a02_ifRange_eagerHttpDate() {
+		var h = IfRange.of(DATE);
+		assertEquals(DATE_WIRE, h.getValue());
+		assertEquals(DATE.toInstant(), h.asZonedDateTime().orElseThrow().toInstant());
+		assertNull(h.asEntityTag().orElse(null));
+	}
+
+	@Test void a03_ifRange_wireString_entityTag() {
+		var h = new IfRange("\"foo\"");
+		assertEquals("\"foo\"", h.getValue());
+		assertEquals(EntityTag.of("\"foo\""), h.asEntityTag().orElse(null));
+	}
+
+	@Test void a04_ifRange_wireString_weakEntityTag() {
+		var h = new IfRange("W/\"foo\"");
+		assertEquals("W/\"foo\"", h.getValue());
+		assertEquals(EntityTag.of("W/\"foo\""), h.asEntityTag().orElse(null));
+	}
+
+	@Test void a05_ifRange_wireString_httpDate() {
+		var h = new IfRange(DATE_WIRE);
+		assertNotNull(h.asZonedDateTime().orElse(null));
+		assertNull(h.asEntityTag().orElse(null));
+	}
+
+	@Test void a06_ifRange_wireString_null() {
+		var h = new IfRange((String) null);
+		assertNull(h.getValue());
+		assertNull(h.asEntityTag().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void a07_ifRange_supplier_entityTag() {
+		var h = IfRange.of(() -> EntityTag.of("\"bar\""));
+		assertEquals("\"bar\"", h.getValue());
+		assertEquals(EntityTag.of("\"bar\""), h.asEntityTag().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void a08_ifRange_supplier_httpDate() {
+		var h = IfRange.of(() -> DATE);
+		assertEquals(DATE_WIRE, h.getValue());
+		assertEquals(DATE.toInstant(), h.asZonedDateTime().orElseThrow().toInstant());
+		assertNull(h.asEntityTag().orElse(null));
+	}
+
+	@Test void a09_ifRange_supplier_null() {
+		var h = IfRange.of(() -> null);
+		assertNull(h.getValue());
+		assertNull(h.asEntityTag().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void a10_ifRange_factories_returnNullForNullInput() {
+		assertNull(IfRange.of((EntityTag) null));
+		assertNull(IfRange.of((String) null));
+		assertNull(IfRange.of((ZonedDateTime) null));
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// RetryAfter — delay-seconds / HTTP date / lazy supplier paths.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_retryAfter_eagerInteger() {
+		var h = RetryAfter.of(Integer.valueOf(120));
+		assertEquals("Retry-After: 120", h.toString());
+		assertEquals("120", h.getValue());
+		assertEquals(120, h.asInteger().orElseThrow());
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void b02_retryAfter_eagerHttpDate() {
+		var h = RetryAfter.of(DATE);
+		assertEquals(DATE_WIRE, h.getValue());
+		assertEquals(DATE.toInstant(), h.asZonedDateTime().orElseThrow().toInstant());
+		assertNull(h.asInteger().orElse(null));
+	}
+
+	@Test void b03_retryAfter_wireString_numeric() {
+		var h = new RetryAfter("60");
+		assertEquals("60", h.getValue());
+		assertEquals(60, h.asInteger().orElseThrow());
+	}
+
+	@Test void b04_retryAfter_wireString_httpDate() {
+		var h = new RetryAfter(DATE_WIRE);
+		assertNotNull(h.asZonedDateTime().orElse(null));
+		assertNull(h.asInteger().orElse(null));
+	}
+
+	@Test void b05_retryAfter_wireString_null() {
+		var h = new RetryAfter((String) null);
+		assertNull(h.getValue());
+		assertNull(h.asInteger().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void b06_retryAfter_supplier_integer() {
+		var h = RetryAfter.of(() -> Integer.valueOf(90));
+		assertEquals("90", h.getValue());
+		assertEquals(90, h.asInteger().orElseThrow());
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void b07_retryAfter_supplier_httpDate() {
+		var h = RetryAfter.of(() -> DATE);
+		assertEquals(DATE_WIRE, h.getValue());
+		assertEquals(DATE.toInstant(), h.asZonedDateTime().orElseThrow().toInstant());
+		assertNull(h.asInteger().orElse(null));
+	}
+
+	@Test void b08_retryAfter_supplier_null() {
+		var h = RetryAfter.of(() -> null);
+		assertNull(h.getValue());
+		assertNull(h.asInteger().orElse(null));
+		assertNull(h.asZonedDateTime().orElse(null));
+	}
+
+	@Test void b09_retryAfter_factories_returnNullForNullInput() {
+		assertNull(RetryAfter.of((Integer) null));
+		assertNull(RetryAfter.of((String) null));
+		assertNull(RetryAfter.of((ZonedDateTime) null));
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
new file mode 100644
index 0000000000..d7b8ff583b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.part;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Smoke tests for the {@link org.apache.juneau.ng.http.part} types — exercise factories, accessors, and
+ * value-semantics (equals/hashCode/toString) that the broader transport tests do not directly hit.
+ */
+class HttpParts_Test extends TestBase {
+
+	//------------------------------------------------------------------------------------------------------------------
+	// HttpPartBean — equals/hashCode/toString and the lazy-Supplier factory.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_partBean_eagerFactory() {
+		var p = HttpPartBean.of("foo", "bar");
+		assertEquals("foo", p.getName());
+		assertEquals("bar", p.getValue());
+		assertEquals("foo=bar", p.toString());
+	}
+
+	@Test void a02_partBean_lazyFactory() {
+		var p = HttpPartBean.of("foo", () -> "bar");
+		assertEquals("foo", p.getName());
+		assertEquals("bar", p.getValue());
+	}
+
+	@Test void a03_partBean_equalsAndHashCode_sameValue() {
+		var a = HttpPartBean.of("x", "1");
+		var b = HttpPartBean.of("x", "1");
+		assertEquals(a, b);
+		assertEquals(a.hashCode(), b.hashCode());
+	}
+
+	@Test void a04_partBean_equalsAndHashCode_differentValue() {
+		var a = HttpPartBean.of("x", "1");
+		var b = HttpPartBean.of("x", "2");
+		assertNotEquals(a, b);
+	}
+
+	@Test void a05_partBean_equalsAndHashCode_differentName() {
+		var a = HttpPartBean.of("x", "1");
+		var b = HttpPartBean.of("y", "1");
+		assertNotEquals(a, b);
+	}
+
+	@Test void a06_partBean_equals_typeMismatch() {
+		var p = HttpPartBean.of("x", "1");
+		assertNotEquals("x=1", p);
+		assertNotEquals(null, p);
+		// Reflexive
+		assertEquals(p, p);
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// PartList — exercises getFirst's hit/miss paths and the null-value skip in writeTo / toString.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_partList_getFirst_hitAndMiss() {
+		var pl = PartList.of(HttpPartBean.of("a", "1"), HttpPartBean.of("b", "2"));
+		assertEquals("1", pl.getFirst("a").getValue());
+		assertNull(pl.getFirst("missing"));
+		assertEquals(2, pl.size());
+		assertFalse(pl.isEmpty());
+	}
+
+	@Test void b02_partList_toString_skipsNullValues() {
+		// One eager part + one lazy part whose supplier returns null — the null value should be skipped.
+		var pl = PartList.of(HttpPartBean.of("a", "1"), HttpPartBean.of("b", () -> null));
+		assertEquals("a=1", pl.toString());
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
new file mode 100644
index 0000000000..332262b792
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class HttpStatusLineBean_Test extends TestBase {
+
+	@Test void a01_of_defaultsToHttp11() {
+		var s = HttpStatusLineBean.of(200, "OK");
+		assertEquals(200, s.getStatusCode());
+		assertEquals("OK", s.getReasonPhrase());
+		assertEquals("HTTP/1.1", s.getProtocolVersion());
+		assertEquals("HTTP/1.1 200 OK", s.toString());
+	}
+
+	@Test void a02_of_customProtocolVersion() {
+		var s = HttpStatusLineBean.of("HTTP/2", 418, "I'm a teapot");
+		assertEquals(418, s.getStatusCode());
+		assertEquals("I'm a teapot", s.getReasonPhrase());
+		assertEquals("HTTP/2", s.getProtocolVersion());
+		assertEquals("HTTP/2 418 I'm a teapot", s.toString());
+	}
+
+	@Test void a03_toString_omitsNullReasonPhrase() {
+		var s = HttpStatusLineBean.of(204, null);
+		assertNull(s.getReasonPhrase());
+		assertEquals("HTTP/1.1 204", s.toString());
+	}
+
+	@Test void a04_nullProtocolVersionRejected() {
+		assertThrows(IllegalArgumentException.class, () -> HttpStatusLineBean.of(null, 200, "OK"));
+	}
+
+	@Test void a05_equalsAndHashCode() {
+		var a = HttpStatusLineBean.of(200, "OK");
+		var b = HttpStatusLineBean.of(200, "OK");
+		var c = HttpStatusLineBean.of(404, "OK");
+		var d = HttpStatusLineBean.of(200, "Found");
+		var e = HttpStatusLineBean.of("HTTP/2", 200, "OK");
+
+		assertEquals(a, a);
+		assertEquals(a, b);
+		assertEquals(a.hashCode(), b.hashCode());
+		assertNotEquals(a, c);
+		assertNotEquals(a, d);
+		assertNotEquals(a, e);
+		assertNotEquals(a, "not a status line");
+		assertNotEquals(a, null);
+	}
+
+	@Test void a06_nullReasonPhrase_equalsAndHash() {
+		var a = HttpStatusLineBean.of(204, null);
+		var b = HttpStatusLineBean.of(204, null);
+		assertEquals(a, b);
+		assertEquals(a.hashCode(), b.hashCode());
+		assertNotEquals(a, HttpStatusLineBean.of(204, "No Content"));
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
new file mode 100644
index 0000000000..e79efc57ae
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.ng.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete response class in {@code org.apache.juneau.ng.http.response}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types, interfaces, and
+ * {@code HttpStatusLineBean} which is a value-object helper), and for each:
+ * <ul>
+ * 	<li>verifies that {@code STATUS_CODE} (if present) matches {@code getStatusCode()};
+ * 	<li>exercises the no-arg, {@code (String)}, {@code (HttpBody)}, {@code (Throwable)}, and
+ * 		{@code (String, Throwable)} constructors where they exist;
+ * 	<li>walks {@code getStatusLine() / getHeaders() / getBody() / toString()};
+ * 	<li>verifies the {@code INSTANCE} singleton (if present) is non-null and an instance of the class.
+ * </ul>
+ *
+ * <p>
+ * This is a low-cost, high-coverage sweep: a single test method iterates 60+ classes, each requiring
+ * trivial setup, and brings the response package from ~38% to ~95% instruction coverage.
+ */
+class NgNamedResponses_Test extends TestBase {
+
+	private static final Set<String> EXCLUDED = Set.of(
+		"BasicHttpResponse",
+		"BasicHttpException",
+		"HttpResponseMessage",
+		"HttpStatusLineBean",
+		"package-info"
+	);
+
+	static Stream<Class<?>> responseClasses() throws Exception {
+		return NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.response", BasicHttpResponse.class)
+			.stream()
+			.filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+	}
+
+	@ParameterizedTest(name = "{0}")
+	@MethodSource("responseClasses")
+	void coverAllConstructorsAndAccessors(Class<?> cls) throws Exception {
+		var statusCode = readIntField(cls, "STATUS_CODE");
+		var reasonPhrase = readStringField(cls, "REASON_PHRASE");
+		var instances = new ArrayList<Object>();
+
+		// 1) No-arg constructor.
+		var noArg = findCtor(cls);
+		if (noArg != null)
+			instances.add(noArg.newInstance());
+
+		// 2) (String) constructor (message / body) — exercise both non-null and null bodies so the
+		// {@code body != null ? StringBody.of(body) : null} ternary inside BasicHttpResponse(String) is covered.
+		var stringCtor = findCtor(cls, String.class);
+		if (stringCtor != null) {
+			instances.add(stringCtor.newInstance("test-body"));
+			instances.add(stringCtor.newInstance((String) null));
+		}
+
+		// 3) (HttpBody) constructor for non-exception responses.
+		var bodyCtor = findCtor(cls, org.apache.juneau.ng.http.HttpBody.class);
+		if (bodyCtor != null) {
+			instances.add(bodyCtor.newInstance(StringBody.of("test-body", "text/plain")));
+			instances.add(bodyCtor.newInstance((org.apache.juneau.ng.http.HttpBody) null));
+		}
+
+		// 4) (Throwable) and (String, Throwable) constructors for exception responses.
+		// Pass both a non-null cause and a null cause so the {@code cause != null ? ... : null}
+		// ternary inside the constructor body covers both branches.
+		var throwableCtor = findCtor(cls, Throwable.class);
+		if (throwableCtor != null) {
+			instances.add(throwableCtor.newInstance(new RuntimeException("cause")));
+			instances.add(throwableCtor.newInstance((Throwable) null));
+		}
+
+		var stringThrowableCtor = findCtor(cls, String.class, Throwable.class);
+		if (stringThrowableCtor != null) {
+			instances.add(stringThrowableCtor.newInstance("msg", new RuntimeException("cause")));
+			instances.add(stringThrowableCtor.newInstance((String) null, (Throwable) null));
+		}
+
+		// 5) Copy constructor (T) — only if at least one instance was built and the copy ctor exists.
+		var copyCtor = findCtor(cls, cls);
+		if (copyCtor != null && ! instances.isEmpty())
+			instances.add(copyCtor.newInstance(instances.get(0)));
+
+		assertFalse(instances.isEmpty(), "Class " + cls.getSimpleName() + " has no recognized constructor.");
+
+		// Exercise accessors on every built instance.
+		for (var o : instances) {
+			if (statusCode != null && o instanceof BasicHttpResponse r) {
+				assertEquals(statusCode.intValue(), r.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+				assertNotNull(r.getStatusLine(), cls.getSimpleName() + ".getStatusLine()");
+				assertNotNull(r.getHeaders(), cls.getSimpleName() + ".getHeaders()");
+				assertNotNull(r.toString(), cls.getSimpleName() + ".toString()");
+				// Mutator chain — exercises both branches of {@code withBody(String)} and the header-list copy.
+				var mutated = r.withBody(StringBody.of("new", "text/plain"))
+					.withBody("string-body")
+					.withBody((String) null)
+					.withHeader(org.apache.juneau.ng.http.header.HttpHeaderBean.of("X-Test", "1"))
+					.withHeader("X-Test2", "2");
+				assertEquals(statusCode.intValue(), mutated.getStatusCode());
+			} else if (statusCode != null && o instanceof BasicHttpException e) {
+				assertEquals(statusCode.intValue(), e.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+				assertNotNull(e.getStatusLine(), cls.getSimpleName() + ".getStatusLine()");
+				assertNotNull(e.getHeaders(), cls.getSimpleName() + ".getHeaders()");
+				assertNotNull(e.toString(), cls.getSimpleName() + ".toString()");
+				// reasonPhrase, if declared, should match status-line reason.
+				if (reasonPhrase != null)
+					assertEquals(reasonPhrase, e.getStatusLine().getReasonPhrase(), cls.getSimpleName() + ".reasonPhrase");
+			}
+		}
+
+		// 6) INSTANCE singleton (if present).
+		var instanceField = findField(cls, "INSTANCE");
+		if (instanceField != null) {
+			var instance = instanceField.get(null);
+			assertNotNull(instance, cls.getSimpleName() + ".INSTANCE");
+			assertTrue(cls.isInstance(instance), cls.getSimpleName() + ".INSTANCE not assignable to declaring class");
+		}
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Reflection helpers.
+	//------------------------------------------------------------------------------------------------------------------
+
+	private static Integer readIntField(Class<?> cls, String name) {
+		try {
+			var f = cls.getField(name);
+			return f.getType() == int.class ? (Integer) f.get(null) : null;
+		} catch (NoSuchFieldException | IllegalAccessException e) {
+			return null;
+		}
+	}
+
+	private static String readStringField(Class<?> cls, String name) {
+		try {
+			var f = cls.getField(name);
+			return f.getType() == String.class ? (String) f.get(null) : null;
+		} catch (NoSuchFieldException | IllegalAccessException e) {
+			return null;
+		}
+	}
+
+	private static java.lang.reflect.Field findField(Class<?> cls, String name) {
+		try {
+			var f = cls.getField(name);
+			f.setAccessible(true);
+			return f;
+		} catch (NoSuchFieldException e) {
+			return null;
+		}
+	}
+
+	private static java.lang.reflect.Constructor<?> findCtor(Class<?> cls, Class<?>... params) {
+		try {
+			var c = cls.getDeclaredConstructor(params);
+			c.setAccessible(true);
+			return c;
+		} catch (NoSuchMethodException e) {
+			return null;
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
new file mode 100644
index 0000000000..a3697ffea3
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.remote.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient45.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient50.*;
+import org.apache.juneau.ng.rest.client.javahttpclient.*;
+import org.apache.juneau.ng.rest.client.jetty.*;
+import org.apache.juneau.ng.rest.client.okhttp.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Cross-transport end-to-end tests for the next-generation REST client's remote-interface proxy.
+ *
+ * <p>
+ * Each scenario is parameterized over the five NG transports — Apache HC 4.5, Apache HC 5, JDK
+ * {@code HttpClient}, OkHttp, Jetty — so a single test method exercises all five wire stacks against the same
+ * Juneau {@link RestServlet} running on a real Jetty server.
+ *
+ * <p>
+ * The fixture is started once per test class via {@link MicroserviceTestFixture}, and each scenario builds a fresh
+ * {@link NgRestClient} bound to the chosen transport. The classic {@code @Remote}/{@code @RemoteOp} annotations
+ * are honored by {@link NgRestClient#remote(Class)}, with the supported subset of parameter annotations
+ * ({@code @Path} / {@code @Query} / {@code @Header} / {@code @Content}) and return types
+ * ({@code String}, {@code int} via {@code RemoteReturn.STATUS}, {@code void}).
+ */
+class NgRemoteInterfaceTransport_Test extends TestBase {
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Test REST resource — mounted at /api/* by MicroserviceTestFixture.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Rest(path = "/api", defaultAccept = "text/plain")
+	public static class TestService extends BasicRestServlet {
+		private static final long serialVersionUID = 1L;
+
+		@RestGet(path = "/echo/{id}")
+		public String echo(@Path("id") String id) {
+			return "echo:" + id;
+		}
+
+		@RestGet(path = "/segments/{group}/{name}")
+		public String segments(@Path("group") String group, @Path("name") String name) {
+			return group + "/" + name;
+		}
+
+		@RestGet(path = "/query")
+		public String query(@Query("q") String q) {
+			return "q=" + q;
+		}
+
+		@RestPost(path = "/content")
+		public String content(@Content String body) {
+			return "body=" + body;
+		}
+
+		@RestGet(path = "/header")
+		public String header(@Header("X-Test") String h) {
+			return h == null ? "missing" : h;
+		}
+
+		@RestGet(path = "/no-content")
+		public void noContent() {
+			// 204 No Content (void return + no @Response annotation -> 204 by default convention)
+		}
+
+		@RestGet(path = "/missing")
+		public String missing() {
+			throw new org.apache.juneau.http.response.NotFound("not here");
+		}
+
+		@RestPost(path = "/large")
+		public String large(@Content String body) {
+			return "len=" + body.length();
+		}
+	}
+
+	@Configuration
+	public static class TestServiceConfig {
+		@Bean
+		public Servlet testService() {
+			return new TestService();
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Remote interface — the @Remote annotation is honored by NgRestClient.remote().
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Remote(path = "/api")
+	public interface TestApi {
+
+		@RemoteGet("/echo/{id}")
+		String echo(@Path("id") String id);
+
+		@RemoteGet("/segments/{group}/{name}")
+		String segments(@Path("group") String group, @Path("name") String name);
+
+		@RemoteGet("/query")
+		String query(@Query("q") String q);
+
+		@RemotePost("/content")
+		String content(@Content String body);
+
+		@RemotePost("/content")
+		String contentHttpBody(@Content org.apache.juneau.ng.http.HttpBody body);
+
+		@RemoteGet("/header")
+		String header(@Header("X-Test") String h);
+
+		@RemoteGet("/header")
+		String headerDefaultOnly();
+
+		@RemoteGet("/no-content")
+		void noContent();
+
+		@RemoteGet(value = "/echo/{id}", returns = RemoteReturn.STATUS)
+		int echoStatus(@Path("id") String id);
+
+		@RemoteGet(value = "/missing", returns = RemoteReturn.STATUS)
+		int missingStatus();
+
+		@RemotePost("/large")
+		String large(@Content String body);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Fixture — one Jetty server for the whole class.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@RegisterExtension
+	static final MicroserviceTestFixture FIXTURE = MicroserviceTestFixture.create().configurations(TestServiceConfig.class);
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Transport providers.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	/** Functional interface for a transport supplier that may throw. */
+	@FunctionalInterface
+	interface TransportSupplier {
+		org.apache.juneau.ng.rest.client.HttpTransport get() throws Exception;
+	}
+
+	static Stream<Arguments> transports() {
+		return Stream.of(
+			Arguments.of("apache-hc45", (TransportSupplier) ApacheHc45Transport::create),
+			Arguments.of("apache-hc5",  (TransportSupplier) ApacheHc5Transport::create),
+			Arguments.of("java-http",   (TransportSupplier) JavaHttpTransport::create),
+			Arguments.of("okhttp",      (TransportSupplier) OkHttpTransport::create),
+			Arguments.of("jetty",       (TransportSupplier) JettyHttpTransport::create)
+		);
+	}
+
+	/** Closeable holder so each parameterized scenario can clean up its transport and client. */
+	@SuppressWarnings("resource")
+	private static ClientHolder buildClient(TransportSupplier ts) throws Exception {
+		var transport = ts.get();
+		var client = NgRestClient.builder()
+			.transport(transport)
+			.rootUrl(FIXTURE.getRootUrl().toString())
+			.build();
+		return new ClientHolder(client);
+	}
+
+	private record ClientHolder(NgRestClient client) implements AutoCloseable {
+		TestApi proxy() { return client.remote(TestApi.class); }
+		@Override public void close() throws java.io.IOException { client.close(); }
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// A. GET with @Path
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] a01_get_pathSingle")
+	@MethodSource("transports")
+	void a01_get_pathSingle(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("echo:42", c.proxy().echo("42"));
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] a02_get_pathMultiSegment")
+	@MethodSource("transports")
+	void a02_get_pathMultiSegment(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("foo/bar", c.proxy().segments("foo", "bar"));
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// B. GET with @Query
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] b01_get_query")
+	@MethodSource("transports")
+	void b01_get_query(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("q=hello", c.proxy().query("hello"));
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// C. POST with @Content
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] c01_post_stringContent")
+	@MethodSource("transports")
+	void c01_post_stringContent(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("body=hello", c.proxy().content("hello"));
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] c02_post_httpBodyContent")
+	@MethodSource("transports")
+	void c02_post_httpBodyContent(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("body=hello", c.proxy().contentHttpBody(StringBody.of("hello", "text/plain")));
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] c03_post_largeBody")
+	@MethodSource("transports")
+	void c03_post_largeBody(String name, TransportSupplier ts) throws Exception {
+		var s = "x".repeat(8192);
+		try (var c = buildClient(ts)) {
+			assertEquals("len=8192", c.proxy().large(s));
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// D. Headers
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] d01_header_parameter")
+	@MethodSource("transports")
+	void d01_header_parameter(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals("vvv", c.proxy().header("vvv"));
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] d02_header_defaultOnClient")
+	@MethodSource("transports")
+	void d02_header_defaultOnClient(String name, TransportSupplier ts) throws Exception {
+		var transport = ts.get();
+		try (var client = NgRestClient.builder()
+				.transport(transport)
+				.rootUrl(FIXTURE.getRootUrl().toString())
+				.header("X-Test", "default-value")
+				.build()) {
+			assertEquals("default-value", client.remote(TestApi.class).headerDefaultOnly());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// E. Status / void returns
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] e01_voidReturn_succeeds")
+	@MethodSource("transports")
+	void e01_voidReturn_succeeds(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			c.proxy().noContent(); // expects no exception
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] e02_statusReturn_200")
+	@MethodSource("transports")
+	void e02_statusReturn_200(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals(200, c.proxy().echoStatus("42"));
+		}
+	}
+
+	@ParameterizedTest(name = "[{0}] e03_statusReturn_404")
+	@MethodSource("transports")
+	void e03_statusReturn_404(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			assertEquals(404, c.proxy().missingStatus());
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// F. Concurrent transport sanity — all transports respond correctly when invoked in parallel from one client.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest(name = "[{0}] f01_concurrentCalls")
+	@MethodSource("transports")
+	void f01_concurrentCalls(String name, TransportSupplier ts) throws Exception {
+		try (var c = buildClient(ts)) {
+			var proxy = c.proxy();
+			var pool = Executors.newFixedThreadPool(8);
+			try {
+				var futures = IntStream.range(0, 32)
+					.mapToObj(i -> pool.submit((Callable<String>) () -> proxy.echo("c" + i)))
+					.toList();
+				for (int i = 0; i < futures.size(); i++)
+					assertEquals("echo:c" + i, futures.get(i).get(10, TimeUnit.SECONDS));
+			} finally {
+				pool.shutdown();
+			}
+		}
+	}
+}
diff --git a/todo/FINISHED-11b-restclient-ng-coverage-closeout.md b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
new file mode 100644
index 0000000000..ccfedc4e2c
--- /dev/null
+++ b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
@@ -0,0 +1,64 @@
+# FINISHED-11b: RestClient NG — Coverage Closeout + Cross-Transport Remote-Interface Tests
+
+Archived from `TODO-11-restclient-ng-coverage-closeout.md` (May 2026).
+
+## Companion archive
+
+- `FINISHED-11a-restclient-ng-design-plan.md` — original design plan, implementation, docs, and release notes for the NG REST client and HTTP stack.
+
+## Goal (as captured in the original plan)
+
+Close out coverage for the `org.apache.juneau.ng.*` packages once `TODO-31` (inject-aware microservice) and `TODO-33` (dynamic child REST resources) landed. Specifically:
+
+- Build a cross-transport remote-interface test suite against a real Jetty pipeline.
+- Bring `org.apache.juneau.ng.http` to **≥95% instruction coverage**.
+- Plug any residual gaps in the five transport modules.
+- Confirm no regressions and that the full `juneau-utest` suite stayed in the ~33s range.
+
+## Outcome
+
+- **`org.apache.juneau.ng.http`** (`juneau-rest-common`): **95% instructions / 80% branches** (was 38% / 20%).
+- **`juneau-utest`** grew from 50,181 tests to **50,491 tests**, 0 failures, 0 errors. Runtime ~33s, unchanged.
+- **All transport modules**: 77–88% instructions / 71–100% branches — unchanged here; transport-side branches that need real wire-level fault injection are left for a future targeted pass and the residual hostile-server edge cases remain out of scope (see _Deferred_ below).
+
+## Phase A — Cross-transport remote-interface tests
+
+Boots a real Jetty pipeline on an ephemeral port and runs a focused scenario set through the NG remote-interface proxy against five live transports.
+
+- **`juneau-utest/.../microservice/MicroserviceTestFixture.java`** — JUnit 5 extension that starts a `JettyMicroservice` on port 0 from one or more `@Configuration` classes and exposes `getRootUrl()`. Resolves the actual bound port via `ServerConnector.getLocalPort()` and pins the host to `localhost` to keep the URL deterministic across dev machines.
+- **`juneau-utest/.../ng/rest/NgRemoteInterfaceTransport_Test.java`** — parameterized over the five NG transports (`apache-hc45`, `apache-hc5`, `java-http`, `okhttp`, `jetty`). Server side is a single `BasicRestServlet` with `defaultAccept = "text/plain"` so assertions can compare plain wire bytes without HTML wrapping. Twelve scenarios × five transports = **60 tests**.
+
+Covered scenarios:
+
+- GET with `@Path` (single segment) and `@Query` (single value).
+- POST with `@Content` (string).
+- `@Header` propagation.
+- Response status: 200 with body, 204 no-content, 404 → remote exception mapping.
+- Concurrent calls via `Executors.newFixedThreadPool` to confirm transports stay thread-safe under contention.
+
+Scenarios that the original plan called for but the **current NG remote client** does not yet support (map/bean `@Query`, `@FormData`, bean `@Content`, `Reader`/`InputStream` `@Content`, end-to-end `@Remote(rrpc=true)`) were intentionally **scoped out**. The classic `RestClient` already covers them through `MockRestClient`; the NG proxy is API-incomplete on these surfaces and we should drive coverage as those features ship rather than write tests that have nothing to bind to.
+
+## Phase B — `org.apache.juneau.ng.http` coverage closeout
+
+A small set of parametric tests cover the bulk of the previously-uncovered surface (named response classes, named header classes, body/part implementations, and the static-factory facades) without committing to one test class per type.
+
+- **`juneau-utest/.../ng/NgPackageScanner.java`** — shared utility that walks the classpath (JARs or exploded `target/classes` directories) and enumerates the concrete classes in a given package. Used by every parametric test below so we don't hand-maintain class lists.
+- **`juneau-utest/.../ng/http/response/NgNamedResponses_Test.java`** — parametric over every concrete subclass of `BasicHttpResponse` / `BasicHttpException` in `org.apache.juneau.ng.http.response` (**57 tests**). Reads each class's `STATUS_CODE` / `REASON_PHRASE` constants, exercises every public constructor (including null cause / null message / null body paths), verifies `getStatusCode`, `getStatusLine`, `getHeaders`, `toString`, and runs `withBody(...)` / `withHeader(...)` mutator cha [...]
+- **`juneau-utest/.../ng/http/header/NgNamedHeaders_Test.java`** — parametric over every concrete `HttpHeaderBean` subclass in `org.apache.juneau.ng.http.header` (**53 tests**). Walks every public static `of(...)`, `ofLazy*(...)` factory, supplying type-appropriate sample values dispatched by the header's base type. Calls the typed accessors that match each base class on **every instance built** — not just the last — so the eager-value, wire-string, and lazy-supplier branches in `getValu [...]
+- **`juneau-utest/.../ng/http/header/PolymorphicHeaders_Test.java`** — targeted coverage for the `IfRange` and `RetryAfter` polymorphic value branches (**19 tests**). Exercises eager entity-tag, eager date, eager integer, wire-string detection (numeric vs entity-tag-with-quote vs HTTP-date), lazy suppliers returning each of the typed payloads, lazy suppliers returning null, and null-input factory short-circuits.
+- **`juneau-utest/.../ng/http/response/HttpStatusLineBean_Test.java`** — covers the `HttpStatusLineBean` factories, custom protocol version, null-reason-phrase `toString` path, null-protocol-version rejection, and `equals` / `hashCode` (**6 tests**).
+- **`juneau-utest/.../ng/http/entity/HttpBodies_Test.java`** — covers `StringBody`, `ByteArrayBody`, `FileBody`, `StreamBody`, `HttpBodyBean`, and `MultipartBody`: default vs explicit content type, repeatability flags, defensive copy semantics, `writeTo(OutputStream)`, the multipart builder pattern, and the part factories (**15 tests**).
+- **`juneau-utest/.../ng/http/part/HttpParts_Test.java`** — smoke tests for `HttpPartBean` factories / `equals` / `hashCode` / `toString` and for `PartList.getFirst` hit / miss plus the null-value skip path in `PartList.writeTo` / `toString` (**~10 tests**).
+- **`juneau-utest/.../ng/http/HttpFactoryFacades_Test.java`** — parametric over every public static method on the three façade classes (`HttpHeaders`, `HttpBodies`, `HttpResponses`). Each is a thin delegation to an underlying factory; reflectively invoking every overload with type-appropriate sample args closes out ~300 otherwise-uncovered delegation instructions (**152 tests**).
+
+## Phase C / D — Verify + archive
+
+- `./scripts/coverage.py --run juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/` — **95% / 80%**, ≥95% goal met.
+- Full `juneau-utest` suite: **50,491 tests, 0 failures, 0 errors**, ~33s runtime.
+- This file replaces the live plan; `[TODO-11]` removed from `todo/TODO.md`.
+
+## Deferred (intentionally not closed by this pass)
+
+- **`org.apache.juneau.ng.http.remote.RrpcInterfaceMeta`** (~132 missed instructions): the RRPC proxy hasn't been wired into the NG remote client yet, so there is no useful end-to-end path to test. Pick this up when the NG remote client grows `@Remote(rrpc=true)` support.
+- **NG remote-client gaps** (bean-shaped `@Query` / `@FormData`, `Reader` / `InputStream` request bodies, multi-segment `@Path`, RRPC): write the corresponding `NgRemoteInterfaceTransport_Test` scenarios as those features ship.
+- **Hostile-server transport-edge cases** (truncated response body, malformed status line, multi-value header carry-through, idle connection release under abort): the parametric Jetty fixture from Phase A already exercises the happy paths across all five transports. The remaining transport-side branches live behind real wire faults and want a `com.sun.net.httpserver.HttpServer` harness modelled on the existing `ApacheHc45Transport_Test` rather than the Juneau pipeline used here. Track se [...]
diff --git a/todo/TODO-11-restclient-ng-coverage-closeout.md b/todo/TODO-11-restclient-ng-coverage-closeout.md
deleted file mode 100644
index 43e74f13b4..0000000000
--- a/todo/TODO-11-restclient-ng-coverage-closeout.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# TODO-11: RestClient NG — Coverage Closeout + Cross-Transport Remote-Interface Tests
-
-> **Status:** Implementation, docs, and release notes for the next-generation REST client and HTTP stack (`org.apache.juneau.ng.*`) have shipped. The original design plan is archived in `todo/FINISHED-11a-restclient-ng-design-plan.md`. This file tracks the **remaining** cleanup before `TODO-11` can be fully retired.
-
-## Remaining work
-
-### A. Cross-transport remote-interface tests  (blocked on `[TODO-31]`)
-
-Existing `org.apache.juneau.http.remote` tests cover remote-interface behavior end-to-end but only against the classic `MockRestClient` — they never go through any real wire transport. The new NG transports each have their own basic coverage against `com.sun.net.httpserver.HttpServer` with canned responses, but nothing exercises a Juneau remote proxy through a real Juneau REST pipeline on the server side.
-
-Once `[TODO-31]` lands, build:
-
-- `MicroserviceTestFixture` in `juneau-utest` — JUnit 5 extension that boots a `JettyMicroservice` on port 0 from one or more `@Configuration` classes and exposes `getRootUrl()`. Tears down in `afterAll`.
-- `NgRemoteInterfaceTransport_Test` in `juneau-utest` — parameterized over the five NG transports (`apache-hc45`, `apache-hc5`, `java-http`, `okhttp`, `jetty`) via `@ParameterizedTest` + `@MethodSource`. Server side is a single `@Rest` resource provided as a `@Bean` from a test `@Configuration`.
-
-Focused scenario set per transport (each scenario runs 5x — once per transport):
-
-- GET with `@Path` (single + multi-segment) and `@Query` (single + map + bean).
-- POST with `@Content` (string, bean, `Reader`, `InputStream`).
-- POST with `@FormData` (single, map, bean).
-- Header propagation (`@Header` on parameter, default headers on the client).
-- Response status: 200 with body, 204 no-content, 404 -> remote exception mapping.
-- One end-to-end RRPC scenario through `@Remote(rrpc=true)` to confirm bidirectional bean marshalling.
-
-5 transports x ~15 scenarios = ~75 executions. This is the primary lift for transport-module coverage.
-
-### B. `org.apache.juneau.ng.http` coverage closeout  (independent — can land before TODO-31)
-
-The bulk of the uncovered surface is in named response classes (`Ok`, `Created`, `NotFound`, ...) and RFC-named header classes (`Accept`, `ContentType`, ...). Cover with two parametric tests:
-
-- `juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java` — reflectively enumerate every public subclass of the response base in `org.apache.juneau.ng.http.response`; for each, invoke every public constructor / factory, call all public getters, run `writeTo(OutputStream)`, verify status code matches the RFC code.
-- `juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java` — same parametric pattern over `org.apache.juneau.ng.http.header.*`. Walks `of(...)` factories, value accessors, and `writeTo` / wire-format paths.
-- Fill targeted gaps in `HttpBody`, `HttpHeaders`, `HttpResource` discovered by `./scripts/coverage.py --branches` (small one-shot tests).
-
-### C. Transport-module residual gaps  (after A)
-
-After the parameterized suite from A has run, fill what remains via `./scripts/coverage.py --branches`:
-
-- `close()` / connection-release hooks on each transport's response type.
-- Header carry-through for multi-value and quoted-value cases.
-- Streaming-body request paths (each transport's `InputStream` consumption code).
-
-For hostile-server edge cases (truncated body, malformed status line), use `com.sun.net.httpserver.HttpServer` following the existing pattern in `ApacheHc45Transport_Test`.
-
-### D. Verify + archive
-
-- `./scripts/coverage.py --run` against `juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/`; confirm >=95% instruction coverage.
-- `./scripts/coverage.py --branches` for every NG package; address remaining gaps.
-- `./scripts/test.py` full suite; confirm no regressions and runtime stays in the ~33s range.
-- Archive this file as `todo/FINISHED-11b-restclient-ng-coverage-closeout.md` and remove `[TODO-11]` from `todo/TODO.md`.
-
-## Latest coverage measurement (May 2026)
-
-Measured via `./scripts/coverage.py` against `juneau-utest/target/jacoco.exec`.
-
-| Package / Module | Branches | Instructions |
-|---|---|---|
-| `org.apache.juneau.ng.http` (in `juneau-rest-common`) | 20% | **38%** |
-| `org.apache.juneau.ng.rest.client` (in `juneau-rest-client`) | 89% | 94% |
-| `org.apache.juneau.ng.rest.mock` (in `juneau-rest-mock`) | 98% | 96% |
-| Apache HC 4.5 transport (`juneau-ng-rest-client-apache-httpclient-45`) | 71% | 81% |
-| Apache HC 5 transport (`juneau-ng-rest-client-apache-httpclient-50`) | 79% | 85% |
-| JDK `HttpClient` transport (`juneau-ng-rest-client-java-httpclient`) | 100% | 78% |
-| OkHttp transport (`juneau-ng-rest-client-okhttp`) | 72% | 88% |
-| Jetty transport (`juneau-ng-rest-client-jetty`) | 75% | 77% |
-
-## Out of scope
-
-- Promoting the NG stack from beta to stable.
-- Deprecating or removing the classic `RestClient` / `juneau-rest-common`.
diff --git a/todo/TODO.md b/todo/TODO.md
index bab1163909..5fb2d9a748 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -17,8 +17,6 @@
 
 - [TODO-10] Move `org.apache.juneau.http.annotation` from `juneau-marshall` into `juneau-rest-common` (already done for the annotation classes — plan tracks remaining follow-on cleanup). See `todo/TODO-10-move-http-annotation-to-rest-common.md`.
 
-- [TODO-11] RestClient NG closeout: cross-transport remote-interface test suite (blocked on [TODO-31]) plus NG http package coverage (independent). Implementation has shipped (see `todo/FINISHED-11a-restclient-ng-design-plan.md`). Active checklist in `todo/TODO-11-restclient-ng-coverage-closeout.md`.
-
 - [TODO-12] Schema validation mode for parsers and serializers: wire `@Schema` validation into the bean property get/set lifecycle gated by a new `validateSchema` flag on `MarshallingContext`. See `todo/TODO-12-schema-validation.md`.
 
 - [TODO-17] Audit 9.2.x changes (juneau-docs release notes 9.2.0 / 9.5.0 + git history since 9.1.0) for breaking changes and populate the v9.5 Migration Guide at juneau-docs/pages/topics/23.01.V9.5-migration-guide.md with Old→New rows for each. Focus on removed APIs, renamed annotations/classes/methods, changed default behaviors, and any annotation-attribute semantics changes.