Skip to content

Commit

Permalink
#1431 Changes for Open API Spec Servers Parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Samy authored and Samy committed Nov 21, 2024
1 parent 79fbdc4 commit 9a8f7e1
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 147 deletions.
4 changes: 3 additions & 1 deletion application/src/main/kotlin/application/HTTPStubEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class HTTPStubEngine {
workingDirectory: WorkingDirectory,
gracefulRestartTimeoutInMs: Long,
pathPrefix: String? = null,
serverDescription: String? = null
): HttpStub? {
val features = stubs.map { it.first }

Expand All @@ -45,7 +46,8 @@ class HTTPStubEngine {
workingDirectory = workingDirectory,
specmaticConfigPath = specmaticConfigPath,
timeoutMillis = gracefulRestartTimeoutInMs,
pathPrefix = pathPrefix
pathPrefix = pathPrefix,
serverDescription = serverDescription
).also {
consoleLog(NewLineLogMessage)
val protocol = if (keyStoreData != null) "https" else "http"
Expand Down
17 changes: 12 additions & 5 deletions application/src/main/kotlin/application/StubCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.specmatic.core.Configuration.Companion.DEFAULT_HTTP_STUB_PORT
import io.specmatic.core.log.*
import io.specmatic.core.utilities.ContractPathData
import io.specmatic.core.utilities.Flags.Companion.PATH_PREFIX
import io.specmatic.core.utilities.Flags.Companion.SERVER_DESCRIPTION
import io.specmatic.core.utilities.Flags.Companion.SPECMATIC_STUB_DELAY
import io.specmatic.core.utilities.exitIfAnyDoNotExist
import io.specmatic.core.utilities.throwExceptionIfDirectoriesAreInvalid
Expand Down Expand Up @@ -48,8 +49,8 @@ class StubCommand : Callable<Unit> {
@Option(names = ["--port"], description = ["Port for the http stub"], defaultValue = DEFAULT_HTTP_STUB_PORT)
var port: Int = 0

@Option(names = ["--pathPrefix"], description = ["Path prefix to actual url"], defaultValue = "")
lateinit var pathPrefix: String
@Option(names = ["--pathPrefix"], description = ["Path prefix to actual url"])
var pathPrefix: String? = null

@Option(names = ["--strict"], description = ["Start HTTP stub in strict mode"], required = false)
var strictMode: Boolean = false
Expand Down Expand Up @@ -108,10 +109,14 @@ class StubCommand : Callable<Unit> {
@Option(names = ["--delay-in-ms"], description = ["Stub response delay in milliseconds"])
var delayInMilliseconds: Long = 0

@Option(names = ["--description"], description = ["The server description to pickup the server url"])
var serverDescription: String? = null

@Option(
names = ["--graceful-restart-timeout-in-ms"],
description = ["Time to wait for the server to stop before starting it again"]
)

var gracefulRestartTimeoutInMs: Long = 1000

@Autowired
Expand All @@ -131,8 +136,9 @@ class StubCommand : Callable<Unit> {
if (delayInMilliseconds > 0) {
System.setProperty(SPECMATIC_STUB_DELAY, delayInMilliseconds.toString())
}
if (pathPrefix.isNotEmpty())
System.setProperty(PATH_PREFIX, pathPrefix)

pathPrefix?.let { System.setProperty(PATH_PREFIX, it) }
serverDescription?.let { System.setProperty(SERVER_DESCRIPTION, it) }

val logPrinters = configureLogPrinters()

Expand Down Expand Up @@ -225,7 +231,8 @@ class StubCommand : Callable<Unit> {
httpClientFactory,
workingDirectory,
gracefulRestartTimeoutInMs,
pathPrefix
pathPrefix,
serverDescription
)

LogTail.storeSnapshot()
Expand Down
3 changes: 2 additions & 1 deletion application/src/main/kotlin/application/TestCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ For example:
@Option(names = ["--overlay-file"], description = ["Overlay file for the specification"], required = false)
var overlayFilePath: String? = null

@Option(names = ["--description"], description = ["The port to bind to"])
@Option(names = ["--description"], description = ["The server description to pickup the server url"])
var description: String ?= null

override fun call() = try {
setParallelism()

Expand Down
304 changes: 194 additions & 110 deletions core/src/main/kotlin/io/specmatic/conversions/OpenApiSpecification.kt

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions core/src/main/kotlin/io/specmatic/core/utilities/Flags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Flags {
const val SPECMATIC_TEST_PARALLELISM = "SPECMATIC_TEST_PARALLELISM"
const val SPECMATIC_STUB_DELAY = "SPECMATIC_STUB_DELAY"
const val SPECMATIC_TEST_TIMEOUT = "SPECMATIC_TEST_TIMEOUT"
const val PATH_PREFIX = "PATH_PREFIX"

const val CONFIG_FILE_PATH = "CONFIG_FILE_PATH"

const val IGNORE_INLINE_EXAMPLES = "IGNORE_INLINE_EXAMPLES"
Expand All @@ -23,11 +23,14 @@ class Flags {
const val EXTENSIBLE_QUERY_PARAMS = "EXTENSIBLE_QUERY_PARAMS"
const val ADDITIONAL_EXAMPLE_PARAMS_FILE = "ADDITIONAL_EXAMPLE_PARAMS_FILE"

const val PATH_PREFIX = "PATH_PREFIX"
const val SERVER_DESCRIPTION = "SERVER_DESCRIPTION"
fun getStringValue(flagName: String): String? = System.getenv(flagName) ?: System.getProperty(flagName)

fun getBooleanValue(flagName: String, default: Boolean = false) = getStringValue(flagName)?.toBoolean() ?: default
fun getBooleanValue(flagName: String, default: Boolean = false) =
getStringValue(flagName)?.toBoolean() ?: default

fun getLongValue(flagName: String): Long? = ( getStringValue(flagName))?.toLong()
fun getLongValue(flagName: String): Long? = (getStringValue(flagName))?.toLong()

fun <T> using(vararg properties: Pair<String, String>, fn: () -> T): T {
try {
Expand Down
25 changes: 21 additions & 4 deletions core/src/main/kotlin/io/specmatic/stub/HttpStub.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.ktor.server.plugins.doublereceive.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.specmatic.conversions.OpenApiSpecification
import io.specmatic.core.*
import io.specmatic.core.log.*
import io.specmatic.core.pattern.ContractException
Expand Down Expand Up @@ -51,6 +52,7 @@ class HttpStub(
val specmaticConfigPath: String? = null,
private val timeoutMillis: Long = 0,
val pathPrefix: String? = null,
val serverDescription: String? = null,
) : ContractStub {
constructor(
feature: Feature,
Expand Down Expand Up @@ -182,7 +184,12 @@ class HttpStub(
return threadSafeHttpStubQueue.size
}

val endPoint = endPointFromHostAndPort(host, pathPrefix, port, keyData)
private val serverUrlFromOpenSpecs = serverDescription?.let {
val contractFilePaths = contractTestPathsFrom(getConfigFilePath(), workingDirectory!!.path)
getOpenApiSpecificationFromFilePath(contractFilePaths.first().path).getURLByDescription(it)
}

val endPoint = endPointFromHostAndPort(serverUrlFromOpenSpecs, host, pathPrefix, port, keyData)

override val client = HttpClient(this.endPoint)

Expand Down Expand Up @@ -224,7 +231,7 @@ class HttpStub(
anyHost()
}

val urlPrefixInterceptor = UrlPrefixInterceptor()
val urlPrefixInterceptor = UrlPrefixInterceptor(serverUrlFromOpenSpecs)
val urlDecodeInterceptor = UrlDecodeInterceptor()
registerRequestInterceptor(urlPrefixInterceptor)
registerRequestInterceptor(urlDecodeInterceptor)
Expand Down Expand Up @@ -1220,7 +1227,17 @@ internal fun httpResponseLog(response: HttpResponse): String =
internal fun httpRequestLog(httpRequest: HttpRequest): String =
">> Request Start At ${Date()}\n${httpRequest.toLogString("-> ")}"

fun endPointFromHostAndPort(host: String, pathPrefix: String?, port: Int?, keyData: KeyData?): String {
private fun getOpenApiSpecificationFromFilePath(configFilePath: String): OpenApiSpecification {
return OpenApiSpecification.fromFile(configFilePath)
}

fun endPointFromHostAndPort(
serverUrlFromOpenSpecs: String?,
host: String,
pathPrefix: String?,
port: Int?,
keyData: KeyData?
): String {
val protocol = when (keyData) {
null -> "http"
else -> "https"
Expand All @@ -1230,10 +1247,10 @@ fun endPointFromHostAndPort(host: String, pathPrefix: String?, port: Int?, keyDa
80, null -> ""
else -> ":$port"
}

pathPrefix?.let { return "$protocol://$host$computedPortString/${it.trim('/')}" }

return "$protocol://$host$computedPortString"

}

internal fun isPath(path: String?, lastPart: String): Boolean {
Expand Down
25 changes: 21 additions & 4 deletions core/src/main/kotlin/io/specmatic/stub/UrlPrefixIntraceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package io.specmatic.stub
import io.specmatic.core.HttpRequest
import io.specmatic.core.utilities.Flags.Companion.PATH_PREFIX

class UrlPrefixInterceptor : RequestInterceptor {
class UrlPrefixInterceptor(private val serverUrlFromOpenSpecs: String?) : RequestInterceptor {
override fun interceptRequest(httpRequest: HttpRequest): HttpRequest {

val pathPrefix = System.getProperty(PATH_PREFIX)
val prefixedPath = urlPrefixPathSegments(httpRequest.path!!, pathPrefix)
val prefixedPath = urlPrefixPathSegments(httpRequest.path!!, pathPrefix, serverUrlFromOpenSpecs)
return httpRequest.copy(path = prefixedPath)

}

private fun urlPrefixPathSegments(url: String, rawPrefix: String?): String {
val pathPrefix = rawPrefix?.let { "/${it.trim('/')}" }
private fun urlPrefixPathSegments(url: String, rawPrefix: String?,serverUrlFromOpenSpecs: String?): String {
val serverUrlPrefix = serverUrlFromOpenSpecs?.let {
java.net.URL(it).path
}
val pathPrefix = rawPrefix?.let { "/${it.trim('/')}" } ?: serverUrlPrefix?.let { "/${it.trim('/')}" }
val relativePath = getRelativePath(url, pathPrefix)
return relativePath;
}
Expand All @@ -27,5 +31,18 @@ class UrlPrefixInterceptor : RequestInterceptor {
else -> ""
}
}
// val serverURL = description?.let {
// getOpenApiSpecificationFromFilePath(getConfigFilePath()).getURLByDescription(it)
// }
//
// val prefixFromServerURL = serverURL?.substringBeforeLast('/')
//
// val finalURL = pathPrefix?.let {
// "$protocol://$host$computedPortString/${it.trim('/')}"
// } ?: prefixFromServerURL?.let {
// "$it"
// } ?: "$protocol://$host$computedPortString"
//
// return finalURL

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should decode path segments with path prefix`() {
System.setProperty("PATH_PREFIX", "api/v1")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/api/v1/products/123")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("/products/123")
Expand All @@ -18,7 +18,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should decode path segments without path prefix`() {
System.clearProperty("PATH_PREFIX")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/products/123")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("/products/123")
Expand All @@ -27,7 +27,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should return original path when path prefix is not a prefix`() {
System.setProperty("PATH_PREFIX", "api/v2")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/api/v1/products/123%20abc")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("")
Expand All @@ -36,7 +36,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should return empty path when URL does not match path prefix`() {
System.setProperty("PATH_PREFIX", "api/v2")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/unrelated/path")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("")
Expand All @@ -45,7 +45,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should decode default URL path`() {
System.setProperty("PATH_PREFIX", "/")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/api/v1/products")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("/api/v1/products")
Expand All @@ -54,7 +54,7 @@ class UrlPrefixInterceptorTest {
@Test
fun `should handle empty path prefix gracefully`() {
System.setProperty("PATH_PREFIX", "")
val interceptor = UrlPrefixInterceptor()
val interceptor = UrlPrefixInterceptor(null)
val request = HttpRequest(method = "GET", path = "/products/123")
val interceptedRequest = interceptor.interceptRequest(request)
assertThat(interceptedRequest.path).isEqualTo("/products/123")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.specmatic.test

import com.fasterxml.jackson.databind.ObjectMapper
import io.specmatic.conversions.OpenApiSpecification
import io.specmatic.conversions.convertPathParameterStyle
import io.specmatic.core.*
import io.specmatic.core.filters.ScenarioMetadataFilter
Expand Down Expand Up @@ -102,6 +103,10 @@ open class SpecmaticJUnitSupport {
}
}

private fun getOpenApiSpecificationFromFilePath(configFilePath: String): OpenApiSpecification {
return OpenApiSpecification.fromFile(configFilePath)
}

private fun getReportConfiguration(): ReportConfiguration {
return when (val reportConfiguration = specmaticConfig?.report) {
null -> {
Expand Down Expand Up @@ -234,7 +239,7 @@ open class SpecmaticJUnitSupport {
val suggestionsPath = System.getProperty(SUGGESTIONS_PATH) ?: ""

val workingDirectory = WorkingDirectory(givenWorkingDirectory ?: DEFAULT_WORKING_DIRECTORY)

val contractFilePaths = contractTestPathsFrom(configFile, workingDirectory.path)
val envConfig = getEnvConfig(System.getProperty(ENV_NAME))
val testConfig = try {
loadTestConfig(envConfig).withVariablesFromFilePath(System.getProperty(VARIABLES_FILE_NAME))
Expand Down Expand Up @@ -271,8 +276,6 @@ open class SpecmaticJUnitSupport {

createIfDoesNotExist(workingDirectory.path)

val contractFilePaths = contractTestPathsFrom(configFile, workingDirectory.path)

exitIfAnyDoNotExist("The following specifications do not exist", contractFilePaths.map { it.path })

val testScenariosAndEndpointsPairList = contractFilePaths.filter {
Expand Down Expand Up @@ -319,13 +322,22 @@ open class SpecmaticJUnitSupport {
}

val testBaseURL = try {
constructTestBaseURL()
val specificationURL =
getOpenApiSpecificationFromFilePath(contractFilePaths.first().path).getURLByDescription(description)
val testBaseURL = System.getProperty(TEST_BASE_URL)

when {
testBaseURL != null -> constructTestBaseURL(testBaseURL)
specificationURL != null -> specificationURL
else -> constructURLFromHostAndPort()
}
} catch (e: Throwable) {
logger.logError(e)
logger.newLine()
throw (e)
throw e
}


return try {
dynamicTestStream(testScenarios, testBaseURL, timeoutInMilliseconds)
} catch (e: Throwable) {
Expand All @@ -334,6 +346,7 @@ open class SpecmaticJUnitSupport {
}
}


private fun dynamicTestStream(
testScenarios: Sequence<ContractTest>,
testBaseURL: String,
Expand Down Expand Up @@ -386,15 +399,14 @@ open class SpecmaticJUnitSupport {
}.asStream()
}

fun constructTestBaseURL(): String {
val testBaseURL = System.getProperty(TEST_BASE_URL)
if (testBaseURL != null) {
when (val validationResult = validateURI(testBaseURL)) {
Success -> return testBaseURL
else -> throw TestAbortedException("${validationResult.message} in $TEST_BASE_URL environment variable")
}
fun constructTestBaseURL(testBaseURL: String): String {
return when (val validationResult = validateURI(testBaseURL)) {
Success -> testBaseURL
else -> throw TestAbortedException("${validationResult.message} in $TEST_BASE_URL environment variable")
}
}

private fun constructURLFromHostAndPort(): String {
val hostProperty = System.getProperty(HOST)
?: throw TestAbortedException("Please specify $TEST_BASE_URL OR $HOST and $PORT as environment variables")
val host = if (hostProperty.startsWith("http")) {
Expand Down Expand Up @@ -489,7 +501,7 @@ open class SpecmaticJUnitSupport {
suggestionsData.isNotEmpty() -> suggestionsFromCommandLine(suggestionsData)
else -> emptyList()
}
val servers = feature.serverDetails

val allEndpoints: List<Endpoint> = feature.scenarios.map { scenario ->
Endpoint(
convertPathParameterStyle(scenario.path),
Expand Down

0 comments on commit 9a8f7e1

Please sign in to comment.