enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles
This commit is contained in:
@@ -40,7 +40,7 @@ class HttpServerConcurrencyTest {
|
||||
server = new HttpServer(config);
|
||||
server.get("/ping", (req, res) -> "pong");
|
||||
server.post("/echo", (req, res) -> {
|
||||
byte[] body = req.getBody();
|
||||
byte[] body = req.body().bytes();
|
||||
return new Response(200, body, ContentType.TEXT_PLAIN);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -39,14 +40,29 @@ class HttpServerTest {
|
||||
server.get("/api/ping", (req, res) -> "pong");
|
||||
|
||||
server.post("/api/echo", (req, res) -> {
|
||||
byte[] body = req.getBody();
|
||||
return new Response(201, body, ContentType.TEXT_PLAIN); // echo body & change status
|
||||
byte[] body = req.body().bytes();
|
||||
return res.status(201).body(body); // echo body & change status
|
||||
});
|
||||
|
||||
|
||||
server.get("/api/crash", (req, res) -> {
|
||||
throw new RuntimeException("Simulated Crash");
|
||||
});
|
||||
|
||||
byte[] streamData = "streaming response body".getBytes(StandardCharsets.UTF_8);
|
||||
server.get("/api/stream", (req, res) ->
|
||||
res.stream(new ByteArrayInputStream(streamData), streamData.length));
|
||||
|
||||
server.get("/api/chunked-out", (req, res) ->
|
||||
res.chunked(new ByteArrayInputStream(streamData)));
|
||||
|
||||
server.get("/api/custom-header", (req, res) ->
|
||||
res.body("ok").header("X-Flash", "works"));
|
||||
|
||||
server.post("/api/chunked-in", (req, res) -> {
|
||||
byte[] body = req.body().bytes();
|
||||
return res.body(body);
|
||||
});
|
||||
|
||||
server.start().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@@ -57,27 +73,35 @@ class HttpServerTest {
|
||||
}
|
||||
}
|
||||
|
||||
// --- raw socket helper ---
|
||||
// --- helpers ---
|
||||
|
||||
private static final int SOCKET_TIMEOUT_MS = 5000;
|
||||
|
||||
private String sendRawRequest(String rawHttp) throws Exception {
|
||||
try (Socket socket = new Socket("127.0.0.1", port);
|
||||
OutputStream out = socket.getOutputStream();
|
||||
InputStream in = socket.getInputStream()) {
|
||||
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
out.write(rawHttp.getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
return readOneResponse(in);
|
||||
}
|
||||
}
|
||||
|
||||
// Read until \r\n\r\n to get the full header block
|
||||
ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
|
||||
int b, prev3 = -1, prev2 = -1, prev1 = -1;
|
||||
while ((b = in.read()) != -1) {
|
||||
headerBuf.write(b);
|
||||
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
|
||||
prev3 = prev2; prev2 = prev1; prev1 = b;
|
||||
}
|
||||
String headers = headerBuf.toString(StandardCharsets.UTF_8);
|
||||
private String readOneResponse(InputStream in) throws Exception {
|
||||
ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
|
||||
int b, prev3 = -1, prev2 = -1, prev1 = -1;
|
||||
while ((b = in.read()) != -1) {
|
||||
headerBuf.write(b);
|
||||
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
|
||||
prev3 = prev2; prev2 = prev1; prev1 = b;
|
||||
}
|
||||
String headers = headerBuf.toString(StandardCharsets.UTF_8);
|
||||
|
||||
// Parse Content-Length
|
||||
String body;
|
||||
if (headers.toLowerCase().contains("transfer-encoding: chunked")) {
|
||||
body = readChunkedBody(in);
|
||||
} else {
|
||||
int contentLength = 0;
|
||||
for (String line : headers.split("\r\n")) {
|
||||
if (line.toLowerCase().startsWith("content-length:")) {
|
||||
@@ -85,11 +109,25 @@ class HttpServerTest {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read exactly Content-Length bytes for the body
|
||||
byte[] body = in.readNBytes(contentLength);
|
||||
return headers + new String(body, StandardCharsets.UTF_8);
|
||||
body = new String(in.readNBytes(contentLength), StandardCharsets.UTF_8);
|
||||
}
|
||||
return headers + body;
|
||||
}
|
||||
|
||||
private String readChunkedBody(InputStream in) throws Exception {
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
while (true) {
|
||||
StringBuilder sizeLine = new StringBuilder();
|
||||
int b;
|
||||
while ((b = in.read()) != -1 && b != '\n') {
|
||||
if (b != '\r') sizeLine.append((char) b);
|
||||
}
|
||||
int size = Integer.parseInt(sizeLine.toString().trim(), 16);
|
||||
if (size == 0) break;
|
||||
result.write(in.readNBytes(size));
|
||||
in.read(); in.read(); // \r\n after chunk data
|
||||
}
|
||||
return result.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// --- E2E Tests ---
|
||||
@@ -154,10 +192,84 @@ class HttpServerTest {
|
||||
String req = "GET / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"\r\n";
|
||||
|
||||
|
||||
String res = sendRawRequest(req);
|
||||
|
||||
|
||||
assertTrue(res.startsWith("HTTP/1.1 404 Not Found"));
|
||||
assertTrue(res.contains("No route matched this request."));
|
||||
}
|
||||
|
||||
// --- streaming response ---
|
||||
|
||||
@Test
|
||||
void testStreamingResponse_writesContentLengthAndBody() throws Exception {
|
||||
String req = "GET /api/stream HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
String res = sendRawRequest(req);
|
||||
|
||||
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(res.contains("Content-Length: 23")); // "streaming response body"
|
||||
assertTrue(res.endsWith("streaming response body"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChunkedResponse_writesTransferEncodingChunked() throws Exception {
|
||||
String req = "GET /api/chunked-out HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
String res = sendRawRequest(req);
|
||||
|
||||
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(res.contains("Transfer-Encoding: chunked"));
|
||||
assertTrue(res.endsWith("streaming response body"));
|
||||
}
|
||||
|
||||
// --- custom response headers ---
|
||||
|
||||
@Test
|
||||
void testCustomHeader_appearsInResponse() throws Exception {
|
||||
String req = "GET /api/custom-header HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
String res = sendRawRequest(req);
|
||||
|
||||
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(res.contains("X-Flash: works\r\n"));
|
||||
}
|
||||
|
||||
// --- chunked request body ---
|
||||
|
||||
@Test
|
||||
void testChunkedRequestBody_decodedAndEchoed() throws Exception {
|
||||
String req = "POST /api/chunked-in HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Transfer-Encoding: chunked\r\n" +
|
||||
"\r\n" +
|
||||
"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
|
||||
|
||||
String res = sendRawRequest(req);
|
||||
|
||||
assertTrue(res.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(res.endsWith("hello world"));
|
||||
}
|
||||
|
||||
// --- keep-alive ---
|
||||
|
||||
@Test
|
||||
void testKeepAlive_twoRequestsOnSameConnection() throws Exception {
|
||||
String req = "GET /api/ping HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
||||
|
||||
try (Socket socket = new Socket("127.0.0.1", port);
|
||||
OutputStream out = socket.getOutputStream();
|
||||
InputStream in = socket.getInputStream()) {
|
||||
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
out.write(req.getBytes(StandardCharsets.UTF_8));
|
||||
out.write(req.getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
|
||||
String first = readOneResponse(in);
|
||||
String second = readOneResponse(in);
|
||||
|
||||
assertTrue(first.contains("pong"));
|
||||
assertTrue(second.contains("pong"));
|
||||
assertTrue(first.contains("Connection: keep-alive"));
|
||||
assertTrue(second.contains("Connection: keep-alive"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ class RequestParserTest {
|
||||
@Test
|
||||
void queryParam_resolvedFromPath() throws IOException {
|
||||
Request r = parse(req("GET /search?q=flash&page=2 HTTP/1.1", "Host: localhost"));
|
||||
assertEquals("flash", r.getQueryParam("q"));
|
||||
assertEquals("2", r.getQueryParam("page"));
|
||||
assertEquals("flash", r.query("q"));
|
||||
assertEquals("2", r.query("page"));
|
||||
}
|
||||
|
||||
// --- headers ---
|
||||
@@ -53,15 +53,15 @@ class RequestParserTest {
|
||||
@Test
|
||||
void headers_parsed() throws IOException {
|
||||
Request r = parse(req("GET / HTTP/1.1", "Host: example.com", "Accept: application/json"));
|
||||
assertEquals("example.com", r.getHeader("Host"));
|
||||
assertEquals("application/json", r.getHeader("Accept"));
|
||||
assertEquals("example.com", r.header("Host"));
|
||||
assertEquals("application/json", r.header("Accept"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void headers_caseInsensitive() throws IOException {
|
||||
Request r = parse(req("GET / HTTP/1.1", "Content-Type: text/plain"));
|
||||
assertEquals("text/plain", r.getHeader("content-type"));
|
||||
assertEquals("text/plain", r.getHeader("CONTENT-TYPE"));
|
||||
assertEquals("text/plain", r.header("content-type"));
|
||||
assertEquals("text/plain", r.header("CONTENT-TYPE"));
|
||||
}
|
||||
|
||||
// --- body ---
|
||||
@@ -72,13 +72,13 @@ class RequestParserTest {
|
||||
String raw = "POST / HTTP/1.1\r\nContent-Length: " + body.length() + "\r\n\r\n" + body;
|
||||
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
|
||||
assertNotNull(r);
|
||||
assertEquals(body, new String(r.getBody(), StandardCharsets.UTF_8));
|
||||
assertEquals(body, new String(r.body().bytes(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void body_emptyWhenNoContentLength() throws IOException {
|
||||
Request r = parse(req("GET / HTTP/1.1", "Host: localhost"));
|
||||
assertEquals(0, r.getBody().length);
|
||||
assertTrue(r.body().isEmpty());
|
||||
}
|
||||
|
||||
// --- edge cases / robustness ---
|
||||
@@ -102,19 +102,45 @@ class RequestParserTest {
|
||||
|
||||
@Test
|
||||
void requestLine_noProtocol_throwsIOException() {
|
||||
// No space after path — parser cannot find protocol boundary
|
||||
// No space after path, parser cannot find protocol boundary
|
||||
assertThrows(IOException.class, () -> parse(req("GET /noproto")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void headers_exceedingMaxBufferSize_throwsIOException() {
|
||||
// Feed more bytes than the configured cap with no \r\n\r\n — must throw
|
||||
// Feed more bytes than the configured cap with no \r\n\r\n : must throw
|
||||
int cap = 16 * 1024;
|
||||
byte[] giant = new byte[cap + 1];
|
||||
Arrays.fill(giant, (byte) 'A');
|
||||
assertThrows(IOException.class, () -> new RequestParser(cap).parse(new ByteArrayInputStream(giant)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chunkedTransferEncoding_bodyReadable() throws IOException {
|
||||
String raw = "POST / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Transfer-Encoding: chunked\r\n" +
|
||||
"\r\n" +
|
||||
"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
|
||||
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
|
||||
assertNotNull(r);
|
||||
assertEquals(-1L, r.body().contentLength()); // -1 = chunked
|
||||
assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8), r.body().bytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentLength_parsedAsLong() throws IOException {
|
||||
// 5 GB — too large to materialize, but contentLength must be a long
|
||||
String raw = "POST / HTTP/1.1\r\n" +
|
||||
"Host: localhost\r\n" +
|
||||
"Content-Length: 5000000000\r\n" +
|
||||
"\r\n";
|
||||
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
|
||||
assertNotNull(r);
|
||||
assertEquals(5_000_000_000L, r.body().contentLength());
|
||||
assertThrows(IllegalStateException.class, r.body()::bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentLength_largerThanBody_readsPartial() throws IOException {
|
||||
// Content-Length claims 50 but stream ends after 5 bytes
|
||||
@@ -122,7 +148,7 @@ class RequestParserTest {
|
||||
String raw = "POST / HTTP/1.1\r\nContent-Length: 50\r\n\r\n" + body;
|
||||
Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
|
||||
assertNotNull(r);
|
||||
assertEquals(50, r.getBody().length);
|
||||
assertEquals(body, new String(r.getBody(), 0, body.length(), StandardCharsets.UTF_8));
|
||||
assertEquals(50, r.body().bytes().length);
|
||||
assertEquals(body, new String(r.body().bytes(), 0, body.length(), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,73 +12,63 @@ class HeaderMapTest {
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private static HeaderMap parse(String raw, String... headers) {
|
||||
private static HeaderMap parse(String... headers) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String h : headers) sb.append(h).append("\r\n");
|
||||
byte[] buffer = sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
HeaderMap map = new HeaderMap(buffer);
|
||||
|
||||
int current = 0;
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
int colon = sb.indexOf(":", current);
|
||||
int lineEnd = sb.indexOf("\r\n", current);
|
||||
int valueStart = colon + 1;
|
||||
while (valueStart < lineEnd && buffer[valueStart] == ' ') valueStart++;
|
||||
|
||||
map.add(current, colon - current, valueStart, lineEnd - valueStart);
|
||||
current = lineEnd + 2;
|
||||
}
|
||||
HeaderMap map = new HeaderMap();
|
||||
map.reset(buffer, 0, buffer.length);
|
||||
return map;
|
||||
}
|
||||
|
||||
// --- getFirst ---
|
||||
// --- first ---
|
||||
|
||||
@Test
|
||||
void getFirst_existingHeader() {
|
||||
HeaderMap map = parse("", "Host: localhost", "Accept: text/plain");
|
||||
assertEquals("localhost", map.getFirst("Host"));
|
||||
assertEquals("text/plain", map.getFirst("Accept"));
|
||||
void first_existingHeader() {
|
||||
HeaderMap map = parse("Host: localhost", "Accept: text/plain");
|
||||
assertEquals("localhost", map.first("Host"));
|
||||
assertEquals("text/plain", map.first("Accept"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirst_caseInsensitive() {
|
||||
HeaderMap map = parse("", "ConteNT-tYPe: application/json");
|
||||
assertEquals("application/json", map.getFirst("content-type"));
|
||||
assertEquals("application/json", map.getFirst("CONTENT-TYPE"));
|
||||
void first_caseInsensitive() {
|
||||
HeaderMap map = parse("ConteNT-tYPe: application/json");
|
||||
assertEquals("application/json", map.first("content-type"));
|
||||
assertEquals("application/json", map.first("CONTENT-TYPE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFirst_missingHeader_returnsNull() {
|
||||
HeaderMap map = parse("", "Host: localhost");
|
||||
assertNull(map.getFirst("Accept"));
|
||||
void first_missingHeader_returnsNull() {
|
||||
HeaderMap map = parse("Host: localhost");
|
||||
assertNull(map.first("Accept"));
|
||||
}
|
||||
|
||||
// --- getAll ---
|
||||
// --- all ---
|
||||
|
||||
@Test
|
||||
void getAll_multipleValuesByName() {
|
||||
HeaderMap map = parse("", "Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
|
||||
assertEquals(List.of("a=1", "b=2"), map.getAll("Cookie"));
|
||||
void all_multipleValuesByName() {
|
||||
HeaderMap map = parse("Cookie: a=1", "Set-Cookie: token=123", "Cookie: b=2");
|
||||
assertEquals(List.of("a=1", "b=2"), map.all("Cookie"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAll_missingHeader_returnsEmptyList() {
|
||||
HeaderMap map = parse("", "Host: localhost");
|
||||
assertTrue(map.getAll("Cookie").isEmpty());
|
||||
void all_missingHeader_returnsEmptyList() {
|
||||
HeaderMap map = parse("Host: localhost");
|
||||
assertTrue(map.all("Cookie").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAll_returnsAllHeaders() {
|
||||
HeaderMap map = parse("", "A: 1", "B: 2");
|
||||
assertEquals(List.of("1", "2"), map.getAll());
|
||||
void all_returnsAllHeaders() {
|
||||
HeaderMap map = parse("A: 1", "B: 2");
|
||||
assertEquals(List.of("1", "2"), map.all());
|
||||
}
|
||||
|
||||
// --- getView ---
|
||||
// --- view ---
|
||||
|
||||
@Test
|
||||
void getView_returnsZeroCopyView() {
|
||||
HeaderMap map = parse("", "Host: localhost");
|
||||
ByteView view = map.getView("Host");
|
||||
void view_returnsZeroCopyView() {
|
||||
HeaderMap map = parse("Host: localhost");
|
||||
ByteView view = map.view("Host");
|
||||
assertNotNull(view);
|
||||
assertEquals(9, view.length());
|
||||
assertEquals('l', view.byteAt(0));
|
||||
@@ -86,20 +76,18 @@ class HeaderMapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getView_missingHeader_returnsNull() {
|
||||
HeaderMap map = parse("", "Host: localhost");
|
||||
assertNull(map.getView("Accept"));
|
||||
void view_missingHeader_returnsNull() {
|
||||
HeaderMap map = parse("Host: localhost");
|
||||
assertNull(map.view("Accept"));
|
||||
}
|
||||
|
||||
// --- limit ---
|
||||
|
||||
|
||||
// --- empty ---
|
||||
|
||||
@Test
|
||||
void maxHeadersLimit() {
|
||||
byte[] buffer = new byte[100];
|
||||
HeaderMap map = new HeaderMap(buffer);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
map.add(0, 1, 1, 1);
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> map.add(0, 1, 1, 1));
|
||||
void emptyMap_returnsNullAndEmptyList() {
|
||||
HeaderMap map = new HeaderMap();
|
||||
assertNull(map.first("Host"));
|
||||
assertTrue(map.all("Host").isEmpty());
|
||||
assertTrue(map.all().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class RequestLineTest {
|
||||
ByteView path = viewOf("/api");
|
||||
ByteView query = viewOf("q=1");
|
||||
ByteView proto = viewOf("HTTP/1.1");
|
||||
HeaderMap headers = new HeaderMap(new byte[0]);
|
||||
HeaderMap headers = new HeaderMap();
|
||||
|
||||
RequestLine rl = new RequestLine(HttpMethod.GET, path, query, proto, headers);
|
||||
|
||||
|
||||
@@ -25,64 +25,65 @@ class RequestTest {
|
||||
// --- creation ---
|
||||
|
||||
@Test
|
||||
void request_creationAndGetters() {
|
||||
HeaderMap headers = new HeaderMap(new byte[0]);
|
||||
void request_creationAndAccessors() {
|
||||
HeaderMap headers = new HeaderMap();
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/path"), viewOf("q=1"), viewOf("HTTP/1.1"), headers);
|
||||
byte[] body = "body".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
Request r = new Request(line, body);
|
||||
|
||||
assertEquals(line, r.getRequestLine());
|
||||
assertArrayEquals(body, r.getBody());
|
||||
|
||||
assertEquals(HttpMethod.GET, r.method());
|
||||
assertEquals("/path", r.path());
|
||||
assertArrayEquals(body, r.body().bytes());
|
||||
assertNotNull(r.toString());
|
||||
}
|
||||
|
||||
// --- delegates ---
|
||||
// --- headers ---
|
||||
|
||||
@Test
|
||||
void headers_delegatesToRequestLine() {
|
||||
void header_delegatesToRequestLine() {
|
||||
byte[] buffer = "Host: localhost\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
HeaderMap headers = new HeaderMap(buffer);
|
||||
headers.add(0, 4, 6, 9);
|
||||
HeaderMap headers = new HeaderMap();
|
||||
headers.reset(buffer, 0, buffer.length);
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), headers);
|
||||
Request r = new Request(line, new byte[0]);
|
||||
|
||||
assertEquals("localhost", r.getHeader("Host"));
|
||||
assertEquals(List.of("localhost"), r.getHeaders("Host"));
|
||||
assertEquals(List.of("localhost"), r.getHeaders());
|
||||
|
||||
assertEquals("localhost", r.header("Host"));
|
||||
assertEquals(List.of("localhost"), r.headers("Host"));
|
||||
assertEquals(List.of("localhost"), r.headers());
|
||||
}
|
||||
|
||||
// --- pathParams ---
|
||||
// --- path params ---
|
||||
|
||||
@Test
|
||||
void pathParam_lazyGet() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
|
||||
void param_lazyGet() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
|
||||
Request r = new Request(line, new byte[0]);
|
||||
|
||||
assertNull(r.getPathParam("id"));
|
||||
|
||||
|
||||
assertNull(r.param("id"));
|
||||
|
||||
r.setPathParams(new PathParams(viewOf("/123"), new String[]{"id"}, new int[]{1}, new int[]{3}));
|
||||
assertEquals("123", r.getPathParam("id"));
|
||||
assertEquals("123", r.param("id"));
|
||||
}
|
||||
|
||||
// --- queryParams ---
|
||||
// --- query params ---
|
||||
|
||||
@Test
|
||||
void queryParam_lazyGet_fromQueryString() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
|
||||
void query_lazyGet_fromQueryString() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), viewOf("a=1&b=2&b=3"), viewOf("HTTP/1.1"), new HeaderMap());
|
||||
Request r = new Request(line, new byte[0]);
|
||||
|
||||
assertEquals("1", r.getQueryParam("a"));
|
||||
assertEquals("2", r.getQueryParam("b")); // First value
|
||||
assertEquals(List.of("2", "3"), r.getQueryParams("b"));
|
||||
|
||||
assertEquals("1", r.query("a"));
|
||||
assertEquals("2", r.query("b"));
|
||||
assertEquals(List.of("2", "3"), r.queries("b"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void queryParam_lazyGet_nullQueryString() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap(new byte[0]));
|
||||
void query_lazyGet_nullQueryString() {
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, viewOf("/"), null, viewOf("HTTP/1.1"), new HeaderMap());
|
||||
Request r = new Request(line, new byte[0]);
|
||||
|
||||
assertNull(r.getQueryParam("a"));
|
||||
assertTrue(r.getQueryParams("a").isEmpty());
|
||||
|
||||
assertNull(r.query("a"));
|
||||
assertTrue(r.queries("a").isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@ package dev.relism.models;
|
||||
import dev.relism.http.ContentType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ResponseTest {
|
||||
|
||||
// --- creation ---
|
||||
// --- construction ---
|
||||
|
||||
@Test
|
||||
void constructor_withByteArray() {
|
||||
byte[] body = "bytes".getBytes(StandardCharsets.UTF_8);
|
||||
Response r = new Response(200, body, ContentType.BINARY);
|
||||
|
||||
|
||||
assertEquals(200, r.getStatusCode());
|
||||
assertArrayEquals(body, r.getBody());
|
||||
assertArrayEquals("application/octet-stream".getBytes(StandardCharsets.UTF_8), r.getContentType());
|
||||
@@ -25,47 +27,98 @@ class ResponseTest {
|
||||
@Test
|
||||
void constructor_withStringText() {
|
||||
Response r = new Response(404, "Not Found Text", ContentType.TEXT_PLAIN);
|
||||
|
||||
|
||||
assertEquals(404, r.getStatusCode());
|
||||
assertArrayEquals("Not Found Text".getBytes(StandardCharsets.UTF_8), r.getBody());
|
||||
assertArrayEquals(ContentType.TEXT_PLAIN.getBytes(), r.getContentType());
|
||||
}
|
||||
|
||||
// --- setters ---
|
||||
// --- fluent mutators ---
|
||||
|
||||
@Test
|
||||
void setContentType_byEnum() {
|
||||
void type_byEnum() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
r.setContentType(ContentType.JSON);
|
||||
r.type(ContentType.JSON);
|
||||
assertArrayEquals(ContentType.JSON.getBytes(), r.getContentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setContentType_byString() {
|
||||
void type_byString() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
r.setContentType("application/custom");
|
||||
r.type("application/custom");
|
||||
assertArrayEquals("application/custom".getBytes(StandardCharsets.UTF_8), r.getContentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setBody_withByteArray() {
|
||||
void body_withByteArray() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
byte[] newBody = "new".getBytes(StandardCharsets.UTF_8);
|
||||
r.setBody(newBody);
|
||||
r.body(newBody);
|
||||
assertArrayEquals(newBody, r.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setBody_withObjectConvertedToString() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
r.setBody(12345); // auto boxes to Integer, toString called
|
||||
r.setBody(12345);
|
||||
assertArrayEquals("12345".getBytes(StandardCharsets.UTF_8), r.getBody());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void setStatusCode() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
r.setStatusCode(201);
|
||||
assertEquals(201, r.getStatusCode());
|
||||
}
|
||||
|
||||
// --- streaming ---
|
||||
|
||||
@Test
|
||||
void stream_knownLength() {
|
||||
InputStream src = new ByteArrayInputStream("body".getBytes());
|
||||
Response r = new Response(200, ContentType.TEXT_PLAIN).stream(src, 4L);
|
||||
|
||||
assertTrue(r.isStreaming());
|
||||
assertFalse(r.isChunked());
|
||||
assertEquals(4L, r.getStreamLength());
|
||||
assertSame(src, r.getStream());
|
||||
assertNull(r.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chunked_setsChunkedFlag() {
|
||||
InputStream src = new ByteArrayInputStream("x".getBytes());
|
||||
Response r = new Response(200, ContentType.TEXT_PLAIN).chunked(src);
|
||||
|
||||
assertTrue(r.isStreaming());
|
||||
assertTrue(r.isChunked());
|
||||
assertSame(src, r.getStream());
|
||||
assertNull(r.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isStreaming_falseForFixedBody() {
|
||||
assertFalse(new Response(200, new byte[0], ContentType.TEXT_PLAIN).isStreaming());
|
||||
}
|
||||
|
||||
// --- header ---
|
||||
|
||||
@Test
|
||||
void header_encodedCorrectly() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
r.header("X-Foo", "bar");
|
||||
assertArrayEquals("X-Foo: bar\r\n".getBytes(StandardCharsets.UTF_8), r.getHeaders().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void header_chaining_returnsSelf() {
|
||||
Response r = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||
assertSame(r, r.header("A", "1").header("B", "2"));
|
||||
assertEquals(2, r.getHeaders().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHeaders_emptyWhenNoneAdded() {
|
||||
assertTrue(new Response(200, new byte[0], ContentType.TEXT_PLAIN).getHeaders().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class GlobalRouterTest {
|
||||
RequestLine line = new RequestLine(
|
||||
HttpMethod.GET, pathView, null,
|
||||
new FastPathViews.RequestByteView("HTTP/1.1".getBytes(StandardCharsets.UTF_8), 0, 8),
|
||||
new HeaderMap(new byte[0])
|
||||
new HeaderMap()
|
||||
);
|
||||
return new Request(line, new byte[0]);
|
||||
}
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ class FastPathRouterImplTest {
|
||||
RequestLine line = new RequestLine(
|
||||
method, pathView, null,
|
||||
new FastPathViews.RequestByteView("HTTP/1.1".getBytes(StandardCharsets.UTF_8), 0, 8),
|
||||
new HeaderMap(new byte[0])
|
||||
new HeaderMap()
|
||||
);
|
||||
return new Request(line, new byte[0]);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class FastPathRouterImplTest {
|
||||
// --- route ---
|
||||
|
||||
@Test
|
||||
void route_lazyCompilationAndMatch() {
|
||||
void route_lazyCompilationAndMatch() throws Exception {
|
||||
FastPathRouterImpl router = new FastPathRouterImpl();
|
||||
|
||||
router.get("/a", (req, res) -> "A");
|
||||
@@ -57,7 +57,7 @@ class FastPathRouterImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void route_extractsPathParams() {
|
||||
void route_extractsPathParams() throws Exception {
|
||||
FastPathRouterImpl router = new FastPathRouterImpl();
|
||||
|
||||
router.get("/users/{id}/items/{itemId}", (req, res) -> "Extract");
|
||||
@@ -70,7 +70,7 @@ class FastPathRouterImplTest {
|
||||
|
||||
// Verify path params were injected
|
||||
assertNotNull(request.getPathParams());
|
||||
assertEquals("123", request.getPathParam("id"));
|
||||
assertEquals("456", request.getPathParam("itemId"));
|
||||
assertEquals("123", request.param("id"));
|
||||
assertEquals("456", request.param("itemId"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ErrorPagesTest {
|
||||
byte[] protoBytes = protocol.getBytes(StandardCharsets.UTF_8);
|
||||
FastPathViews.RequestByteView protoView = new FastPathViews.RequestByteView(protoBytes, 0, protoBytes.length);
|
||||
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, pathView, null, protoView, new HeaderMap(new byte[0]));
|
||||
RequestLine line = new RequestLine(HttpMethod.GET, pathView, null, protoView, new HeaderMap());
|
||||
return new Request(line, new byte[0]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user