fix: swagger ui regression from dependency bump

This commit is contained in:
Jevgeni Goloborodko
2022-04-01 16:21:50 +03:00
committed by GitHub
parent 7a1e57f50c
commit 26c481e1a4
9 changed files with 153 additions and 15 deletions

View File

@ -3,8 +3,10 @@
## Unreleased
### Added
- Added tests for Swagger UI module that verify that plugin generates correct responses for Swagger UI WEB resources (tests should detect future incompatible changes in new versions of `org.webjars.swagger-ui`)
### Changed
- Fixed broken Swagger UI plugin (`org.webjars.swagger-ui` WEB resources structure changed in version 4.9.X). Issue: https://github.com/bkbnio/kompendium/issues/236
### Remove

View File

@ -1,6 +1,5 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.constraint.Format
import io.bkbn.kompendium.core.handler.CollectionHandler
import io.bkbn.kompendium.core.handler.EnumHandler
import io.bkbn.kompendium.core.handler.MapHandler
@ -12,7 +11,6 @@ import io.bkbn.kompendium.oas.schema.SimpleSchema
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.typeOf
import org.slf4j.LoggerFactory

View File

@ -72,13 +72,13 @@ private fun Application.mainModule() {
// Example is prepared according to this documentation: https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/
jsInit = {
"""
ui.initOAuth({
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
realm: 'MY REALM',
appName: 'TEST APP',
useBasicAuthenticationWithAccessCodeGrant: true
});
window.ui.initOAuth({
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
realm: 'MY REALM',
appName: 'TEST APP',
useBasicAuthenticationWithAccessCodeGrant: true
});
"""
}
)

View File

