Skip to content

Commit

Permalink
Inject example data as seed data into the stub cache for virtual-service
Browse files Browse the repository at this point in the history
  • Loading branch information
yogeshnikam671 committed Nov 18, 2024
1 parent ef7f36e commit a1869a6
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 12 deletions.
18 changes: 14 additions & 4 deletions application/src/main/kotlin/application/VirtualServiceCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ import java.util.concurrent.CountDownLatch
)
class VirtualServiceCommand : Callable<Int> {

@Option(names = ["--host"], description = ["Host for the http stub"], defaultValue = DEFAULT_HTTP_STUB_HOST)
@Option(names = ["--host"], description = ["Host for the virtual service"], defaultValue = DEFAULT_HTTP_STUB_HOST)
lateinit var host: String

@Option(names = ["--port"], description = ["Port for the http stub"], defaultValue = DEFAULT_HTTP_STUB_PORT)
@Option(names = ["--port"], description = ["Port for the virtual service"], defaultValue = DEFAULT_HTTP_STUB_PORT)
var port: Int = 0

@Option(names = ["--examples"], description = ["Directories containing JSON examples"], required = false)
var exampleDirs: List<String> = mutableListOf()

private val stubLoaderEngine = StubLoaderEngine()
private var server: StatefulHttpStub? = null
private val latch = CountDownLatch(1)
private val newLine = System.lineSeparator()

override fun call(): Int {
setup()
Expand Down Expand Up @@ -82,7 +86,7 @@ class VirtualServiceCommand : Callable<Int> {
private fun startServer() {
val stubData: List<Pair<Feature, List<ScenarioStub>>> = stubLoaderEngine.loadStubs(
stubContractPathData(),
emptyList(), // TODO - to be replaced with exampleDirs
exampleDirs,
Configuration.configFilePath,
false
)
Expand All @@ -91,11 +95,17 @@ class VirtualServiceCommand : Callable<Int> {
host,
port,
stubData.map { it.first },
Configuration.configFilePath
Configuration.configFilePath,
stubData.flatMap { it.second }.also { it.logExamplesCachedAsSeedData() }
)
logger.log("Virtual service started on http://$host:$port")
latch.await()
}

private fun List<ScenarioStub>.logExamplesCachedAsSeedData() {
logger.log("${newLine}Injecting the data read from the following stub files into the stub server's state..".prependIndent(" "))
this.forEach { logger.log(it.filePath.orEmpty().prependIndent(" ")) }
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.specmatic.core.utilities.exceptionCauseMessage
import io.specmatic.core.value.JSONArrayValue
import io.specmatic.core.value.JSONObjectValue
import io.specmatic.core.value.Value
import io.specmatic.mock.ScenarioStub
import io.specmatic.stub.ContractStub
import io.specmatic.stub.CouldNotParseRequest
import io.specmatic.stub.FoundStubbedResponse
Expand All @@ -49,7 +50,8 @@ class StatefulHttpStub(
port: Int = 9000,
private val features: List<Feature>,
private val specmaticConfigPath: String? = null,
private val timeoutMillis: Long = 2000
private val scenarioStubs: List<ScenarioStub> = emptyList(),
private val timeoutMillis: Long = 2000,
): ContractStub {

private val environment = applicationEngineEnvironment {
Expand Down Expand Up @@ -140,7 +142,7 @@ class StatefulHttpStub(
}

private val specmaticConfig = loadSpecmaticConfig()
private val stubCache = StubCache()
private val stubCache = stubCacheWithExampleData()

private fun cachedHttpResponse(
httpRequest: HttpRequest,
Expand Down Expand Up @@ -181,7 +183,7 @@ class StatefulHttpStub(
val pathSegments = httpRequest.pathSegments()
if(isUnsupportedResponseBodyForCaching(generatedResponse, method, pathSegments)) return null

val (resourcePath, resourceId) = resourcePathAndIdFrom(pathSegments)
val (resourcePath, resourceId) = resourcePathAndIdFrom(httpRequest)
val resourceIdKey = resourceIdKeyFrom(scenario?.httpRequestPattern)
val attributeSelectionKeys: Set<String> =
scenario?.getFieldsToBeMadeMandatoryBasedOnAttributeSelection(httpRequest.queryParams).orEmpty()
Expand Down Expand Up @@ -233,7 +235,8 @@ class StatefulHttpStub(
return null
}

private fun resourcePathAndIdFrom(pathSegments: List<String>): Pair<String, String> {
private fun resourcePathAndIdFrom(httpRequest: HttpRequest): Pair<String, String> {
val pathSegments = httpRequest.pathSegments()
val resourcePath = "/${pathSegments.first()}"
val resourceId = pathSegments.last()
return Pair(resourcePath, resourceId)
Expand Down Expand Up @@ -376,4 +379,35 @@ class StatefulHttpStub(
else
SpecmaticConfig()
}

private fun stubCacheWithExampleData(): StubCache {
val stubCache = StubCache()

scenarioStubs.forEach {
val httpRequest = it.request
if (httpRequest.method !in setOf("GET", "POST")) return@forEach
if (isUnsupportedResponseBodyForCaching(
generatedResponse = it.response,
method = httpRequest.method,
pathSegments = httpRequest.pathSegments()
)
) return@forEach

val (resourcePath, _) = resourcePathAndIdFrom(httpRequest)
val responseBody = it.response.body
if (httpRequest.method == "GET" && httpRequest.pathSegments().size == 1) {
val responseBodies = (it.response.body as JSONArrayValue).list.filterIsInstance<JSONObjectValue>()
responseBodies.forEach { body ->
stubCache.addResponse(resourcePath, body)
}
} else {
if (responseBody !is JSONObjectValue) return@forEach
if(httpRequest.method == "POST" && httpRequest.body !is JSONObjectValue) return@forEach

stubCache.addResponse(resourcePath, responseBody)
}
}

return stubCache
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package io.specmatic.stub.stateful
import io.specmatic.conversions.OpenApiSpecification
import io.specmatic.core.*
import io.specmatic.core.pattern.parsedJSONObject
import io.specmatic.core.utilities.ContractPathData
import io.specmatic.core.value.*
import io.specmatic.stub.ContractStub
import io.specmatic.stub.HttpStub
import io.specmatic.stub.loadContractStubsFromImplicitPaths
import io.specmatic.stub.stateful.StatefulHttpStubTest.Companion
import io.specmatic.stub.stateful.StatefulHttpStubTest.Companion.resourceId
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
Expand Down Expand Up @@ -163,9 +166,6 @@ class StatefulHttpStubTest {
assertThat(getResponse.status).isEqualTo(404)
}

private fun JSONObjectValue.getStringValue(key: String): String? {
return this.jsonObject[key]?.toStringLiteral()
}
}

@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
Expand Down Expand Up @@ -321,3 +321,86 @@ class StatefulHttpStubWithAttributeSelectionTest {
return this.jsonObject[key]?.toStringLiteral()
}
}

class StatefulHttpStubSeedDataFromExamplesTest {
companion object {
private lateinit var httpStub: ContractStub
private const val SPEC_DIR_PATH = "src/test/resources/openapi/spec_with_strictly_restful_apis"

@JvmStatic
@BeforeAll
fun setup() {
val specPath = "$SPEC_DIR_PATH/spec_with_strictly_restful_apis.yaml"

val scenarioStubs = loadContractStubsFromImplicitPaths(
contractPathDataList = listOf(ContractPathData("", specPath)),
specmaticConfig = loadSpecmaticConfig("$SPEC_DIR_PATH/specmatic.yaml")
).flatMap { it.second }

httpStub = StatefulHttpStub(
specmaticConfigPath = "$SPEC_DIR_PATH/specmatic.yaml",
features = listOf(
OpenApiSpecification.fromFile(specPath).toFeature()
),
scenarioStubs = scenarioStubs
)
}

@JvmStatic
@AfterAll
fun tearDown() {
httpStub.close()
}
}

@Test
fun `should get the list of products from seed data loaded from examples`() {
val response = httpStub.client.execute(
HttpRequest(
method = "GET",
path = "/products"
)
)

assertThat(response.status).isEqualTo(200)
assertThat(response.body).isInstanceOf(JSONArrayValue::class.java)

val responseBody = (response.body as JSONArrayValue)
assertThat(responseBody.list.size).isEqualTo(4)

val responseObjectFromResponseBody = (response.body as JSONArrayValue)
.list.filterIsInstance<JSONObjectValue>().first { it.getStringValue("id") == "300" }

assertThat(responseObjectFromResponseBody.getStringValue("id")).isEqualTo("300")
assertThat(responseObjectFromResponseBody.getStringValue("name")).isEqualTo("iPhone 16")
assertThat(responseObjectFromResponseBody.getStringValue("description")).isEqualTo("New iPhone 16")
assertThat(responseObjectFromResponseBody.getStringValue("price")).isEqualTo("942")
assertThat(responseObjectFromResponseBody.getStringValue("inStock")).isEqualTo("true")
}


@Test
fun `should get the product from seed data loaded from examples`() {
val response = httpStub.client.execute(
HttpRequest(
method = "GET",
path = "/products/300"
)
)

assertThat(response.status).isEqualTo(200)
val responseBody = response.body as JSONObjectValue

assertThat(responseBody.getStringValue("id")).isEqualTo("300")
assertThat(responseBody.getStringValue("name")).isEqualTo("iPhone 16")
assertThat(responseBody.getStringValue("description")).isEqualTo("New iPhone 16")
assertThat(responseBody.getStringValue("price")).isEqualTo("942")
assertThat(responseBody.getStringValue("inStock")).isEqualTo("true")
}


}

private fun JSONObjectValue.getStringValue(key: String): String? {
return this.jsonObject[key]?.toStringLiteral()
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ components:
inStock:
type: boolean
example: true
required:
- id

ProductInput:
type: object
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"http-request": {
"path": "/products/100",
"method": "DELETE"
},
"http-response": {
"status": 204,
"status-text": "No Content"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"http-request": {
"path": "/products",
"method": "GET"
},
"http-response": {
"status": 200,
"body": [
{
"id": 100,
"name": "iPhone",
"description": "Description for an iPhone",
"price": 636,
"inStock": true
},
{
"id": 200,
"name": "iPhone Pro Max",
"description": "Description for an iPhone Pro Max",
"price": 866,
"inStock": false
}
],
"status-text": "OK",
"headers": {
"Content-Type": "application/json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"http-request": {
"path": "/products/400",
"method": "GET"
},
"http-response": {
"status": 200,
"body": {
"id": 400,
"name": "Macbook Pro",
"description": "Description for a Macbook Pro",
"price": 1000,
"inStock": true
},
"status-text": "OK",
"headers": {
"Content-Type": "application/json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"http-request": {
"path": "/products/100",
"method": "PATCH",
"headers": {
"Content-Type": "application/json"
},
"body": {
"description": "Patched description for iPhone",
"price": 300,
"inStock": false
}
},
"http-response": {
"status": 200,
"body": {
"id": 100,
"name": "iPhone",
"description": "Patched description for iPhone",
"price": 300,
"inStock": false
},
"status-text": "OK",
"headers": {
"Content-Type": "application/json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"http-request": {
"path": "/products",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"name": "iPhone 16",
"description": "New iPhone 16",
"price": 942,
"inStock": true
}
},
"http-response": {
"status": 201,
"body": {
"id": 300,
"name": "iPhone 16",
"description": "New iPhone 16",
"price": 942,
"inStock": true
},
"status-text": "Created",
"headers": {
"Content-Type": "application/json"
}
}
}

0 comments on commit a1869a6

Please sign in to comment.