refactor(core): unify HTTP protocol package boundaries

This commit is contained in:
Zakaria El Orche
2026-08-13 16:24:23 +00:00
parent d882ea255c
commit 885c450f6b
107 changed files with 122 additions and 484 deletions
@@ -127,7 +127,6 @@ class ChunkedInputStreamTest {
assertEquals("hi", new String(in.readAllBytes(), StandardCharsets.UTF_8));
}
// --- EX-10: no per-byte syscalls against the underlying stream ------------
/** Counts every {@code read} call that reaches the wrapped stream — i.e. every syscall. */
private static final class CountingInputStream extends ByteArrayInputStream {
@@ -159,7 +158,6 @@ class ChunkedInputStreamTest {
assertEquals(1, counting.reads);
}
// --- EX-02/09 chunk safety limits ------------------------------------------
@Test
void chunkSizeAboveLimit_rejected() {
@@ -17,7 +17,6 @@ import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-07}: a socket-level {@code SO_TIMEOUT} alone never trips against a peer that keeps
* trickling bytes slower than the timeout window — each individual read still succeeds. These
* tests prove the absolute deadline in {@code dev.relism.flash.transport.BufferedByteSource}
* actually bounds the total time, not just each read.
@@ -142,7 +141,6 @@ class HttpServerTimeoutTest {
long start = System.nanoTime();
// A plain socket that never speaks TLS at all — the server's explicit
// startHandshake() (EX-30) blocks waiting for a ClientHello that is never coming,
// and must be bounded by headerReadTimeoutMs rather than hanging forever. Whether the
// JSSE implementation sends a TLS alert record before closing or just closes outright
// is a JSSE implementation detail, not something this test should pin down — the
@@ -13,8 +13,6 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* One test per rejection rule added in HTTP/2 plan Phase 1 (EX-02, EX-03, EX-08, EX-18, EX-35,
* EX-36), each asserting the specific status code {@link MalformedRequestException} carries —
* not merely that some exception was thrown. {@code Http1Connection}/{@code ConnectionRunner} always closes the connection
* after any of these (never keep-alive); that behaviour is exercised at the integration level
* by {@code HttpServerTest}.
@@ -34,7 +32,6 @@ class RequestParserSecurityTest {
return assertThrows(MalformedRequestException.class, () -> parse(raw));
}
// --- EX-02: Content-Length + Transfer-Encoding smuggling -------------------
@Test
void contentLengthAndTransferEncodingBothPresent_rejected400() {
@@ -79,7 +76,6 @@ class RequestParserSecurityTest {
assertEquals(501, e.status());
}
// --- EX-03: strict Content-Length parsing -----------------------------------
@Test
void contentLength_nonDigitSuffix_rejected400() {
@@ -112,7 +108,6 @@ class RequestParserSecurityTest {
assertEquals(413, expect("POST / HTTP/1.1\nHost: h\nContent-Length: " + tooLarge + "\n\n").status());
}
// --- EX-08: header/request-line limits --------------------------------------
@Test
void tooManyHeaders_rejected431() {
@@ -140,7 +135,6 @@ class RequestParserSecurityTest {
assertEquals(431, expect("GET " + path + " HTTP/1.1\nHost: h\n\n").status());
}
// --- EX-18: bare CR / obs-fold -----------------------------------------------
@Test
void bareLfInsteadOfCrlf_headerLine_rejected() {
@@ -178,7 +172,6 @@ class RequestParserSecurityTest {
assertThrows(MalformedRequestException.class, () -> new RequestParser().parse(source(raw)));
}
// --- EX-36: header line missing ':' -------------------------------------------
@Test
void headerLineMissingColon_rejected400() {
@@ -56,7 +56,6 @@ class RequestParserTest {
assertEquals("2", r.query("page"));
}
// --- EX-42: pooled RequestByteViews (path/query/protocol) don't leak across requests ---
@Test
void samePooledParser_secondRequestWithoutQuery_doesNotLeakFirstRequestsQuery() throws IOException {
@@ -135,8 +134,6 @@ class RequestParserTest {
@Test
void missingHeaderTerminator_throwsMalformedRequestException() {
// Valid request line but stream ends before \r\n\r\n. Previously a generic IOException;
// now the same typed rejection EX-08's over-limit case uses, since both mean "the
// header block could never be completed within the allowed buffer" (EX-08).
byte[] raw = "GET / HTTP/1.1\r\nHost: localhost\r\n".getBytes(StandardCharsets.UTF_8);
assertThrows(MalformedRequestException.class, () -> new RequestParser().parse(source(raw)));
}
@@ -159,7 +156,6 @@ class RequestParserTest {
@Test
void headers_exceedingMaxBufferSize_throwsMalformedRequestException() {
// Feed more bytes than the configured cap with no \r\n\r\n : must throw 431 (EX-08).
int cap = 16 * 1024;
byte[] giant = new byte[cap + 1];
Arrays.fill(giant, (byte) 'A');
@@ -183,7 +179,6 @@ class RequestParserTest {
@Test
void transferEncoding_multiValueEndingInChunked_recognised() throws IOException {
// EX-35: "gzip, chunked" — chunked need only be the FINAL coding (RFC 9112 §6.1). The
// old whole-value comparison misclassified this as not chunked at all.
String raw = "POST / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
@@ -245,7 +245,6 @@ class MultipartTest {
}
// -------------------------------------------------------------------------
// EX-29: resource-exhaustion bounds
// -------------------------------------------------------------------------
@Test
@@ -10,31 +10,21 @@ import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.fail;
/**
* {@code R1}/{@code DEC-02}: HTTP/1.1 and HTTP/2 are peers behind the {@code ConnectionProtocol}
* seam, never coupled to each other directly. A lightweight source-scan rather than ArchUnit —
* this project has no bytecode-analysis test dependency yet, and one import-statement check per
* package pair does not need one; record the choice here rather than in {@code DECISIONS.md}
* since it is this test's own implementation detail, not a design decision affecting shipped
* code.
*/
/** Ensures that the HTTP/1.1 and HTTP/2 implementations remain independent peers. */
class PackageBoundaryTest {
@Test
void http1DoesNotImportH2() throws IOException {
assertNoImportOfPackage("dev/relism/flash/http1", "dev.relism.flash.h2");
void http1DoesNotImportHttp2() throws IOException {
assertNoImportOfPackage("dev/relism/flash/http1", "dev.relism.flash.http2");
}
@Test
void h2DoesNotImportHttp1() throws IOException {
assertNoImportOfPackage("dev/relism/flash/h2", "dev.relism.flash.http1");
void http2DoesNotImportHttp1() throws IOException {
assertNoImportOfPackage("dev/relism/flash/http2", "dev.relism.flash.http1");
}
private static void assertNoImportOfPackage(String sourceDirRelative, String forbiddenImportPrefix) throws IOException {
Path root = findSourceRoot(sourceDirRelative);
// Neither package boundary can be meaningfully checked before both packages exist; once
// dev.relism.flash.h2 gains real classes (Phase 3+) this stops being a no-op for the
// h2-side test.
if (root == null) return;
try (Stream<Path> files = Files.walk(root)) {
@@ -45,7 +35,7 @@ class PackageBoundaryTest {
if (trimmed.startsWith("import " + forbiddenImportPrefix + ".")
|| trimmed.startsWith("import " + forbiddenImportPrefix + ";")) {
fail(file + " imports " + forbiddenImportPrefix
+ " violates the h1/h2 package boundary (R1/DEC-02): " + trimmed);
+ " and violates the HTTP protocol package boundary: " + trimmed);
}
}
}
@@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* Randomized agreement testing for {@link ByteScan}'s SWAR methods against their scalar
* counterparts, per Phase 4's task 1 ("property-test SWAR against scalar on random inputs of
* every length 0..256 ... including unaligned starts"). {@link ByteScanTest} already covers
* every exact boundary deterministically; this class instead throws a large volume of fully
* random bytes and random sub-ranges at both implementations, on a fixed seed for reproducible
@@ -41,7 +41,6 @@ class HttpStatusTest {
assertNull(HttpStatus.reasonForCode(0));
}
// --- EX-17: bound computed from values(), not a hand-maintained constant -----
@Test
void statusesAboveThePreviousHandMaintainedBound_workCorrectly() {
@@ -26,7 +26,6 @@ class Http1ResponseWriterTest {
return out.toString(StandardCharsets.UTF_8);
}
// --- EX-14: HEAD ------------------------------------------------------------
@Test
void head_reportsContentLengthButWritesNoBody() throws IOException {
@@ -46,7 +45,6 @@ class Http1ResponseWriterTest {
assertTrue(raw.endsWith("hello world"), raw);
}
// --- EX-15: 204 / 304 / 1xx never carry Content-Length or a body ------------
@Test
void status204_omitsContentLengthAndBody() throws IOException {
@@ -85,7 +83,6 @@ class Http1ResponseWriterTest {
assertTrue(raw.contains("Content-Length: 1\r\n"), raw);
}
// --- EX-15: ContentType.NONE omits the Content-Type line entirely -----------
@Test
void contentTypeNone_omitsContentTypeLine() throws IOException {
@@ -101,7 +98,6 @@ class Http1ResponseWriterTest {
assertTrue(raw.contains("Content-Type: text/plain\r\n"), raw);
}
// --- EX-16: Date header -------------------------------------------------------
@Test
void sendDateTrue_includesDateHeader() throws IOException {
@@ -135,7 +131,6 @@ class Http1ResponseWriterTest {
assertTrue(raw.contains("Connection: close\r\n"), raw);
}
// --- EX-27: one bulk write for a small fixed body -----------------------------
/** Counts calls to {@code write(byte[], int, int)} — the only overload {@link Http1ResponseWriter} uses. */
private static final class CountingOutputStream extends java.io.OutputStream {
@@ -1,4 +1,4 @@
package dev.relism.flash.h2;
package dev.relism.flash.http2;
import org.junit.jupiter.api.Test;
@@ -53,7 +53,6 @@ class Http2ErrorCodeTest {
@Test
void bytesInstanceIsStablePerConstant() {
// Precomputed at class init (R4) must not be rebuilt per call.
assertSame(Http2ErrorCode.PROTOCOL_ERROR.bytes(), Http2ErrorCode.PROTOCOL_ERROR.bytes());
}
}
@@ -1,4 +1,4 @@
package dev.relism.flash.h2;
package dev.relism.flash.http2;
import org.junit.jupiter.api.Test;
@@ -1,4 +1,4 @@
package dev.relism.flash.h2;
package dev.relism.flash.http2;
import org.junit.jupiter.api.Test;
@@ -1,4 +1,4 @@
package dev.relism.flash.h2;
package dev.relism.flash.http2;
import org.junit.jupiter.api.Test;
@@ -1,7 +1,7 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import dev.relism.flash.h2.Http2ErrorCode;
import dev.relism.flash.h2.Http2Exception;
import dev.relism.flash.http2.Http2ErrorCode;
import dev.relism.flash.http2.Http2Exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@@ -175,7 +175,7 @@ class FrameValidatorTest {
@Test
void declaredLengthAboveMaxFrameSize_isFrameSizeError() {
FrameHeader h = headerOf(dev.relism.flash.h2.Http2Limits.MAX_FRAME_SIZE_LOCAL + 1, FrameType.DATA, 0, 1);
FrameHeader h = headerOf(dev.relism.flash.http2.Http2Limits.MAX_FRAME_SIZE_LOCAL + 1, FrameType.DATA, 0, 1);
assertEquals(Http2ErrorCode.FRAME_SIZE_ERROR, codeOf(h, false));
}
}
@@ -1,6 +1,6 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import dev.relism.flash.h2.Http2Exception;
import dev.relism.flash.http2.Http2Exception;
import dev.relism.flash.transport.BufferedByteSource;
import org.junit.jupiter.api.Test;
@@ -13,7 +13,6 @@ import java.util.Random;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Phase 5's DoD: "Fuzz test green for 10 million random inputs." Throws fully random bytes at
* {@link Http2FrameReader} and asserts that only a typed, expected outcome ever results: a
* {@link Http2Exception} (a declared length exceeding {@code MAX_FRAME_SIZE_LOCAL} the
* overwhelmingly common outcome, since a random 24-bit length is astronomically likely to
@@ -1,7 +1,7 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import dev.relism.flash.bytes.ByteWriter;
import dev.relism.flash.h2.Http2Exception;
import dev.relism.flash.http2.Http2Exception;
import dev.relism.flash.transport.BufferedByteSource;
import org.junit.jupiter.api.Test;
@@ -70,9 +70,9 @@ class Http2FrameReaderTest {
byte[] payload = new byte[len];
byte[] wire = buildFrame(FrameType.DATA, 0, 1, payload);
Http2FrameReader reader = new Http2FrameReader(sourceOf(wire));
if (len > dev.relism.flash.h2.Http2Limits.MAX_FRAME_SIZE_LOCAL) {
if (len > dev.relism.flash.http2.Http2Limits.MAX_FRAME_SIZE_LOCAL) {
Http2Exception ex = assertThrows(Http2Exception.class, reader::readFrame);
assertEquals(dev.relism.flash.h2.Http2ErrorCode.FRAME_SIZE_ERROR, ex.errorCode());
assertEquals(dev.relism.flash.http2.Http2ErrorCode.FRAME_SIZE_ERROR, ex.errorCode());
} else {
FrameHeader header = reader.readFrame();
assertNotNull(header);
@@ -151,7 +151,7 @@ class Http2FrameReaderTest {
out.beginFrame(FrameType.PING, 0, 0);
out.writer().writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
out.endFrame();
out.beginFrame(FrameType.PING, dev.relism.flash.h2.frame.FrameFlags.ACK, 0);
out.beginFrame(FrameType.PING, dev.relism.flash.http2.frame.FrameFlags.ACK, 0);
out.writer().writeBytes(new byte[]{8, 7, 6, 5, 4, 3, 2, 1});
out.endFrame();
byte[] wire = new byte[w.length()];
@@ -1,4 +1,4 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import org.junit.jupiter.api.Test;
@@ -26,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.*;
* full gate verification (1000 iterations per N, plus a
* {@code -Djdk.virtualThreadScheduler.parallelism=1} run to surface pinning/lost-wakeup bugs
* that only appear at parallelism 1) was run manually and is recorded, with its numbers, in
* {@code flash/docs/http2/WRITER.md} and {@code DECISIONS.md} (`DEC-09`).
*/
class Http2FrameWriterStressTest {
@@ -1,4 +1,4 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import org.junit.jupiter.api.Test;
@@ -1,7 +1,7 @@
package dev.relism.flash.h2.frame;
package dev.relism.flash.http2.frame;
import dev.relism.flash.h2.Http2ErrorCode;
import dev.relism.flash.h2.Http2Exception;
import dev.relism.flash.http2.Http2ErrorCode;
import dev.relism.flash.http2.Http2Exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@@ -8,7 +8,6 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-09}: dedicated correctness coverage for {@link Http1HeaderMap}'s per-{@code reset()}
* index — duplicate names, case variation, zero headers, and growth past the initial index
* capacity up to {@code Http1Limits.MAX_HEADER_COUNT}. {@link HeaderMapTest} already covers the
* ordinary lookup/forEach contract; this class targets the index machinery specifically.
@@ -104,7 +103,6 @@ class Http1HeaderMapIndexTest {
@Test
void allocation_indexArraysAreNotReallocatedOnceWarm() {
// The rigorous 0 B/op verification is the Phase 17 JMH gate (-prof gc); this is a
// unit-test-level structural guarantee that repeated first()/all()/view() lookups never
// re-trigger index growth (Arrays.copyOf inside ensureIndexCapacity) after the first
// reset() has already sized the arrays for this header count — asserted by identity: the
@@ -124,7 +122,6 @@ class Http1HeaderMapIndexTest {
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
// EX-05's documented hazard, demonstrated through the actual public API: Http1HeaderMap's
// view() pool is sized 4 (VIEW_POOL_SIZE); a 5th call in the same request wraps around
// and silently repositions the object the 1st call returned.
dev.relism.fpr.core.ByteView v1 = null;
@@ -68,7 +68,6 @@ class PathParamsTest {
@Test
void view_poolWraparound_aliasesAnEarlierReturnedView() {
// EX-05's pooled path only engages when `source` is array-backed (ArrayBackedByteView) —
// unlike of()'s plain inline ByteView (which exercises the non-pooled fallback, still
// correct but not the code path this test targets), use the same view type RequestParser
// actually produces.
@@ -9,10 +9,8 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-26}: the clean-value (no {@code %}/{@code +}) fast path in {@code QueryParams.decode}
* must produce byte-for-byte identical results to the percent-decoding slow path it bypasses —
* verified here across clean values, values needing every kind of decoding, and the boundary
* between them. Also covers {@code EX-05}'s pooled {@code view()}.
*/
class QueryParamsFastPathTest {
@@ -63,7 +61,6 @@ class QueryParamsFastPathTest {
assertEquals("a b", qp.get("plussed"));
}
// ── EX-05: pooled view() ────────────────────────────────────────────────
@Test
void view_returnsRawUndecodedBytes() {
@@ -162,7 +162,6 @@ class RequestBodyTest {
assertEquals(0, socket.available());
}
// --- EX-22/EX-23: pooled instance, repositioned via reset() --------------------
@Test
void reset_repositionsSamePooledInstance_overSuccessiveRequests() throws IOException {
@@ -187,7 +186,6 @@ class RequestBodyTest {
body.reset(new ByteArrayInputStream("two".getBytes(StandardCharsets.UTF_8)), 3, new byte[0], 0, 0);
InputStream stream2 = body.stream();
assertSame(stream1, stream2, "EX-23: stream() must reposition the one pooled BoundedBufferedInputStream, not allocate a new one per request");
assertEquals("two", new String(stream2.readAllBytes(), StandardCharsets.UTF_8));
}
@@ -9,13 +9,11 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-22}: {@link Request} is pooled <em>per connection</em> (one instance owned by
* {@code RequestParser}, repositioned via {@link Request#forParsed} for every request on that
* connection) — not via a shared cross-connection pool. The plan's own safety-check wording
* ("connection A's {@code Authorization} header must never be visible on connection B") describes
* a threat model that does not structurally apply to this design: two different connections
* never share a {@code Request} instance at all (each owns its own {@code RequestParser}, hence
* its own {@code Request}) — see {@code DECISIONS.md} for the pooling-granularity decision this
* follows from. The real, applicable threat this class actually tests: request <em>N+1</em> on
* the *same* keep-alive connection must never see stale data left over from request <em>N</em>,
* since those two requests genuinely do share one {@code Request} instance.
@@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-22}'s dev-mode use-after-recycle guard. Exercises the poisoning check directly via
* {@code Request.setPoisoningEnabledForTesting} rather than the real {@code Flash.DEV} flag,
* which is a {@code static final boolean} fixed once at JVM startup and cannot be toggled by an
* individual test — see that field's own comment in {@code Request.java}.
@@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** {@code EX-21}: mirrors {@code RequestPoolingTest} for {@link Response}. */
class ResponsePoolingTest {
@Test
@@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** {@code EX-21}'s dev-mode use-after-recycle guard — mirrors {@code RequestRecycleGuardTest}. */
class ResponseRecycleGuardTest {
@AfterEach
@@ -135,7 +135,6 @@ class ResponseTest {
assertTrue(new Response(200, new byte[0], ContentType.TEXT_PLAIN).getHeaders().isEmpty());
}
// --- EX-43: response header budget (Phase 6 zero-alloc DoD) ---
@Test
void header_exceedingMaxCount_throws() {
@@ -72,7 +72,6 @@ class FastPathRouterImplTest {
@Test
void route_reusesScratchAcrossManyRequests_includingGrowingParamCapacity() throws Exception {
// EX-19: the same scratch, reused across a mix of param counts, must keep matching
// correctly as its arrays grow past their initial size (8) and get reused afterward.
FastPathRouterImpl router = new FastPathRouterImpl();
router.doRegister(HttpMethod.GET, "/a/{p1}/{p2}/{p3}/{p4}/{p5}/{p6}/{p7}/{p8}/{p9}/{p10}",
@@ -12,12 +12,10 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-04}: verifies the {@code longAt()}/{@code supportsLong()} contract against
* {@code fpr-core}'s own word-at-a-time comparison code — not merely against a hand-derived
* expectation, per the plan's explicit instruction to verify by testing against {@code fpr-core}
* directly rather than by reading its bytecode (bytecode-reading only informed which byte order
* to use; this test is the actual verification). A wrong endianness or a wrong bounds assumption
* here produces silently mis-routed requests, the worst possible failure mode ({@code EX-04}'s
* own registry entry) — so this covers both the raw word-read contract and an end-to-end router
* match with the long path actually engaged.
*/
@@ -28,7 +28,6 @@ class FastPathViewsTest {
assertThrows(IndexOutOfBoundsException.class, () -> view.byteAt(10));
}
// --- EX-42: reset() repositions the same instance, zero allocation ---------
@Test
void requestByteView_reset_repositionsSameInstance() {
@@ -56,7 +56,6 @@ class ByteTemplateTest {
assertEquals("A12B", new String(result, StandardCharsets.UTF_8));
}
// --- EX-28: renderInto(buffer, offset, ...) ------------------------------------
@Test
void renderInto_writesAtOffset_andReturnsLength() {
@@ -121,7 +121,6 @@ class TlsConfigTest {
}
}
// --- EX-31: cipher suite filtering when h2 is offered -----------------------
@Test
void negotiatesH2_trueOnlyWhenH2IsInTheOfferedList() throws Exception {
@@ -10,11 +10,8 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-37}: this class previously had zero dedicated tests — its deadline mechanism (the
* actual {@code EX-07} slowloris fix) was exercised only indirectly through real-socket,
* end-to-end tests, which never hit the {@code null}-socket path every isolated unit test in
* this codebase actually uses. Found and fixed while building {@code Http2FrameReaderTest}
* (Phase 5); this class closes the gap.
*/
class BufferedByteSourceTest {
@@ -100,7 +97,6 @@ class BufferedByteSourceTest {
assertThrows(IllegalStateException.class, () -> src.prependOnce(a, 0, 1));
}
// ── Deadline mechanism, EX-37's actual regression coverage ──────────────
@Test
void clearDeadline_withNullSocket_doesNotThrow() throws IOException {
@@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* A scratch is always released — including on an exception path — and a socket is always
* removed from {@code activeSockets}, regardless of how the dispatched
* {@link ConnectionProtocol} exits. This is a resource-leak safety property (Phase 2's Safety
* checks list), verified here with a protocol implementation that deliberately throws.
*/
class ConnectionRunnerTest {
@@ -36,13 +36,13 @@ class ProtocolNegotiatorTest {
@Test
void h2cPrefaceExact_negotiatesH2() throws IOException {
BufferedByteSource src = plaintextSource("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
assertEquals(NegotiatedProtocol.H2, ProtocolNegotiator.negotiate(new Socket(), src));
assertEquals(NegotiatedProtocol.HTTP_2, ProtocolNegotiator.negotiate(new Socket(), src));
}
@Test
void h2cPrefaceFollowedByMoreData_stillNegotiatesH2_andDoesNotConsume() throws IOException {
BufferedByteSource src = plaintextSource("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\nEXTRA");
assertEquals(NegotiatedProtocol.H2, ProtocolNegotiator.negotiate(new Socket(), src));
assertEquals(NegotiatedProtocol.HTTP_2, ProtocolNegotiator.negotiate(new Socket(), src));
// peek() must not have consumed anything — the full 24-byte preface is still there for
// whatever reads next (Http2Connection, once it exists).
byte[] readBack = new byte[24];
@@ -85,7 +85,6 @@ class ProtocolNegotiatorTest {
/**
* Binds a real TLS listener offering {@code serverAlpn}, connects a client offering
* {@code clientAlpn}, forces the handshake on both sides (mirroring {@code Http1Connection}/{@code ConnectionRunner}'s
* EX-30 fix), and hands the accepted server-side socket to {@code assertion}.
*/
private static void withNegotiatedAlpn(Path dir, String[] serverAlpn, String[] clientAlpn,
ThrowingConsumer<SSLSocket> assertion) throws Exception {
@@ -129,7 +128,7 @@ class ProtocolNegotiatorTest {
void alpnH2_negotiatesH2(@TempDir Path dir) throws Exception {
withNegotiatedAlpn(dir, new String[]{"h2", "http/1.1"}, new String[]{"h2", "http/1.1"}, server -> {
assertEquals("h2", server.getApplicationProtocol());
assertEquals(NegotiatedProtocol.H2,
assertEquals(NegotiatedProtocol.HTTP_2,
ProtocolNegotiator.negotiate(server, new BufferedByteSource(server.getInputStream(), server)));
});
}
@@ -16,7 +16,6 @@ import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-32}: the two-stage graceful shutdown — stop accepting, let an in-flight request
* finish (forced to {@code Connection: close}), then force-close whatever remains after
* {@code shutdownDrainTimeoutMs}.
*/
@@ -72,7 +71,6 @@ class ServerLifecycleGracefulShutdownTest {
assertTrue(response.startsWith("HTTP/1.1 200 OK"), response);
assertTrue(response.contains("done"), response);
// EX-32: the in-flight request is forced to close rather than keep-alive, even
// though the client asked for HTTP/1.1's default keep-alive.
assertTrue(response.contains("Connection: close"), response);
@@ -11,7 +11,6 @@ import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@code EX-11} (bulk header read) and {@code EX-12} (continuation reassembly, mandatory
* masking, opcode validation, control-frame constraints, correct close codes) coverage for
* {@link WebSocketSession#readFrame}.
*/
@@ -52,7 +51,6 @@ class WebSocketFragmentationAndValidationTest {
return new WebSocketSession(new ByteArrayInputStream(raw), new ByteArrayOutputStream(), bufferSize);
}
// --- EX-12: continuation reassembly ------------------------------------------
@Test
void continuationFrames_reassembleIntoOneMessage() throws IOException {
@@ -118,7 +116,6 @@ class WebSocketFragmentationAndValidationTest {
assertEquals(1009, e.closeCode());
}
// --- EX-12: mandatory masking direction ---------------------------------------
@Test
void serverSession_unmaskedIncomingFrame_rejected1002() throws IOException {
@@ -149,7 +146,6 @@ class WebSocketFragmentationAndValidationTest {
assertEquals("hi", new String(frame.copyPayload(), StandardCharsets.UTF_8));
}
// --- EX-12: opcode validation --------------------------------------------------
@Test
void reservedOpcode_rejected1002() throws IOException {
@@ -160,7 +156,6 @@ class WebSocketFragmentationAndValidationTest {
assertEquals(1002, e.closeCode());
}
// --- EX-12: control-frame constraints ------------------------------------------
@Test
void fragmentedControlFrame_rejected1002() throws IOException {
@@ -189,7 +184,6 @@ class WebSocketFragmentationAndValidationTest {
assertEquals(125, frame.payloadLength());
}
// --- EX-11: bulk header read, not one syscall per byte -------------------------
private static final class CountingInputStream extends InputStream {
private final InputStream delegate;