From dbca976f81fda580f9f559d6e45620c0d63277df Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Thu, 16 Jan 2025 21:32:16 +0100 Subject: [PATCH 1/3] fix: improve default mappings of interceptors --- .../annotations/ReflectiveEndpointLoader.kt | 39 +- .../annotations/AnnotatedRoutingTest.kt | 726 ++++++++++-------- .../example/AnnotatedRoutingExample.java | 7 +- .../io/javalin/community/routing/Route.kt | 4 +- 4 files changed, 450 insertions(+), 326 deletions(-) diff --git a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt index 09fe4b8..aa2b497 100644 --- a/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt +++ b/routing-annotations/routing-annotated/src/main/kotlin/io/javalin/community/routing/annotations/ReflectiveEndpointLoader.kt @@ -74,23 +74,32 @@ internal class ReflectiveEndpointLoader( val status = method.getAnnotation() val resultHandler = findResultHandler(method) - val route = AnnotatedRoute( - method = httpMethod, - path = ("/$endpointPath/$path").replace(repeatedPathSeparatorRegex, "/"), - version = method.getAnnotation()?.value, - handler = { - val arguments = argumentSuppliers - .map { it(this, Unit) } - .toTypedArray() + val declaredPath = "/$endpointPath/$path".replace(repeatedPathSeparatorRegex, "/").dropLastWhile { it == '/' } + val processedPaths = mutableSetOf(declaredPath) - when (async) { - true -> async { invokeAndUnwrapIfErrored(method, endpoint, arguments, ctx = this, status = status, resultHandler = resultHandler) } - else -> invokeAndUnwrapIfErrored(method, endpoint, arguments, ctx = this, status = status, resultHandler = resultHandler) - } - } - ) + if (!httpMethod.isHttpMethod && declaredPath.endsWith("*")) { + processedPaths.add(declaredPath.dropLast(1).dropLastWhile { it == '/' }) + } - endpointRoutes.add(route) + processedPaths.forEach { pathToAdd -> + endpointRoutes.add( + AnnotatedRoute( + method = httpMethod, + path = pathToAdd, + version = method.getAnnotation()?.value, + handler = { + val arguments = argumentSuppliers + .map { it(this, Unit) } + .toTypedArray() + + when (async) { + true -> async { invokeAndUnwrapIfErrored(method, endpoint, arguments, ctx = this, status = status, resultHandler = resultHandler) } + else -> invokeAndUnwrapIfErrored(method, endpoint, arguments, ctx = this, status = status, resultHandler = resultHandler) + } + } + ) + ) + } } return endpointRoutes diff --git a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt index 558f8be..02fa542 100644 --- a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt +++ b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt @@ -7,402 +7,514 @@ import io.javalin.event.JavalinLifecycleEvent.SERVER_STARTED import io.javalin.http.Context import io.javalin.http.HandlerType import io.javalin.http.HttpStatus +import io.javalin.testtools.HttpClient import io.javalin.testtools.JavalinTest import kong.unirest.Unirest import kong.unirest.Unirest.request import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import java.io.Closeable import java.util.* class AnnotatedRoutingTest { - @Test - fun `should sanitize repeated path separators`() { - val app = Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints("/test/") - object { - @Get("/with") - fun get(ctx: Context) { - } - }, - @Endpoints("test") - object { - @Get("without") - fun get(ctx: Context) { + @Nested + inner class Paths { + + @Nested + inner class Registration { + + private fun withinSharedScenario(test: (HttpClient) -> Unit) { + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints("/test") + object { + // formatter:off + @Before fun beforeEach(ctx: Context) { ctx.header("before", "true") } + @Before("/specific") fun beforeSpecific(ctx: Context) { ctx.header("before", "specific") } + @BeforeMatched fun beforeEachMatched(ctx: Context) { ctx.header("before-matched", "true") } + @AfterMatched fun afterEachMatched(ctx: Context) { ctx.header("after-matched", "true") } + @After("/specific") fun afterSpecific(ctx: Context) { ctx.header("after", "specific") } + @After fun afterEach(ctx: Context) { ctx.header("after", "true") } + @Get("/get") fun testGet(ctx: Context) { ctx.header("get", "true") } + @Post("/post") fun testPost(ctx: Context) { ctx.header("post", "true") } + @Put("/put") fun testPut(ctx: Context) { ctx.header("put", "true") } + @Delete("/delete") fun testDelete(ctx: Context) { ctx.header("delete", "true") } + @Patch("/patch") fun testPatch(ctx: Context) { ctx.header("patch", "true") } + @Head("/head") fun testHead(ctx: Context) { ctx.header("head", "true") } + @Options("/options") fun testOptions(ctx: Context) { ctx.header("options", "true") } + // formatter:on + } + ) } } - ) + ) { _, client -> test(client) } } - } - val matcher = app.unsafeConfig().pvt.internalRouter + @Test + fun `should register all annotated http endpoints with their interceptors`() { + withinSharedScenario { client -> + Route.entries + .filter { it.isHttpMethod } + .forEach { + val response = request(it.name, "${client.origin}/test/${it.name.lowercase()}").asEmpty() + assertThat(response.headers.getFirst(it.name.lowercase())).isEqualTo("true") + assertThat(response.headers.getFirst("before")).isEqualTo("true") + assertThat(response.headers.getFirst("before-matched")).isEqualTo("true") + assertThat(response.headers.getFirst("after")).isEqualTo("true") + assertThat(response.headers.getFirst("after-matched")).isEqualTo("true") + } + } + } - assertThat(matcher.findHttpHandlerEntries(HandlerType.GET, "/test/with")) - .hasSize(1) - .allMatch { it.endpoint.path == "/test/with" } + @Test + fun `should register all possible before handlers`() { + withinSharedScenario { client -> + val beforeAtRootLevel = request("GET", "${client.origin}/test").asEmpty() + assertThat(beforeAtRootLevel.headers.getFirst("before")).isEqualTo("true") - assertThat(matcher.findHttpHandlerEntries(HandlerType.GET, "/test/without")) - .hasSize(1) - .allMatch { it.endpoint.path == "/test/without" } - } + val beforeAtRootLevelWithTrailingSlash = request("GET", "${client.origin}/test/").asEmpty() + assertThat(beforeAtRootLevelWithTrailingSlash.headers.getFirst("before")).isEqualTo("true") - @Test - fun `should throw exception if route has unsupported parameter in signature`() { - assertThatThrownBy { - Javalin.create().unsafeConfig().router.mount(Annotated) { - it.registerEndpoints( - @Endpoints - object { - @Get("/test") fun test(ctx: Context, unsupported: String) {} - } - ) + val beforeAtRootLevelWithWildcard = request("GET", "${client.origin}/test/a/b/c").asEmpty() + assertThat(beforeAtRootLevelWithWildcard.headers.getFirst("before")).isEqualTo("true") + } } - } - .isExactlyInstanceOf(IllegalArgumentException::class.java) - .hasMessageContaining("Unsupported parameter type") - } - @Test - fun `should properly register all annotated endpoints`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints("/test") - object { - // formatter:off - @Before fun beforeEach(ctx: Context) { ctx.header("before", "true") } - @BeforeMatched fun beforeEachMatched(ctx: Context) { ctx.header("before-matched", "true") } - @AfterMatched fun afterEachMatched(ctx: Context) { ctx.header("after-matched", "true") } - @After fun afterEach(ctx: Context) { ctx.header("after", "true") } - @Get("/get") fun testGet(ctx: Context) { ctx.header("get", "true") } - @Post("/post") fun testPost(ctx: Context) { ctx.header("post", "true") } - @Put("/put") fun testPut(ctx: Context) { ctx.header("put", "true") } - @Delete("/delete") fun testDelete(ctx: Context) { ctx.header("delete", "true") } - @Patch("/patch") fun testPatch(ctx: Context) { ctx.header("patch", "true") } - @Head("/head") fun testHead(ctx: Context) { ctx.header("head", "true") } - @Options("/options") fun testOptions(ctx: Context) { ctx.header("options", "true") } - // formatter:on - } - ) + @Test + fun `should register before handler only at the explicitly specified path`() { + withinSharedScenario { client -> + val beforeSpecific = request("GET", "${client.origin}/test/specific").asEmpty() + assertThat(beforeSpecific.headers.getFirst("before")).isEqualTo("specific") + + val beforeTooSpecific = request("GET", "${client.origin}/test/specific/too-specific").asEmpty() + assertThat(beforeTooSpecific.headers.getFirst("before")).isNotEqualTo("specific") } } - ) { _, client -> - Route.entries - .filter { it.isHttpMethod } - .forEach { - val response = request(it.name, "${client.origin}/test/${it.name.lowercase()}").asEmpty() - assertThat(response.headers.getFirst(it.name.lowercase())).isEqualTo("true") - assertThat(response.headers.getFirst("before")).isEqualTo("true") - assertThat(response.headers.getFirst("before-matched")).isEqualTo("true") - assertThat(response.headers.getFirst("after")).isEqualTo("true") - assertThat(response.headers.getFirst("after-matched")).isEqualTo("true") + + @Test + fun `should register after handler only at the explicitly specified path`() { + withinSharedScenario { client -> + val afterSpecific = request("GET", "${client.origin}/test/specific").asEmpty() + assertThat(afterSpecific.headers.getFirst("after")).isEqualTo("specific") + + val afterTooSpecific = request("GET", "${client.origin}/test/specific/too-specific").asEmpty() + assertThat(afterTooSpecific.headers.getFirst("after")).isNotEqualTo("specific") + } + } + + @Test + fun `should register all possible after handlers`() { + withinSharedScenario { client -> + val afterAtRootLevel = request("GET", "${client.origin}/test").asEmpty() + assertThat(afterAtRootLevel.headers.getFirst("after")).isEqualTo("true") + + val afterAtRootLevelWithTrailingSlash = request("GET", "${client.origin}/test/").asEmpty() + assertThat(afterAtRootLevelWithTrailingSlash.headers.getFirst("after")).isEqualTo("true") + + val afterAtRootLevelWithWildcard = request("GET", "${client.origin}/test/a/b/c").asEmpty() + assertThat(afterAtRootLevelWithWildcard.headers.getFirst("after")).isEqualTo("true") + } + } + + @Test + fun `should not register before and after handlers below the specified path`() { + withinSharedScenario { client -> + val beforeAtRootLevel = request("GET", "${client.origin}/").asEmpty() + assertThat(beforeAtRootLevel.headers.getFirst("after")).isEmpty() + + val afterAtRootLevel = request("GET", "${client.origin}/").asEmpty() + assertThat(afterAtRootLevel.headers.getFirst("after")).isEmpty() } + } } - @Test - fun `should run async method in async context`() { - JavalinTest.test( - Javalin.create { cfg -> + @Test + fun `should sanitize repeated path separators`() { + val app = Javalin.create { cfg -> cfg.router.mount(Annotated) { it.registerEndpoints( - @Endpoints + @Endpoints("/test/") object { - // formatter:off - @Before("/test") fun before(ctx: Context) { ctx.header("sync", Thread.currentThread().name) } - @Get("/test", async = true) fun test(ctx: Context) { ctx.header("async", Thread.currentThread().name) } - // formatter:on + @Get("/with") + fun get(ctx: Context) { + } + }, + @Endpoints("test") + object { + @Get("without") + fun get(ctx: Context) { + } } ) } } - ) { _, client -> - val response = Unirest.get("${client.origin}/test").asEmpty() - val sync = response.headers.getFirst("sync") - assertThat(sync).isNotNull + val matcher = app.unsafeConfig().pvt.internalRouter - val async = response.headers.getFirst("async") - assertThat(async).isNotNull + assertThat(matcher.findHttpHandlerEntries(HandlerType.GET, "/test/with")) + .hasSize(1) + .allMatch { it.endpoint.path == "/test/with" } - assertThat(sync).isNotEqualTo(async) + assertThat(matcher.findHttpHandlerEntries(HandlerType.GET, "/test/without")) + .hasSize(1) + .allMatch { it.endpoint.path == "/test/without" } } - } - @Test - fun `should inject all supported properties from context`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints - object { - @Post("/test/{param}") - fun test( - ctx: Context, - @Param param: Int, - @Header header: Int, - @Header optionalHeader: Optional, - @Query query: Int, - @Cookie cookie: Int, - @Body body: Int, - ) { - ctx.header("param", param.toString()) - ctx.header("header", header.toString()) - ctx.header("optionalHeader", optionalHeader.orElse("default")) - ctx.header("query", query.toString()) - ctx.header("cookie", cookie.toString()) - ctx.header("body", body.toString()) + @Test + fun `should skip methods in endpoint class that are not annotated`() { + assertDoesNotThrow { + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints + object { + fun regularMethod() {} } - } - ) + ) + } } } - ) { _, client -> - val responseHeaders = Unirest.post("${client.origin}/test/1") - .header("header", "2") - .queryString("query", "3") - .cookie("cookie", "4") - .body("5") - .asEmpty() - .headers - - assertThat(responseHeaders.getFirst("param")).isEqualTo("1") - assertThat(responseHeaders.getFirst("header")).isEqualTo("2") - assertThat(responseHeaders.getFirst("optionalHeader")).isEqualTo("default") - assertThat(responseHeaders.getFirst("query")).isEqualTo("3") - assertThat(responseHeaders.getFirst("cookie")).isEqualTo("4") - assertThat(responseHeaders.getFirst("body")).isEqualTo("5") } + } - @Test - fun `should respond with bad request if property cannot be mapped into parameter`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints - object { - @Get("/test/{param}") - fun test(@Param param: Int) = Unit - } - ) + @Nested + inner class Injections { + + @Test + fun `should inject all supported properties from context`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints + object { + @Post("/test/{param}") + fun test( + ctx: Context, + @Param param: Int, + @Header header: Int, + @Header optionalHeader: Optional, + @Query query: Int, + @Cookie cookie: Int, + @Body body: Int, + ) { + ctx.header("param", param.toString()) + ctx.header("header", header.toString()) + ctx.header("optionalHeader", optionalHeader.orElse("default")) + ctx.header("query", query.toString()) + ctx.header("cookie", cookie.toString()) + ctx.header("body", body.toString()) + } + } + ) + } } + ) { _, client -> + val responseHeaders = Unirest.post("${client.origin}/test/1") + .header("header", "2") + .queryString("query", "3") + .cookie("cookie", "4") + .body("5") + .asEmpty() + .headers + + assertThat(responseHeaders.getFirst("param")).isEqualTo("1") + assertThat(responseHeaders.getFirst("header")).isEqualTo("2") + assertThat(responseHeaders.getFirst("optionalHeader")).isEqualTo("default") + assertThat(responseHeaders.getFirst("query")).isEqualTo("3") + assertThat(responseHeaders.getFirst("cookie")).isEqualTo("4") + assertThat(responseHeaders.getFirst("body")).isEqualTo("5") } - ) { _, client -> - val response = Unirest.get("${client.origin}/test/abc").asString() - assertThat(response.status).isEqualTo(400) - } - @Test - fun `should skip methods in endpoint class that are not annotated`() { - assertDoesNotThrow { - Javalin.create { cfg -> - cfg.router.mount(Annotated) { + @Test + fun `should respond with bad request if property cannot be mapped into parameter`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints + object { + @Get("/test/{param}") + fun test(@Param param: Int) = Unit + } + ) + } + } + ) { _, client -> + val response = Unirest.get("${client.origin}/test/abc").asString() + assertThat(response.status).isEqualTo(400) + } + + + @Test + fun `should throw exception if route has unsupported parameter in signature`() { + assertThatThrownBy { + Javalin.create().unsafeConfig().router.mount(Annotated) { it.registerEndpoints( @Endpoints object { - fun regularMethod() {} + @Get("/test") fun test(ctx: Context, unsupported: String) {} } ) } } + .isExactlyInstanceOf(IllegalArgumentException::class.java) + .hasMessageContaining("Unsupported parameter type") } + } - @Test - fun `should throw if two routes with the same versions are found`() { - assertThatThrownBy { - Javalin.create { - it.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints("/api/users") - object { - @Version("1") - @Get - fun findAll(ctx: Context) { - ctx.result("Panda") - } - }, - @Endpoints("/api/users") - object { - @Version("1") - @Get - fun test(ctx: Context) { - ctx.result("Red Panda") + + @Nested + inner class Versioning { + + @Test + fun `should throw if two routes with the same versions are found`() { + assertThatThrownBy { + Javalin.create { + it.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints("/api/users") + object { + @Version("1") + @Get + fun findAll(ctx: Context) { + ctx.result("Panda") + } + }, + @Endpoints("/api/users") + object { + @Version("1") + @Get + fun test(ctx: Context) { + ctx.result("Red Panda") + } } - } - ) + ) + } } } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Duplicated version found for the same route: GET /api/users (versions: [1, 1])") } - .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("Duplicated version found for the same route: GET /api/users/ (versions: [1, 1])") - } - @Test - fun `should properly serve versioned routes`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - @Endpoints("/api/users") - object { - @Version("1") - @Get - fun findAll(ctx: Context) { - ctx.result("Panda") - } - }, - @Endpoints("/api/users") - object { - @Version("2") - @Get - fun test(ctx: Context) { - ctx.result("Red Panda") + @Test + fun `should properly serve versioned routes`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints("/api/users") + object { + @Version("1") + @Get + fun findAll(ctx: Context) { + ctx.result("Panda") + } + }, + @Endpoints("/api/users") + object { + @Version("2") + @Get + fun test(ctx: Context) { + ctx.result("Red Panda") + } } - } - ) + ) + } } + ) { _, client -> + val v1 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "1").asString().body + val v2 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "2").asString().body + val v3 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "3").asString().body + + assertThat(v1).isEqualTo("Panda") + assertThat(v2).isEqualTo("Red Panda") + assertThat(v3).isEqualTo("This endpoint does not support the requested API version (3).") } - ) { _, client -> - val v1 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "1").asString().body - val v2 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "2").asString().body - val v3 = Unirest.get("${client.origin}/api/users").header("X-API-Version", "3").asString().body - - assertThat(v1).isEqualTo("Panda") - assertThat(v2).isEqualTo("Red Panda") - assertThat(v3).isEqualTo("This endpoint does not support the requested API version (3).") - } - @Test - fun `should properly handle exceptions`() = - JavalinTest.test( - Javalin.create { - it.router.mount(Annotated) { cfg -> - cfg.registerEndpoints(object { - @Get("/throwing") - fun throwing(ctx: Context): Nothing = throw IllegalStateException("This is a test") - - @ExceptionHandler(IllegalStateException::class) - fun handleException(ctx: Context, e: IllegalStateException) { - ctx.result(e::class.java.name) - } - }) + } + + @Nested + inner class Async { + + @Test + fun `should run async method in async context`() { + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + @Endpoints + object { + // formatter:off + @Before("/test") fun before(ctx: Context) { ctx.header("sync", Thread.currentThread().name) } + @Get("/test", async = true) fun test(ctx: Context) { ctx.header("async", Thread.currentThread().name) } + // formatter:on + } + ) + } } + ) { _, client -> + val response = Unirest.get("${client.origin}/test").asEmpty() + + val sync = response.headers.getFirst("sync") + assertThat(sync).isNotNull + + val async = response.headers.getFirst("async") + assertThat(async).isNotNull + + assertThat(sync).isNotEqualTo(async) } - ) { _, client -> - assertThat(Unirest.get("${client.origin}/throwing").asString().body).isEqualTo("java.lang.IllegalStateException") } - @Test - fun `should handle lifecycle events`() { - val log = mutableListOf() - - JavalinTest.test( - Javalin.create { - it.router.mount(Annotated) { cfg -> - cfg.registerEndpoints(object { - @LifecycleEventHandler(SERVER_STARTED) - fun onStart() { - log.add("Started") - } - }) + } + + @Nested + inner class Exceptions { + + @Test + fun `should properly handle exceptions`() = + JavalinTest.test( + Javalin.create { + it.router.mount(Annotated) { cfg -> + cfg.registerEndpoints(object { + @Get("/throwing") + fun throwing(ctx: Context): Nothing = throw IllegalStateException("This is a test") + + @ExceptionHandler(IllegalStateException::class) + fun handleException(ctx: Context, e: IllegalStateException) { + ctx.result(e::class.java.name) + } + }) + } } + ) { _, client -> + assertThat(Unirest.get("${client.origin}/throwing").asString().body).isEqualTo("java.lang.IllegalStateException") } - ) { _, _ -> - assertThat(log).containsExactly("Started") - } + } - @Test - fun `should throw for unsupported return types`() { - assertThatThrownBy { - Javalin.create { - it.router.mount(Annotated) { cfg -> - cfg.registerEndpoints( - object { - @Get("/unsupported") - fun unsupported(ctx: Context): Int = 1 - } - ) + @Nested + inner class LifecycleEvents { + + @Test + fun `should handle lifecycle events`() { + val log = mutableListOf() + + JavalinTest.test( + Javalin.create { + it.router.mount(Annotated) { cfg -> + cfg.registerEndpoints(object { + @LifecycleEventHandler(SERVER_STARTED) + fun onStart() { + log.add("Started") + } + }) + } } + ) { _, _ -> + assertThat(log).containsExactly("Started") } } - .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("Unsupported return type: int") - } - - private open class Animal - private open class Panda : Animal() - private open class RedPanda : Panda() - @Test - fun `should properly handle inheritance`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerResultHandler { ctx, _ -> ctx.result("Animal") } - it.registerResultHandler { ctx, _ -> ctx.result("RedPanda") } - it.registerResultHandler { ctx, _ -> ctx.result("Panda") } + } - it.registerEndpoints( - object { - @Get("/base") - fun base(ctx: Context): Animal = Animal() + @Nested + inner class Results { + + @Test + fun `should use status code from annotation`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerEndpoints( + object { + @Get("/test") + @Status(success = HttpStatus.IM_A_TEAPOT) + fun test(ctx: Context): String = "abc" + } + ) + } + } + ) { _, client -> + assertThat(Unirest.get("${client.origin}/test").asString().status).isEqualTo(HttpStatus.IM_A_TEAPOT.code) + } - @Get("/closest") - fun closest(ctx: Context): RedPanda = RedPanda() - } - ) + @Test + fun `should throw for unsupported return types`() { + assertThatThrownBy { + Javalin.create { + it.router.mount(Annotated) { cfg -> + cfg.registerEndpoints( + object { + @Get("/unsupported") + fun unsupported(ctx: Context): Int = 1 + } + ) + } } } - ) { _, client -> - assertThat(Unirest.get("${client.origin}/base").asString().body).isEqualTo("Animal") - assertThat(Unirest.get("${client.origin}/closest").asString().body).isEqualTo("RedPanda") + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Unsupported return type: int") } - interface Heavy - private open class GiantPanda : Panda(), Heavy - - @Test - fun `should throw if result handler matched multiple classes`() { - assertThatThrownBy { - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerResultHandler { ctx, _ -> ctx.result("Panda") } - it.registerResultHandler { ctx, _ -> ctx.result("Heavy") } - it.registerEndpoints(object { - @Get("/test") - fun test(ctx: Context): GiantPanda = GiantPanda() - }) + private open inner class Animal + private open inner class Panda : Animal() + private open inner class RedPanda : Panda() + + @Test + fun `should properly handle inheritance`() = + JavalinTest.test( + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerResultHandler { ctx, _ -> ctx.result("Animal") } + it.registerResultHandler { ctx, _ -> ctx.result("RedPanda") } + it.registerResultHandler { ctx, _ -> ctx.result("Panda") } + + it.registerEndpoints( + object { + @Get("/base") + fun base(ctx: Context): Animal = Animal() + + @Get("/closest") + fun closest(ctx: Context): RedPanda = RedPanda() + } + ) + } } + ) { _, client -> + assertThat(Unirest.get("${client.origin}/base").asString().body).isEqualTo("Animal") + assertThat(Unirest.get("${client.origin}/closest").asString().body).isEqualTo("RedPanda") } + + private open inner class GiantPanda : Panda(), Closeable { + override fun close() {} } - .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("Unable to determine handler for type class") - } - @Test - fun `should use status code from annotation`() = - JavalinTest.test( - Javalin.create { cfg -> - cfg.router.mount(Annotated) { - it.registerEndpoints( - object { + @Test + fun `should throw if result handler matched multiple classes`() { + assertThatThrownBy { + Javalin.create { cfg -> + cfg.router.mount(Annotated) { + it.registerResultHandler { ctx, _ -> ctx.result("Panda") } + it.registerResultHandler { ctx, _ -> ctx.result("Closeable") } + it.registerEndpoints(object { @Get("/test") - @Status(success = HttpStatus.IM_A_TEAPOT) - fun test(ctx: Context): String = "abc" - } - ) + fun test(ctx: Context): GiantPanda = GiantPanda() + }) + } } } - ) { _, client -> - assertThat(Unirest.get("${client.origin}/test").asString().status).isEqualTo(HttpStatus.IM_A_TEAPOT.code) + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Unable to determine handler for type class") } + } + } \ No newline at end of file diff --git a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/example/AnnotatedRoutingExample.java b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/example/AnnotatedRoutingExample.java index 017d5b6..a74a985 100644 --- a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/example/AnnotatedRoutingExample.java +++ b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/example/AnnotatedRoutingExample.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import static io.javalin.community.routing.annotations.AnnotatedRouting.Annotated; import static io.javalin.http.Header.AUTHORIZATION; @@ -60,7 +61,7 @@ public ExampleEndpoints(ExampleService exampleService) { // use Javalin-specific routes @Before void beforeEach(Context ctx) { - ctx.header("X-Example", "Example"); + System.out.println("Before each request: " + ctx.method() + " " + ctx.path()); } // describe http method and path with annotation @@ -117,14 +118,14 @@ public static void main(String[] args) { }); // test request to `saveExample` endpoint - HttpResponse saved = Unirest.post("http://localhost:7000/api/hello") + HttpResponse saved = Unirest.post("http://localhost:8080/api/hello") .basicAuth("Panda", "passwd") .body(new ExampleDto("Panda")) .asEmpty(); System.out.println("Entity saved: " + saved.getStatusText()); // Entity saved: OK // test request to `findExampleV2` endpoint - String result = Unirest.get("http://localhost:7000/api/hello/Panda") + String result = Unirest.get("http://localhost:8080/api/hello/Panda") .header("X-API-Version", "2") .asString() .getBody(); diff --git a/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt b/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt index 492e852..a1b18d2 100644 --- a/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt +++ b/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt @@ -1,6 +1,8 @@ package io.javalin.community.routing -enum class Route(val isHttpMethod: Boolean = true) { +enum class Route( + val isHttpMethod: Boolean = true, +) { HEAD, PATCH, OPTIONS, From 3df22653fdcadf9bcd8a37be8f0347d091fa6afe Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Thu, 16 Jan 2025 21:33:27 +0100 Subject: [PATCH 2/3] chore: revert --- .../src/main/kotlin/io/javalin/community/routing/Route.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt b/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt index a1b18d2..492e852 100644 --- a/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt +++ b/routing-core/src/main/kotlin/io/javalin/community/routing/Route.kt @@ -1,8 +1,6 @@ package io.javalin.community.routing -enum class Route( - val isHttpMethod: Boolean = true, -) { +enum class Route(val isHttpMethod: Boolean = true) { HEAD, PATCH, OPTIONS, From 19e551a0582944f9431e22fe49ec43859dde4b6f Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Thu, 16 Jan 2025 23:20:45 +0100 Subject: [PATCH 3/3] Update routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt Co-authored-by: Itzdlg <15074893+Itzdlg@users.noreply.github.com> --- .../community/routing/annotations/AnnotatedRoutingTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt index 02fa542..aad1dc6 100644 --- a/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt +++ b/routing-annotations/routing-annotated/src/test/java/io/javalin/community/routing/annotations/AnnotatedRoutingTest.kt @@ -126,7 +126,7 @@ class AnnotatedRoutingTest { fun `should not register before and after handlers below the specified path`() { withinSharedScenario { client -> val beforeAtRootLevel = request("GET", "${client.origin}/").asEmpty() - assertThat(beforeAtRootLevel.headers.getFirst("after")).isEmpty() + assertThat(beforeAtRootLevel.headers.getFirst("before")).isEmpty() val afterAtRootLevel = request("GET", "${client.origin}/").asEmpty() assertThat(afterAtRootLevel.headers.getFirst("after")).isEmpty()