refactor(core): unify HTTP protocol package boundaries
This commit is contained in:
@@ -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
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package dev.relism.flash.h2;
|
||||
package dev.relism.flash.http2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.relism.flash.h2;
|
||||
package dev.relism.flash.http2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.relism.flash.h2;
|
||||
package dev.relism.flash.http2;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+4
-4
@@ -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));
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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
|
||||
+5
-5
@@ -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
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package dev.relism.flash.h2.frame;
|
||||
package dev.relism.flash.http2.frame;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+3
-3
@@ -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() {
|
||||
|
||||
-1
@@ -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}",
|
||||
|
||||
-2
@@ -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.
|
||||
*/
|
||||
|
||||
-1
@@ -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)));
|
||||
});
|
||||
}
|
||||
|
||||
-2
@@ -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);
|
||||
|
||||
|
||||
-6
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user