Add test coverage for core server components: annotation scanning, routing, rate limiting, CORS, and JSON handling
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package dev.coph.nextusweb.server.router;
|
||||
|
||||
import dev.coph.nextusweb.server.router.exception.BadRequestException;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RequestTest {
|
||||
|
||||
private FullHttpRequest build(HttpMethod method, String uri, String body) {
|
||||
var content = body == null
|
||||
? Unpooled.EMPTY_BUFFER
|
||||
: Unpooled.copiedBuffer(body, CharsetUtil.UTF_8);
|
||||
return new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, uri, content);
|
||||
}
|
||||
|
||||
record Payload(String name, int age) {}
|
||||
|
||||
@Test
|
||||
void pathParamReturnsFromMap() {
|
||||
Request req = new Request(build(HttpMethod.GET, "/u/1", null), Map.of("id", "1"));
|
||||
assertEquals("1", req.pathParam("id"));
|
||||
assertNull(req.pathParam("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryParamReturnsFirstValue() {
|
||||
Request req = new Request(build(HttpMethod.GET, "/?a=1&a=2&b=foo", null), Map.of());
|
||||
assertEquals("1", req.queryParam("a"));
|
||||
assertEquals("foo", req.queryParam("b"));
|
||||
assertNull(req.queryParam("nope"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryParamsReturnsAllValues() {
|
||||
Request req = new Request(build(HttpMethod.GET, "/?a=1&a=2", null), Map.of());
|
||||
assertEquals(java.util.List.of("1", "2"), req.queryParams("a"));
|
||||
assertTrue(req.queryParams("missing").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerReturnsValue() {
|
||||
FullHttpRequest raw = build(HttpMethod.GET, "/", null);
|
||||
raw.headers().set("X-Foo", "bar");
|
||||
Request req = new Request(raw, Map.of());
|
||||
assertEquals("bar", req.header("X-Foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bodyReturnsContentAsString() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", "hello"), Map.of());
|
||||
assertEquals("hello", req.body());
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodAndPathExpose() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/a/b?q=1", null), Map.of());
|
||||
assertEquals(HttpMethod.POST, req.method());
|
||||
assertEquals("/a/b", req.path());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonReturnsNullNodeForEmptyBody() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", null), Map.of());
|
||||
var node = req.json();
|
||||
assertNotNull(node);
|
||||
assertTrue(node.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonParsesObject() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", "{\"a\":1}"), Map.of());
|
||||
var node = req.json();
|
||||
assertTrue(node.has("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonThrowsBadRequestOnInvalidJson() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", "not-json"), Map.of());
|
||||
assertThrows(BadRequestException.class, req::json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonAsDeserializes() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", "{\"name\":\"x\",\"age\":42}"), Map.of());
|
||||
Payload p = req.jsonAs(Payload.class);
|
||||
assertEquals("x", p.name());
|
||||
assertEquals(42, p.age());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonAsThrowsBadRequestOnInvalid() {
|
||||
Request req = new Request(build(HttpMethod.POST, "/", "not-json"), Map.of());
|
||||
assertThrows(BadRequestException.class, () -> req.jsonAs(Payload.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package dev.coph.nextusweb.server.router;
|
||||
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ResponseTest {
|
||||
|
||||
@Test
|
||||
void defaultStatusIs200AndEmptyBody() {
|
||||
Response res = new Response();
|
||||
assertEquals(200, res.status());
|
||||
assertArrayEquals(new byte[0], res.body());
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusIsFluent() {
|
||||
Response res = new Response().status(404);
|
||||
assertEquals(404, res.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerSetsValue() {
|
||||
Response res = new Response().header("X-Foo", "bar");
|
||||
assertEquals("bar", res.headers().get("X-Foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void textSetsBodyAndContentType() {
|
||||
Response res = new Response().text("hello");
|
||||
assertEquals("hello", new String(res.body(), StandardCharsets.UTF_8));
|
||||
assertTrue(res.headers().get(HttpHeaderNames.CONTENT_TYPE).startsWith("text/plain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonStringSetsBodyAndContentType() {
|
||||
Response res = new Response().json("{\"a\":1}");
|
||||
assertEquals("{\"a\":1}", new String(res.body(), StandardCharsets.UTF_8));
|
||||
assertTrue(res.headers().get(HttpHeaderNames.CONTENT_TYPE).startsWith("application/json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonObjectSerializesValue() {
|
||||
Response res = new Response().json(Map.of("k", "v"));
|
||||
String s = new String(res.body(), StandardCharsets.UTF_8);
|
||||
assertTrue(s.contains("\"k\""));
|
||||
assertTrue(s.contains("\"v\""));
|
||||
assertTrue(res.headers().get(HttpHeaderNames.CONTENT_TYPE).startsWith("application/json"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package dev.coph.nextusweb.server.router;
|
||||
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RouterTest {
|
||||
|
||||
private final Router.Handler noop = (req, res) -> {};
|
||||
|
||||
@Test
|
||||
void getRegistersAndResolvesExactPath() {
|
||||
Router r = new Router().get("/hello", noop);
|
||||
assertInstanceOf(Router.Resolution.Match.class, r.resolve(HttpMethod.GET, "/hello"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void postPutDeleteRegister() {
|
||||
Router r = new Router()
|
||||
.post("/p", noop)
|
||||
.put("/u", noop)
|
||||
.delete("/d", noop);
|
||||
assertInstanceOf(Router.Resolution.Match.class, r.resolve(HttpMethod.POST, "/p"));
|
||||
assertInstanceOf(Router.Resolution.Match.class, r.resolve(HttpMethod.PUT, "/u"));
|
||||
assertInstanceOf(Router.Resolution.Match.class, r.resolve(HttpMethod.DELETE, "/d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void notFoundForUnknownPath() {
|
||||
Router r = new Router().get("/a", noop);
|
||||
assertInstanceOf(Router.Resolution.NotFound.class, r.resolve(HttpMethod.GET, "/x"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodNotAllowedWhenPathMatchesDifferentMethod() {
|
||||
Router r = new Router().get("/a", noop);
|
||||
Router.Resolution res = r.resolve(HttpMethod.POST, "/a");
|
||||
Router.Resolution.MethodNotAllowed mna = assertInstanceOf(Router.Resolution.MethodNotAllowed.class, res);
|
||||
assertTrue(mna.allowedMethods().contains(HttpMethod.GET));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathParamsAreExtracted() {
|
||||
Router r = new Router().get("/u/{id}", noop);
|
||||
Router.Resolution res = r.resolve(HttpMethod.GET, "/u/42");
|
||||
Router.Resolution.Match m = assertInstanceOf(Router.Resolution.Match.class, res);
|
||||
assertEquals("42", m.pathParams().get("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wildcardMatches() {
|
||||
Router r = new Router().get("/files/*", noop);
|
||||
assertInstanceOf(Router.Resolution.Match.class, r.resolve(HttpMethod.GET, "/files/anything"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void useAddsMiddlewareReturned() {
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
Router r = new Router().use((req, res) -> count.incrementAndGet());
|
||||
assertEquals(1, r.middlewares().size());
|
||||
r.middlewares().getFirst().accept(null, null);
|
||||
assertEquals(1, count.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWorksWithCustomMethod() {
|
||||
Router r = new Router().register(HttpMethod.valueOf("OPTIONS"), "/x", noop);
|
||||
assertInstanceOf(Router.Resolution.Match.class,
|
||||
r.resolve(HttpMethod.valueOf("OPTIONS"), "/x"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerInvocationWorks() throws Exception {
|
||||
AtomicInteger called = new AtomicInteger();
|
||||
Router r = new Router().get("/x", (req, res) -> called.incrementAndGet());
|
||||
var match = (Router.Resolution.Match) r.resolve(HttpMethod.GET, "/x");
|
||||
match.handler().handle(null, null);
|
||||
assertEquals(1, called.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.coph.nextusweb.server.router.exception;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class BadRequestExceptionTest {
|
||||
|
||||
@Test
|
||||
void messageIsCarried() {
|
||||
BadRequestException e = new BadRequestException("oops");
|
||||
assertEquals("oops", e.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isRuntimeException() {
|
||||
assertInstanceOf(RuntimeException.class, new BadRequestException("x"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user