From aa3ec93a80b1e85c35655136a8f39a78f2c6a230 Mon Sep 17 00:00:00 2001 From: LeonardoBai12 <69223422+LeonardoBai12@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:38:24 -0300 Subject: [PATCH 1/2] Add runtime auth via X-Mapped-Auth header for mapped routes Allow clients to authenticate outbound API calls at request time using the X-Mapped-Auth header instead of storing credentials in the route. When a route has no pre-configured authHeader, the middleware applies X-Mapped-Auth (Bearer/Basic/raw) for the validation call during route creation and for every subsequent request to that route. Stored routes never persist the runtime token. Pre-configured authHeader always takes priority over X-Mapped-Auth. Co-Authored-By: Claude Sonnet 4.6 --- .../data/request/MIddlewareAuthHeader.kt | 13 ++ .../lb/common/data/service/ServerService.kt | 6 +- documentation/MappingRequest.md | 26 ++- .../ktor/server/service/ServerServiceImpl.kt | 17 +- .../request/MiddlewareAuthHeaderTest.kt | 53 +++++ .../server/service/ServerServiceImplTest.kt | 185 +++++++++++++++++- .../data/datasource/MiddlewareDataSource.kt | 7 +- .../repository/MiddlewareRepositoryImpl.kt | 18 +- .../datasource/MiddlewareDataSourceTest.kt | 4 +- .../MiddlewareRepositoryimplTest.kt | 114 +++++++++++ 10 files changed, 423 insertions(+), 20 deletions(-) create mode 100644 impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/request/MiddlewareAuthHeaderTest.kt diff --git a/common/data/src/main/java/io/lb/common/data/request/MIddlewareAuthHeader.kt b/common/data/src/main/java/io/lb/common/data/request/MIddlewareAuthHeader.kt index ad2d097..0165f08 100644 --- a/common/data/src/main/java/io/lb/common/data/request/MIddlewareAuthHeader.kt +++ b/common/data/src/main/java/io/lb/common/data/request/MIddlewareAuthHeader.kt @@ -31,6 +31,19 @@ data class MiddlewareAuthHeader( return MiddlewareAuthHeader(type, token) } + + fun fromString(value: String): MiddlewareAuthHeader { + val parts = value.split(" ", limit = 2) + if (parts.size == 2) { + val type = MiddlewareAuthHeaderType.entries.firstOrNull { + it != MiddlewareAuthHeaderType.None && it.name.equals(parts[0], ignoreCase = true) + } + if (type != null) { + return MiddlewareAuthHeader(type = type, token = parts[1]) + } + } + return MiddlewareAuthHeader(type = MiddlewareAuthHeaderType.None, token = value) + } } } diff --git a/common/data/src/main/java/io/lb/common/data/service/ServerService.kt b/common/data/src/main/java/io/lb/common/data/service/ServerService.kt index 3a2db81..121322c 100644 --- a/common/data/src/main/java/io/lb/common/data/service/ServerService.kt +++ b/common/data/src/main/java/io/lb/common/data/service/ServerService.kt @@ -2,6 +2,7 @@ package io.lb.common.data.service import io.lb.common.data.model.MappedResponse import io.lb.common.data.model.MappedRoute +import io.lb.common.data.request.MiddlewareAuthHeader import io.lb.common.shared.error.MiddlewareException /** @@ -10,9 +11,10 @@ import io.lb.common.shared.error.MiddlewareException interface ServerService { /** * Starts the generic mapping route. - * @param onReceive The handler for the generic mapping route. It receives the mapped route and returns the URL. + * @param onReceive The handler for the generic mapping route. It receives the mapped route, + * an optional runtime auth header (from X-Mapped-Auth, not stored), and returns the URL. */ - fun startGenericMappingRoute(onReceive: suspend (MappedRoute) -> String) + fun startGenericMappingRoute(onReceive: suspend (MappedRoute, MiddlewareAuthHeader?) -> String) /** * Starts the query all routes route. diff --git a/documentation/MappingRequest.md b/documentation/MappingRequest.md index d72f569..2c4a3c2 100644 --- a/documentation/MappingRequest.md +++ b/documentation/MappingRequest.md @@ -16,7 +16,11 @@ You can also include the following optional fields in the root of your mapping r ### External API Authentication: -To authenticate calls to the original external API, include an `authHeader` field inside `originalRoute`. This is independent from the Middleware's own authentication (handled via the `Authorization` header when calling the Middleware). +There are two ways to authenticate calls to the original external API. Both are independent from the Middleware's own authentication (handled via the `Authorization` header when calling the Middleware). + +#### Option 1 — Pre-configured auth (stored with the route) + +Include an `authHeader` field inside `originalRoute` when creating the route. The token is stored and automatically used on every request to that mapped route. Supported auth types: - **`None`** — No authentication header is sent to the external API. @@ -37,7 +41,25 @@ Supported auth types: } ``` -> **Note:** The Middleware's own `Authorization` header (used by clients to authenticate with the Middleware) is never forwarded to the external API. Only `authHeader` inside `originalRoute` controls external API authentication. +#### Option 2 — Runtime auth via `X-Mapped-Auth` header (not stored) + +If you do not want to store the auth token in the route, omit `authHeader` from `originalRoute` and pass the `X-Mapped-Auth` header instead. This is useful when tokens are short-lived or user-specific. + +The header value should be the full authorization string (same format as an `Authorization` header): + +``` +X-Mapped-Auth: Bearer your-api-token-here +X-Mapped-Auth: Basic dXNlcjpwYXNz +X-Mapped-Auth: raw-token +``` + +**When creating a route (`POST /v1/mapping`):** If `X-Mapped-Auth` is present and the route has no `authHeader`, the middleware uses it to validate the route against the external API before saving. The token is **not** stored — every subsequent call to that route must include `X-Mapped-Auth`. + +**When calling a mapped route (`/v1/{uuid}/path`):** If the route has no pre-configured `authHeader`, the middleware uses `X-Mapped-Auth` from the incoming request to authenticate the outbound call to the external API. + +**Priority:** Pre-configured `authHeader` always takes precedence over `X-Mapped-Auth`. If both are present, `X-Mapped-Auth` is ignored. + +> **Note:** The Middleware's own `Authorization` header (used by clients to authenticate with the Middleware) is never forwarded to the external API. `X-Mapped-Auth` is also never forwarded as-is — it is only used internally to set the outbound `Authorization` header. ### Example Additional Fields: diff --git a/impl/ktor-server/src/main/kotlin/io/lb/impl/ktor/server/service/ServerServiceImpl.kt b/impl/ktor-server/src/main/kotlin/io/lb/impl/ktor/server/service/ServerServiceImpl.kt index 0f07be0..2f6d773 100644 --- a/impl/ktor-server/src/main/kotlin/io/lb/impl/ktor/server/service/ServerServiceImpl.kt +++ b/impl/ktor-server/src/main/kotlin/io/lb/impl/ktor/server/service/ServerServiceImpl.kt @@ -22,6 +22,7 @@ import io.ktor.util.pipeline.PipelineContext import io.ktor.util.toMap import io.lb.common.data.model.MappedResponse import io.lb.common.data.model.MappedRoute +import io.lb.common.data.request.MiddlewareAuthHeader import io.lb.common.data.request.MiddlewareHttpMethods import io.lb.common.data.service.ServerService import io.lb.common.shared.error.MiddlewareException @@ -37,7 +38,10 @@ import kotlinx.serialization.json.JsonObject internal class ServerServiceImpl( private val engine: NettyApplicationEngine ) : ServerService { - override fun startGenericMappingRoute(onReceive: suspend (MappedRoute) -> String) { + companion object { + const val MAPPED_AUTH_HEADER = "X-Mapped-Auth" + } + override fun startGenericMappingRoute(onReceive: suspend (MappedRoute, MiddlewareAuthHeader?) -> String) { engine.application.routing { authenticate { post("v1/mapping") { @@ -45,8 +49,10 @@ internal class ServerServiceImpl( call.respond(HttpStatusCode.BadRequest) return@post } + val runtimeAuthHeader = call.request.headers[MAPPED_AUTH_HEADER] + ?.let { MiddlewareAuthHeader.fromString(it) } val mappedRouteUrl = runCatching { - onReceive(parameter.toMappedRoute()) + onReceive(parameter.toMappedRoute(), runtimeAuthHeader) }.getOrElse { error -> when (error) { is IllegalArgumentException -> call.respond( @@ -188,9 +194,11 @@ internal class ServerServiceImpl( val queries = call.request.queryParameters.toMap().mapValues { query -> query.value.first() } + val runtimeAuthHeader = call.request.headers[MAPPED_AUTH_HEADER] + ?.let { MiddlewareAuthHeader.fromString(it) } val headers = call.request.headers.toMap() .mapValues { it.value.first() } - .filterKeys { it != HttpHeaders.Authorization } + .filterKeys { it != HttpHeaders.Authorization && it != MAPPED_AUTH_HEADER } val body = try { call.receiveNullable() } catch (e: Exception) { @@ -199,7 +207,8 @@ internal class ServerServiceImpl( mappedRoute.originalRoute = mappedRoute.originalRoute.copy( queries = queries, headers = headers, - body = body + body = body, + authHeader = mappedRoute.originalRoute.authHeader ?: runtimeAuthHeader ) val response = onRequest(mappedRoute) diff --git a/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/request/MiddlewareAuthHeaderTest.kt b/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/request/MiddlewareAuthHeaderTest.kt new file mode 100644 index 0000000..6ddf2bd --- /dev/null +++ b/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/request/MiddlewareAuthHeaderTest.kt @@ -0,0 +1,53 @@ +package io.lb.impl.ktor.server.request + +import io.lb.common.data.request.MiddlewareAuthHeader +import io.lb.common.data.request.MiddlewareAuthHeaderType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class MiddlewareAuthHeaderTest { + @Test + fun `fromString with Bearer prefix returns Bearer type and stripped token`() { + val result = MiddlewareAuthHeader.fromString("Bearer mytoken123") + assertEquals(MiddlewareAuthHeaderType.Bearer, result.type) + assertEquals("mytoken123", result.token) + assertEquals("Bearer mytoken123", result.fullToken()) + } + + @Test + fun `fromString with Basic prefix returns Basic type and stripped token`() { + val result = MiddlewareAuthHeader.fromString("Basic dXNlcjpwYXNz") + assertEquals(MiddlewareAuthHeaderType.Basic, result.type) + assertEquals("dXNlcjpwYXNz", result.token) + assertEquals("Basic dXNlcjpwYXNz", result.fullToken()) + } + + @Test + fun `fromString with lowercase bearer prefix returns Bearer type`() { + val result = MiddlewareAuthHeader.fromString("bearer mytoken") + assertEquals(MiddlewareAuthHeaderType.Bearer, result.type) + assertEquals("mytoken", result.token) + } + + @Test + fun `fromString with raw token and no prefix returns None type`() { + val result = MiddlewareAuthHeader.fromString("rawtoken") + assertEquals(MiddlewareAuthHeaderType.None, result.type) + assertEquals("rawtoken", result.token) + assertEquals("rawtoken", result.fullToken()) + } + + @Test + fun `fromString with unknown prefix returns None type with full value as token`() { + val result = MiddlewareAuthHeader.fromString("Digest somecredentials") + assertEquals(MiddlewareAuthHeaderType.None, result.type) + assertEquals("Digest somecredentials", result.token) + } + + @Test + fun `fromString with Bearer and multi-word token preserves full token`() { + val result = MiddlewareAuthHeader.fromString("Bearer a.b.c") + assertEquals(MiddlewareAuthHeaderType.Bearer, result.type) + assertEquals("a.b.c", result.token) + } +} diff --git a/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/service/ServerServiceImplTest.kt b/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/service/ServerServiceImplTest.kt index 8fd99cd..c006413 100644 --- a/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/service/ServerServiceImplTest.kt +++ b/impl/ktor-server/src/test/kotlin/io/lb/impl/ktor/server/service/ServerServiceImplTest.kt @@ -140,7 +140,7 @@ class ServerServiceImplTest { ) = setupService(method) serverService.createMappedRoute(testMappedRoute, onRequestMock) serverService.startQueryAllRoutesRoute { emptyList() } - serverService.startGenericMappingRoute { "Received" } + serverService.startGenericMappingRoute { _, _ -> "Received" } serverService.startPreviewRoute { _, _ -> "Received" } } block() @@ -248,6 +248,119 @@ class ServerServiceImplTest { return Pair(testMappedRoute, onRequestMock) } + @Test + fun `When route has no auth and X-Mapped-Auth Bearer is provided, expect auth applied to route`() { + var capturedAuthHeader: io.lb.common.data.request.MiddlewareAuthHeader? = null + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.createMappedRoute(createTestMappedRouteWithoutAuth(MiddlewareHttpMethods.Get)) { + capturedAuthHeader = it.originalRoute.authHeader + MappedResponse(statusCode = 200, body = "{}") + } + } + client.get("v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path") { + setupRequest() + header(ServerServiceImpl.MAPPED_AUTH_HEADER, "Bearer mytoken123") + } + assertEquals(io.lb.common.data.request.MiddlewareAuthHeaderType.Bearer, capturedAuthHeader?.type) + assertEquals("mytoken123", capturedAuthHeader?.token) + } + } + + @Test + fun `When route has no auth and X-Mapped-Auth Basic is provided, expect auth applied to route`() { + var capturedAuthHeader: io.lb.common.data.request.MiddlewareAuthHeader? = null + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.createMappedRoute(createTestMappedRouteWithoutAuth(MiddlewareHttpMethods.Get)) { + capturedAuthHeader = it.originalRoute.authHeader + MappedResponse(statusCode = 200, body = "{}") + } + } + client.get("v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path") { + setupRequest() + header(ServerServiceImpl.MAPPED_AUTH_HEADER, "Basic dXNlcjpwYXNz") + } + assertEquals(io.lb.common.data.request.MiddlewareAuthHeaderType.Basic, capturedAuthHeader?.type) + assertEquals("dXNlcjpwYXNz", capturedAuthHeader?.token) + } + } + + @Test + fun `When route has pre-configured auth and X-Mapped-Auth is provided, expect pre-configured auth used`() { + var capturedAuthHeader: io.lb.common.data.request.MiddlewareAuthHeader? = null + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.createMappedRoute(createTestMappedRoute(MiddlewareHttpMethods.Get)) { + capturedAuthHeader = it.originalRoute.authHeader + MappedResponse(statusCode = 200, body = "{}") + } + } + client.get("v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path") { + setupRequest() + header(ServerServiceImpl.MAPPED_AUTH_HEADER, "Bearer runtimetoken") + } + assertEquals(MiddlewareAuthHeaderType.None, capturedAuthHeader?.type) + assertEquals("", capturedAuthHeader?.token) + } + } + + @Test + fun `When X-Mapped-Auth header is provided, expect it not forwarded in route headers`() { + val capturedHeaders = mutableMapOf() + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.createMappedRoute(createTestMappedRouteWithoutAuth(MiddlewareHttpMethods.Get)) { + capturedHeaders.putAll(it.originalRoute.headers) + MappedResponse(statusCode = 200, body = "{}") + } + } + client.get("v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path") { + setupRequest() + header(ServerServiceImpl.MAPPED_AUTH_HEADER, "Bearer mytoken") + } + assertFalse(capturedHeaders.containsKey(ServerServiceImpl.MAPPED_AUTH_HEADER)) + } + } + + @Test + fun `When route has no auth and no X-Mapped-Auth is provided, expect null auth on route`() { + var capturedAuthHeader: io.lb.common.data.request.MiddlewareAuthHeader? = + MiddlewareAuthHeader(MiddlewareAuthHeaderType.Bearer, "sentinel") + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.createMappedRoute(createTestMappedRouteWithoutAuth(MiddlewareHttpMethods.Get)) { + capturedAuthHeader = it.originalRoute.authHeader + MappedResponse(statusCode = 200, body = "{}") + } + } + client.get("v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path") { + setupRequest() + } + assertEquals(null, capturedAuthHeader) + } + } + @Test fun `When request has no auth, expect unauthorized`() = serverServiceTestApplication(MiddlewareHttpMethods.Get) { @@ -313,7 +426,7 @@ class ServerServiceImplTest { val engine: NettyApplicationEngine = mockk(relaxed = true) every { engine.application } returns this serverService = ServerServiceImpl(engine) - serverService.startGenericMappingRoute { + serverService.startGenericMappingRoute { _, _ -> throw MiddlewareException(MiddlewareStatusCode.CONFLICT, "Route already exists") } } @@ -333,7 +446,7 @@ class ServerServiceImplTest { val engine: NettyApplicationEngine = mockk(relaxed = true) every { engine.application } returns this serverService = ServerServiceImpl(engine) - serverService.startGenericMappingRoute { + serverService.startGenericMappingRoute { _, _ -> throw IllegalArgumentException("Invalid route configuration") } } @@ -364,6 +477,53 @@ class ServerServiceImplTest { } } + @Test + fun `When mapping route creation receives X-Mapped-Auth, expect runtime auth passed to handler`() { + var capturedRuntimeAuth: io.lb.common.data.request.MiddlewareAuthHeader? = null + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.startGenericMappingRoute { _, runtimeAuth -> + capturedRuntimeAuth = runtimeAuth + "v1/test-path" + } + } + client.post("v1/mapping") { + setupRequest() + header(ServerServiceImpl.MAPPED_AUTH_HEADER, "Bearer creation-token") + setBody(Json.encodeToString(createMappedRoute())) + } + assertEquals(io.lb.common.data.request.MiddlewareAuthHeaderType.Bearer, capturedRuntimeAuth?.type) + assertEquals("creation-token", capturedRuntimeAuth?.token) + } + } + + @Test + fun `When mapping route creation has no X-Mapped-Auth, expect null runtime auth passed to handler`() { + var capturedRuntimeAuth: io.lb.common.data.request.MiddlewareAuthHeader? = + MiddlewareAuthHeader(MiddlewareAuthHeaderType.Bearer, "sentinel") + testApplication { + setupApplication() + application { + val engine: NettyApplicationEngine = mockk(relaxed = true) + every { engine.application } returns this + serverService = ServerServiceImpl(engine) + serverService.startGenericMappingRoute { _, runtimeAuth -> + capturedRuntimeAuth = runtimeAuth + "v1/test-path" + } + } + client.post("v1/mapping") { + setupRequest() + setBody(Json.encodeToString(createMappedRoute())) + } + assertEquals(null, capturedRuntimeAuth) + } + } + private fun createPreviewRequest(): PreviewRequestBody { return PreviewRequestBody( originalResponse = Json.parseToJsonElement("{}").jsonObject, @@ -371,6 +531,25 @@ class ServerServiceImplTest { ) } + private fun createTestMappedRouteWithoutAuth(method: MiddlewareHttpMethods) = MappedRoute( + uuid = "b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b", + method = method, + path = "v1/b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b/test-path", + rulesAsString = "test-rules", + originalRoute = OriginalRoute( + path = "test-route", + method = method, + originalApi = OriginalApi("https://10.0.2.2:8885/"), + authHeader = null, + headers = mapOf("Content-Type" to "application/json"), + body = json.decodeFromString("{}"), + ), + mappedApi = MappedApi( + "a1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b", + originalApi = OriginalApi(baseUrl = "https://www.themealdb.com/api/") + ), + ) + private fun createTestMappedRoute(method: MiddlewareHttpMethods) = MappedRoute( uuid = "b1b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b", method = method, diff --git a/middleware/data/src/main/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSource.kt b/middleware/data/src/main/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSource.kt index 3049f6a..4410b14 100644 --- a/middleware/data/src/main/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSource.kt +++ b/middleware/data/src/main/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSource.kt @@ -5,6 +5,7 @@ import io.ktor.http.isSuccess import io.lb.common.data.model.MappedApi import io.lb.common.data.model.MappedResponse import io.lb.common.data.model.MappedRoute +import io.lb.common.data.request.MiddlewareAuthHeader import io.lb.common.data.service.ClientService import io.lb.common.data.service.DatabaseService import io.lb.common.data.service.MapperService @@ -32,7 +33,7 @@ internal class MiddlewareDataSource( * @throws MiddlewareException If an error occurs while configuring the routes. */ @Throws(MiddlewareException::class) - fun configGenericRoutes(onCreateMappedRoute: suspend (MappedRoute) -> MappedRoute) { + fun configGenericRoutes(onCreateMappedRoute: suspend (MappedRoute, MiddlewareAuthHeader?) -> MappedRoute) { serverService.startQueryAllRoutesRoute { kotlin.runCatching { val routes = databaseService.queryAllMappedRoutes() @@ -41,9 +42,9 @@ internal class MiddlewareDataSource( emptyList() } } - serverService.startGenericMappingRoute { mappedRoute -> + serverService.startGenericMappingRoute { mappedRoute, runtimeAuth -> val route = if (mapperService.validateMappingRules(mappedRoute.rulesAsString.orEmpty())) { - onCreateMappedRoute(mappedRoute) + onCreateMappedRoute(mappedRoute, runtimeAuth) } else { throw MiddlewareException( code = HttpStatusCode.BadRequest.value, diff --git a/middleware/data/src/main/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryImpl.kt b/middleware/data/src/main/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryImpl.kt index 6511bf8..5e604d2 100644 --- a/middleware/data/src/main/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryImpl.kt +++ b/middleware/data/src/main/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryImpl.kt @@ -3,6 +3,7 @@ package io.lb.middleware.data.repository import io.ktor.http.HttpStatusCode import io.ktor.http.isSuccess import io.lb.common.data.model.MappedRoute +import io.lb.common.data.request.MiddlewareAuthHeader import io.lb.common.shared.error.MiddlewareException import io.lb.common.shared.flow.Resource import io.lb.middleware.data.datasource.MiddlewareDataSource @@ -21,8 +22,8 @@ internal class MiddlewareRepositoryImpl( ) : MiddlewareRepository { override fun startMiddleware() = flow { try { - dataSource.configGenericRoutes { - createMappedRoute(it) + dataSource.configGenericRoutes { mappedRoute, runtimeAuth -> + createMappedRoute(mappedRoute, runtimeAuth) } dataSource.configStoredMappedRoutes() emit(Resource.Success(Unit)) @@ -32,7 +33,10 @@ internal class MiddlewareRepositoryImpl( } @VisibleForTesting - private suspend fun createMappedRoute(mappedRoute: MappedRoute): MappedRoute { + private suspend fun createMappedRoute( + mappedRoute: MappedRoute, + runtimeAuth: MiddlewareAuthHeader? = null + ): MappedRoute { val localApi = dataSource.queryMappedApi(mappedRoute.mappedApi.originalApi.baseUrl) ?: run { dataSource.createMappedApi(mappedRoute.mappedApi) mappedRoute.mappedApi @@ -50,7 +54,13 @@ internal class MiddlewareRepositoryImpl( ) } - dataSource.getMappedResponse(mappedRoute).takeIf { + val routeForValidation = if (runtimeAuth != null && mappedRoute.originalRoute.authHeader == null) { + mappedRoute.copy(originalRoute = mappedRoute.originalRoute.copy(authHeader = runtimeAuth)) + } else { + mappedRoute + } + + dataSource.getMappedResponse(routeForValidation).takeIf { HttpStatusCode.fromValue(it.statusCode).isSuccess().not() }?.let { throw MiddlewareException( diff --git a/middleware/data/src/test/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSourceTest.kt b/middleware/data/src/test/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSourceTest.kt index 73fa4e2..1bcb47d 100644 --- a/middleware/data/src/test/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSourceTest.kt +++ b/middleware/data/src/test/kotlin/io/lb/middleware/data/datasource/MiddlewareDataSourceTest.kt @@ -63,8 +63,8 @@ class MiddlewareDataSourceTest { every { serverService.startGenericMappingRoute(any()) } just Runs every { serverService.startPreviewRoute(any()) } just Runs - dataSource.configGenericRoutes { - it + dataSource.configGenericRoutes { route, _ -> + route } verify { serverService.startQueryAllRoutesRoute(any()) } diff --git a/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt b/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt index 914b988..1a96095 100644 --- a/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt +++ b/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt @@ -1,19 +1,30 @@ package io.lb.middleware.data.repository +import io.lb.common.data.model.MappedApi +import io.lb.common.data.model.MappedResponse +import io.lb.common.data.model.MappedRoute +import io.lb.common.data.model.OriginalApi +import io.lb.common.data.model.OriginalRoute +import io.lb.common.data.request.MiddlewareAuthHeader +import io.lb.common.data.request.MiddlewareAuthHeaderType +import io.lb.common.data.request.MiddlewareHttpMethods import io.lb.common.shared.error.MiddlewareException import io.lb.common.shared.flow.Resource import io.lb.middleware.data.datasource.MiddlewareDataSource import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.flow.single import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals +import kotlin.test.assertNull class MiddlewareRepositoryimplTest { private lateinit var dataSource: MiddlewareDataSource @@ -71,4 +82,107 @@ class MiddlewareRepositoryimplTest { coVerify { dataSource.stopMiddleware() } } + + @Test + fun `When createMappedRoute with runtimeAuth and no route authHeader, validation uses runtime auth`() = runTest { + val capturedLambda = slot MappedRoute>() + val capturedValidationRoute = slot() + + every { dataSource.configGenericRoutes(capture(capturedLambda)) } just Runs + coEvery { dataSource.getMappedResponse(capture(capturedValidationRoute)) } returns MappedResponse(200, "{}") + coEvery { dataSource.createMappedRoute(any()) } returns "new-uuid" + + repository.startMiddleware().collect {} + + val runtimeAuth = MiddlewareAuthHeader(MiddlewareAuthHeaderType.Bearer, "creation-token") + capturedLambda.captured.invoke(buildMappedRoute(authHeader = null), runtimeAuth) + + assertEquals(runtimeAuth, capturedValidationRoute.captured.originalRoute.authHeader) + } + + @Test + fun `When createMappedRoute with runtimeAuth, stored route does not contain runtime auth`() = runTest { + val capturedLambda = slot MappedRoute>() + val capturedStoredRoute = slot() + + every { dataSource.configGenericRoutes(capture(capturedLambda)) } just Runs + coEvery { dataSource.getMappedResponse(any()) } returns MappedResponse(200, "{}") + coEvery { dataSource.createMappedRoute(capture(capturedStoredRoute)) } returns "new-uuid" + + repository.startMiddleware().collect {} + + val runtimeAuth = MiddlewareAuthHeader(MiddlewareAuthHeaderType.Bearer, "creation-token") + capturedLambda.captured.invoke(buildMappedRoute(authHeader = null), runtimeAuth) + + assertNull(capturedStoredRoute.captured.originalRoute.authHeader) + } + + @Test + fun `When createMappedRoute with pre-configured auth and runtimeAuth, pre-configured auth used for validation`() = runTest { + val capturedLambda = slot MappedRoute>() + val capturedValidationRoute = slot() + + every { dataSource.configGenericRoutes(capture(capturedLambda)) } just Runs + coEvery { dataSource.getMappedResponse(capture(capturedValidationRoute)) } returns MappedResponse(200, "{}") + coEvery { dataSource.createMappedRoute(any()) } returns "new-uuid" + + repository.startMiddleware().collect {} + + val preConfiguredAuth = MiddlewareAuthHeader(MiddlewareAuthHeaderType.Basic, "stored-token") + val runtimeAuth = MiddlewareAuthHeader(MiddlewareAuthHeaderType.Bearer, "runtime-token") + capturedLambda.captured.invoke(buildMappedRoute(authHeader = preConfiguredAuth), runtimeAuth) + + assertEquals(preConfiguredAuth, capturedValidationRoute.captured.originalRoute.authHeader) + } + + @Test + fun `When createMappedRoute with no runtimeAuth and no authHeader, validation receives null auth`() = runTest { + val capturedLambda = slot MappedRoute>() + val capturedValidationRoute = slot() + + every { dataSource.configGenericRoutes(capture(capturedLambda)) } just Runs + coEvery { dataSource.getMappedResponse(capture(capturedValidationRoute)) } returns MappedResponse(200, "{}") + coEvery { dataSource.createMappedRoute(any()) } returns "new-uuid" + + repository.startMiddleware().collect {} + + capturedLambda.captured.invoke(buildMappedRoute(authHeader = null), null) + + assertNull(capturedValidationRoute.captured.originalRoute.authHeader) + } + + @Test + fun `When createMappedRoute succeeds, expect configureMappedRoute called with uuid path`() = runTest { + val capturedLambda = slot MappedRoute>() + val capturedConfiguredRoute = slot() + + every { dataSource.configGenericRoutes(capture(capturedLambda)) } just Runs + coEvery { dataSource.getMappedResponse(any()) } returns MappedResponse(200, "{}") + coEvery { dataSource.createMappedRoute(any()) } returns "generated-uuid" + coEvery { dataSource.configureMappedRoute(capture(capturedConfiguredRoute)) } just Runs + + repository.startMiddleware().collect {} + + val route = buildMappedRoute(authHeader = null) + capturedLambda.captured.invoke(route, null) + + coVerify { dataSource.configureMappedRoute(any()) } + assertEquals("v1/generated-uuid/${route.path}", capturedConfiguredRoute.captured.path) + } + + private fun buildMappedRoute(authHeader: MiddlewareAuthHeader?) = MappedRoute( + path = "test-path", + method = MiddlewareHttpMethods.Get, + mappedApi = MappedApi( + uuid = "api-uuid", + originalApi = OriginalApi(baseUrl = "https://example.com/") + ), + originalRoute = OriginalRoute( + path = "original-path", + method = MiddlewareHttpMethods.Get, + originalApi = OriginalApi(baseUrl = "https://example.com/"), + authHeader = authHeader + ), + rulesAsString = "{}" + ) } From b15e12557c0dbdc72851ec895cf3405242902917 Mon Sep 17 00:00:00 2001 From: LeonardoBai12 <69223422+LeonardoBai12@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:17:00 -0300 Subject: [PATCH 2/2] Fix MaximumLineLength detekt violation in MiddlewareRepositoryimplTest Co-Authored-By: Claude Sonnet 4.6 --- .../middleware/data/repository/MiddlewareRepositoryimplTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt b/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt index 1a96095..0d99314 100644 --- a/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt +++ b/middleware/data/src/test/kotlin/io/lb/middleware/data/repository/MiddlewareRepositoryimplTest.kt @@ -118,7 +118,7 @@ class MiddlewareRepositoryimplTest { } @Test - fun `When createMappedRoute with pre-configured auth and runtimeAuth, pre-configured auth used for validation`() = runTest { + fun `When createMappedRoute with pre-configured auth and runtimeAuth, pre-configured auth used`() = runTest { val capturedLambda = slot MappedRoute>() val capturedValidationRoute = slot()