@ -7,6 +7,7 @@ plugins {
id("maven-publish")
id("java-library")
id("signing")
id("java-test-fixtures")
}
sourdough {
@ -16,9 +17,13 @@ sourdough {
dependencies {
val ktorVersion: String by project
implementation(projects.kompendiumCore)
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "org.webjars", name = "webjars-locator-core", version = "0.50")
implementation(group = "org.webjars", name = "swagger-ui", version = "4.9.1")
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {

View File

@ -23,7 +23,7 @@ data class JsConfig(
internal fun JsConfig.toJsProps(): String = asMap()
.filterKeys { !setOf("specs", "jsInit").contains(it) }
.map{ "${it.key}: ${it.value.toJs()}" }
.joinToString(separator = ",\n\t\t")
.joinToString(separator = ",\n ")
internal fun JsConfig.getSpecUrlsProps(): String =
if (specs.isEmpty()) "[]" else specs.map { "{url: ${it.value.toJs()}, name: ${it.key.toJs()}}" }.toString()

View File

@ -56,8 +56,8 @@ class SwaggerUI(val config: Configuration) {
get("${config.swaggerBaseUrl}/{filename}") {
call.parameters["filename"]!!.let { filename ->
when(filename) {
"index.html" ->
locator.getSwaggerIndexContent(jsConfig = config.jsConfig)
"swagger-initializer.js" ->
locator.getSwaggerInitializerContent(jsConfig = config.jsConfig)
else ->
locator.getSwaggerResourceContent(path = filename)
}

View File

@ -12,13 +12,13 @@ internal fun WebJarAssetLocator.getSwaggerResource(path: String): URL =
internal fun WebJarAssetLocator.getSwaggerResourceContent(path: String): ByteArrayContent =
ByteArrayContent(getSwaggerResource(path).readBytes())
internal fun WebJarAssetLocator.getSwaggerIndexContent(jsConfig: JsConfig): ByteArrayContent = ByteArrayContent(
getSwaggerResource(path = "index.html").readText()
internal fun WebJarAssetLocator.getSwaggerInitializerContent(jsConfig: JsConfig): ByteArrayContent = ByteArrayContent(
getSwaggerResource(path = "swagger-initializer.js").readText()
.replaceFirst("url: \"https://petstore.swagger.io/v2/swagger.json\",", "urls: ${jsConfig.getSpecUrlsProps()},")
.replaceFirst("deepLinking: true", jsConfig.toJsProps())
.let { content ->
jsConfig.jsInit()?.let {
content.replaceFirst("window.ui = ui", "$it\n\twindow.ui = ui")
content.replaceFirst("});", "});\n$it")
} ?: content
}.toByteArray()
)

View File

@ -0,0 +1,54 @@
package io.bkbn.kompendium.swagger
import io.bkbn.kompendium.swagger.TestHelpers.TEST_SWAGGER_UI_INDEX
import io.bkbn.kompendium.swagger.TestHelpers.TEST_SWAGGER_UI_INIT_JS
import io.bkbn.kompendium.swagger.TestHelpers.TEST_SWAGGER_UI_ROOT
import io.bkbn.kompendium.swagger.TestHelpers.compareRedirect
import io.bkbn.kompendium.swagger.TestHelpers.compareResource
import io.bkbn.kompendium.swagger.TestHelpers.withSwaggerApplication
import io.kotest.core.spec.style.DescribeSpec
class SwaggerUiTest: DescribeSpec ({
describe("Swagger UI resources") {
it ("Redirects /swagger-ui -> index.html") {
withSwaggerApplication {
compareRedirect(TEST_SWAGGER_UI_ROOT, TEST_SWAGGER_UI_INDEX)
}
}
it ("Can return original: index.html") {
withSwaggerApplication {
compareResource(TEST_SWAGGER_UI_INDEX, listOf(
"<title>Swagger UI</title>",
"<div id=\"swagger-ui\"></div>",
"src=\"./swagger-initializer.js\""
))
}
}
it("Can return generated: swagger-initializer.js") {
withSwaggerApplication {
compareResource(TEST_SWAGGER_UI_INIT_JS, listOf(
"url: '/openapi.json', name: 'My API v1'",
"url: '/openapi.json', name: 'My API v2'",
"defaultModelExpandDepth: 4",
"defaultModelsExpandDepth: 4",
"displayOperationId: true",
"displayRequestDuration: true",
"operationsSorter: 'alpha'",
"persistAuthorization: true",
"tagsSorter: 'alpha'",
"window.ui.initOAuth",
"clientId: 'CLIENT_ID'",
"clientSecret: 'CLIENT_SECRET'",
"realm: 'MY REALM'",
"appName: 'TEST APP'",
"useBasicAuthenticationWithAccessCodeGrant: true"
))
}
}
}
})

View File

@ -0,0 +1,79 @@
package io.bkbn.kompendium.swagger
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.fixtures.kompendium
import io.kotest.assertions.ktor.shouldHaveStatus
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.TestApplicationEngine
import io.ktor.server.testing.createTestEnvironment
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withApplication
import java.net.URI
object TestHelpers {
const val TEST_SWAGGER_UI_ROOT = "/swagger-ui"
const val TEST_SWAGGER_UI_INDEX = "/webjars/swagger-ui/index.html"
const val TEST_SWAGGER_UI_INIT_JS = "/webjars/swagger-ui/swagger-initializer.js"
// The same config the same as in Playground
private val basicSwaggerUiConfig: SwaggerUI.Configuration.() -> Unit = {
swaggerUrl = "/swagger-ui"
jsConfig = JsConfig(
specs = mapOf(
"My API v1" to URI("/openapi.json"),
"My API v2" to URI("/openapi.json")
),
jsInit = {
"""
window.ui.initOAuth({
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
realm: 'MY REALM',
appName: 'TEST APP',
useBasicAuthenticationWithAccessCodeGrant: true
});
"""
}
)
}
fun TestApplicationEngine.compareRedirect(resourceName: String, targetResource: String) {
handleRequest(HttpMethod.Get, resourceName).apply {
response shouldHaveStatus HttpStatusCode.Found
response.headers[HttpHeaders.Location] shouldBe targetResource
}
}
fun TestApplicationEngine.compareResource(resourceName: String, mustContain: List<String>) {
handleRequest(HttpMethod.Get, resourceName).apply {
response shouldHaveStatus HttpStatusCode.OK
response.content shouldNotBe null
mustContain.forEach {
response.content!! shouldContain it
}
}
}
fun <R> withSwaggerApplication(
moduleFunction: Application.() -> Unit = {},
kompendiumConfigurer: Kompendium.Configuration.() -> Unit = {},
swaggerUIConfigurer: SwaggerUI.Configuration.() -> Unit = basicSwaggerUiConfig,
test: TestApplicationEngine.() -> R
) {
withApplication(createTestEnvironment()) {
moduleFunction(application.apply {
kompendium(kompendiumConfigurer)
install(SwaggerUI, swaggerUIConfigurer)
})
test()
}
}
}