Skip to content

Commit

Permalink
refactor: switches response file serving to use Vert.x filesystem.
Browse files Browse the repository at this point in the history
This unifies response file reading across adapters, and provides support for response files on the classpath.
  • Loading branch information
outofcoffee committed Oct 2, 2023
1 parent 58d82f8 commit 81a6ec5
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ class HandlerTest : AbstractHandlerTest() {
assertThat(responseEvent.body, containsString(".example"))
}

@ParameterizedTest
@Event(value = "simple/requests_v1/request_nonexistent_static_asset.json", type = APIGatewayProxyRequestEvent::class)
fun `should receive 404 for nonexistent file`(event: APIGatewayProxyRequestEvent) {
val responseEvent = handler!!.handleRequest(event, context!!)

assertNotNull(responseEvent, "Response event should be returned")
assertEquals(404, responseEvent.statusCode)
}

@ParameterizedTest
@Event(value = "simple/requests_v1/request_static_index.json", type = APIGatewayProxyRequestEvent::class)
fun `should load static index file`(event: APIGatewayProxyRequestEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ class HandlerV2Test : AbstractHandlerTest() {
assertThat(responseEvent.body, containsString(".example"))
}

@ParameterizedTest
@Event(value = "simple/requests_v2/request_nonexistent_static_asset.json", type = APIGatewayV2HTTPEvent::class)
fun `should receive 404 for nonexistent file`(event: APIGatewayV2HTTPEvent) {
val responseEvent = handler!!.handleRequest(event, context!!)

assertNotNull(responseEvent, "Response event should be returned")
assertEquals(404, responseEvent.statusCode)
}

@ParameterizedTest
@Event(value = "simple/requests_v2/request_static_index.json", type = APIGatewayV2HTTPEvent::class)
fun `should load static index file`(event: APIGatewayV2HTTPEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"path": "/assets/nonexistent",
"httpMethod": "GET",
"headers": {},
"queryStringParameters": {},
"pathParameters": {},
"body": null,
"isBase64Encoded": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"headers": {},
"queryStringParameters": {},
"pathParameters": {},
"body": null,
"isBase64Encoded": false,
"requestContext": {
"http": {
"path": "/assets/nonexistent",
"method": "GET"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,23 @@ import io.gatehill.imposter.script.ResponseBehaviour
import io.gatehill.imposter.util.LogUtil
import io.gatehill.imposter.util.MetricsUtil
import io.micrometer.core.instrument.Gauge
import io.vertx.core.Vertx
import io.vertx.core.buffer.Buffer
import io.vertx.core.json.JsonArray
import org.apache.logging.log4j.LogManager
import java.io.IOException
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.Paths
import javax.inject.Inject
import kotlin.io.path.exists
import kotlin.io.path.readBytes

/**
* @author Pete Cornish
*/
class ResponseFileServiceImpl @Inject constructor(
private val responseService: ResponseService,
private val vertx: Vertx,
) : ResponseFileService {

/**
Expand All @@ -75,7 +77,7 @@ class ResponseFileServiceImpl @Inject constructor(
*/
private val responseFileCache = CacheBuilder.newBuilder()
.maximumSize(getEnv(ENV_RESPONSE_FILE_CACHE_ENTRIES)?.toLong() ?: DEFAULT_RESPONSE_FILE_CACHE_ENTRIES)
.build<Path, Buffer>()
.build<String, Buffer>()

init {
MetricsUtil.doIfMetricsEnabled(
Expand All @@ -102,29 +104,46 @@ class ResponseFileServiceImpl @Inject constructor(
)

val responseFile = responseBehaviour.responseFile ?: throw IllegalStateException("Response file not set")
val normalisedPath = normalisePath(pluginConfig, responseFile)
val fsPath = resolvePath(pluginConfig, responseFile)

val responseData = responseFileCache.getIfPresent(normalisedPath) ?: run {
if (normalisedPath.exists()) {
Buffer.buffer(normalisedPath.readBytes()).also {
responseFileCache.put(normalisedPath, it)
val responseData = responseFileCache.getIfPresent(fsPath) ?: run {
try {
val buf = vertx.fileSystem().readFileBlocking(fsPath)
buf.also { responseFileCache.put(fsPath, it) }
} catch (e: Exception) {
if (e.cause is NoSuchFileException) {
responseService.failWithNotFoundResponse(httpExchange, "Response file does not exist: $fsPath")
} else {
httpExchange.fail(RuntimeException("Failed to read response file: $fsPath", e))
}
} else {
responseService.failWithNotFoundResponse(httpExchange, "Response file does not exist: $normalisedPath")
return
}
}

val filename = fsPath.substringAfterLast("/")

responseService.writeResponseData(
resourceConfig = resourceConfig,
httpExchange = httpExchange,
filenameHintForContentType = normalisedPath.fileName.toString(),
filenameHintForContentType = filename,
origResponseData = responseData,
template = responseBehaviour.isTemplate,
trustedData = false
)
}

private fun resolvePath(pluginConfig: PluginConfig, responseFile: String): String {
val normalisedPath = normalisePath(pluginConfig, responseFile)

val fsPath = if (normalisedPath.exists()) {
normalisedPath.toString()
} else {
// fall back to relative path, in case its in the working directory or on the classpath
"${pluginConfig.dir}/$responseFile"
}
return fsPath
}

private fun normalisePath(config: PluginConfig, responseFile: String): Path {
return Paths.get(config.dir.absolutePath, responseFile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ import io.gatehill.imposter.http.HttpResponse
import io.gatehill.imposter.plugin.config.PluginConfigImpl
import io.gatehill.imposter.plugin.config.resource.RestResourceConfig
import io.gatehill.imposter.script.ReadWriteResponseBehaviourImpl
import io.vertx.core.Vertx
import io.vertx.core.buffer.Buffer
import io.vertx.core.file.FileSystem
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.kotlin.any
Expand All @@ -73,7 +75,17 @@ class ResponseFileServiceImplTest {
assertEquals("Hello, world!", buffer.toString())
}
}
val service = ResponseFileServiceImpl(responseService)

val fileSystem = mock<FileSystem> {
on { readFileBlocking(any()) } doAnswer {
val path = it.arguments[0] as String
Buffer.buffer(File(path).readBytes())
}
}
val vertx = mock<Vertx> {
on { fileSystem() } doReturn fileSystem
}
val service = ResponseFileServiceImpl(responseService, vertx)

val pluginConfig = PluginConfigImpl().apply {
dir = File(ResponseFileServiceImplTest::class.java.getResource("/response-file.txt")!!.toURI()).parentFile
Expand Down Expand Up @@ -108,7 +120,7 @@ class ResponseFileServiceImplTest {

@Test
fun `should load file as JSON array`() {
val service = ResponseFileServiceImpl(mock())
val service = ResponseFileServiceImpl(mock(), mock())

val jsonFile = File(ResponseFileServiceImplTest::class.java.getResource("/test-array.json")!!.toURI())
val pluginConfig = PluginConfigImpl().apply {
Expand Down

0 comments on commit 81a6ec5

Please sign in to comment.