From 713898ceab2af385754a90f5ad8a28b07a63e376 Mon Sep 17 00:00:00 2001 From: Sparow199 Date: Thu, 7 May 2020 20:17:50 +0200 Subject: [PATCH] feat(SwaggerUI): Add Swagger UI configuration parameters resolves #6314 --- extensions/swagger-ui/deployment/pom.xml | 4 + .../swaggerui/deployment/SwaggerUiConfig.java | 438 ++++++++++++++++++ .../deployment/SwaggerUiProcessor.java | 100 ++-- .../application-custom-config.properties | 33 ++ .../deployment/CustomConfigTest.java | 56 ++- extensions/swagger-ui/runtime/pom.xml | 4 + 6 files changed, 593 insertions(+), 42 deletions(-) create mode 100644 extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java create mode 100644 extensions/swagger-ui/deployment/src/main/resources/application-custom-config.properties diff --git a/extensions/swagger-ui/deployment/pom.xml b/extensions/swagger-ui/deployment/pom.xml index 9dc385902b6590..dcb8bcdb1d3f0d 100644 --- a/extensions/swagger-ui/deployment/pom.xml +++ b/extensions/swagger-ui/deployment/pom.xml @@ -30,6 +30,10 @@ io.quarkus quarkus-smallrye-openapi-common-deployment + + io.quarkus + quarkus-jackson-deployment + io.quarkus quarkus-swagger-ui diff --git a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java new file mode 100644 index 00000000000000..81aa03c6d54e7f --- /dev/null +++ b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java @@ -0,0 +1,438 @@ +package io.quarkus.swaggerui.deployment; + +import java.net.URI; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.vertx.core.http.HttpMethod; + +/** + * Please @see Swagger UI documentation + */ +@ConfigRoot +class SwaggerUiConfig { + + /** + * If Swagger UI should be enabled. By default, Swagger UI is enabled. + */ + @ConfigItem(defaultValue = "true") + boolean enable; + + /** + * The path where Swagger UI is available. + *

+ * The value `/` is not allowed as it blocks the application from serving anything else. + */ + @ConfigItem(defaultValue = "/swagger-ui") + String path; + + /** + * If this should be included every time. By default, this is only included when the application is running + * in dev mode. + */ + @ConfigItem + boolean alwaysInclude; + + /** + * URI to fetch external configuration document from. + */ + @ConfigItem + Optional configUrl; + + /** + * Enables deep linking for tags and operations. + * + * Please @see Swagger UI deep linking + * documentation + */ + @ConfigItem + boolean deepLinking; + + /** + * The default expansion depth for models (set to -1 completely hide the models). + */ + @ConfigItem(defaultValue = "1") + int defaultModelsExpandDepth; + + /** + * The default expansion depth for the model in the model-example section. + */ + @ConfigItem(defaultValue = "1") + int defaultModelExpandDepth; + + /** + * Controls how the model is shown when the API is first rendered. + */ + @ConfigItem(defaultValue = "EXAMPLE") + Rendering defaultModelRendering; + + /** + * Controls the display of operationId in operations list. + */ + @ConfigItem + boolean displayOperationId; + + /** + * Controls the display of the request duration (in milliseconds) for Try-It-Out requests. + */ + @ConfigItem + boolean displayRequestDuration; + + /** + * Controls the default expansion setting for the operations and tags. + */ + @ConfigItem(defaultValue = "LIST") + Expansion docExpansion; + + /** + * If set, enables filtering. The top bar will show an edit box that could be used to filter + * the tagged operations that are shown. + */ + @ConfigItem + boolean filter; + /** + * The name of a component available via the plugin system to use as the top-level layout for Swagger UI. + */ + @ConfigItem(defaultValue = "StandaloneLayout") + String layout; + + /** + * If set, limits the number of tagged operations displayed to at most this many. + */ + @ConfigItem + OptionalInt maxDisplayedTags; + + /** + * Controls the display of vendor extension (x-) fields and values. + */ + @ConfigItem + boolean showExtensions; + + /** + * Controls the display of extensions. + */ + @ConfigItem + boolean showCommonExtensions; + + /** + * The supported try it out methods. + */ + @ConfigItem + Optional> supportedSubmitMethods; + + /** + * Apply a sort to the operation list of each API. + */ + @ConfigItem + Optional operationsSorter; + + /** + * Apply a sort to the tag list of each API. + */ + @ConfigItem + Optional tagsSorter; + + /** + * The url pointing to API definition (the default points to the URI generated by the OpenAPI extension). + */ + @ConfigItem + Optional url; + + /** + * The order of the swagger groups which will be displayed when Swagger UI loads. + */ + @ConfigItem(defaultValue = "ASC") + Order groupsOrder; + + /** + * When used and Topbar plugin is enabled, the url parameter will not be parsed. + * Names and URLs must be unique among all items in this array, since they're used as identifiers. + * SpecGroupUrl must match the pattern: ?group= + */ + @ConfigItem + Optional> urls; + + /** + * The name of the swagger group which will be displayed when Swagger UI loads. + */ + @ConfigItem + Optional urlsPrimaryName; + + /** + * Set a different validator URL, for example for locally deployed validators. + */ + @ConfigItem + Optional validatorUrl; + + /** + * OAuth redirect URL. + */ + @ConfigItem + Optional oauth2RedirectUrl; + + /** + * OAuth configuration. + */ + OAuthConfig oauth; + + public Map getSwaggerUiProps(final String openApiPath) { + updateApiUrl(openApiPath); + updateAuthUrl(); + updateLayout(); + return getProps(); + } + + private Map getProps() { + Map props = new TreeMap<>(); + props.put("deepLinking", deepLinking); + props.put("defaultModelExpandDepth", defaultModelExpandDepth); + props.put("defaultModelRendering", defaultModelRendering.toString().toLowerCase()); + props.put("defaultModelsExpandDepth", defaultModelsExpandDepth); + props.put("displayOperationId", displayOperationId); + props.put("displayRequestDuration", displayRequestDuration); + props.put("docExpansion", docExpansion.toString().toLowerCase()); + props.put("dom_id", "#swagger-ui"); + props.put("filter", filter); + maxDisplayedTags.ifPresent(i -> props.put("maxDisplayedTags", i)); + props.put("layout", layout); + oauth2RedirectUrl.ifPresent(s -> props.put("oauth2RedirectUrl", s)); + operationsSorter.ifPresent(s -> props.put("operationsSorter", s.toString().toLowerCase())); + props.put("showCommonExtensions", showCommonExtensions); + props.put("showExtensions", showExtensions); + supportedSubmitMethods.ifPresent(ls -> props.put("supportedSubmitMethods", getSupportedSubmitMethods(ls))); + tagsSorter.ifPresent(s -> props.put("tagsSorter", s.toString().toLowerCase())); + url.ifPresent(s -> props.put("url", s)); + url.ifPresent(s -> props.put("url", s)); + urls.ifPresent(sgu -> props.put("urls", getSpecGroupUrls(sgu))); + urlsPrimaryName.ifPresent(s -> props.put("urls.primaryName", s)); + validatorUrl.ifPresent(s -> props.put("validatorUrl", s)); + return props; + } + + private void updateApiUrl(String openApiPath) { + if (!StringUtils.isEmpty(openApiPath)) { + final Optional openApiUri = Optional.of(URI.create(openApiPath)); + if (!this.url.isPresent()) { + this.url = openApiUri; + } + } + } + + private void updateAuthUrl() { + if (this.oauth.enable && !this.oauth2RedirectUrl.isPresent()) { + this.oauth2RedirectUrl = Optional.of(this.path + "/oauth2-redirect.html"); + } + } + + private void updateLayout() { + if (this.urls.isPresent() && this.layout.equals("BaseLayout")) { + this.layout = "StandaloneLayout"; + } + } + + private List getSupportedSubmitMethods(List supportedSubmitMethods) { + return supportedSubmitMethods.stream() + .filter(Objects::nonNull) + .map(Enum::name) + .map(String::toLowerCase) + .collect(Collectors.toList()); + } + + enum Expansion { + LIST, + FULL, + NONE + } + + enum Rendering { + EXAMPLE, + MODEL + } + + enum Sort { + ALPHA, + METHOD + } + + enum Order { + ASC, + DESC; + + public boolean isAscending() { + return this.equals(ASC); + } + } + + private Set getSpecGroupUrls(Set specGroupUrls) { + Comparator specGroupUrlComparator; + if (groupsOrder.isAscending()) { + specGroupUrlComparator = Comparator.comparing(SpecGroupUrl::getName); + } else { + specGroupUrlComparator = (url1, url2) -> url2.getName().compareTo(url1.getName()); + } + + return specGroupUrls.stream() + .sorted(specGroupUrlComparator) + .filter(specGroupUrl -> StringUtils.isNotEmpty(specGroupUrl.getUrl().toString())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + } + + public static class SpecGroupUrl { + + URI url; + String name; + + public SpecGroupUrl() { + } + + public SpecGroupUrl(String specGroup) { + Objects.requireNonNull(specGroup, "specGroup cannot be null"); + if (specGroup.contains("?group=")) { + String[] args = specGroup.trim().split("\\?group="); + if (args.length == 2) { + this.url = URI.create(args[0]); + this.name = args[1]; + } else { + throw new IllegalArgumentException( + "Error, expected arguments number to be 2, but found: " + args.length + + ", " + specGroup + " must match the pattern: ?group="); + } + } else { + throw new IllegalArgumentException(specGroup + " doesn't match the pattern: ?group="); + } + } + + public URI getUrl() { + return url; + } + + public void setUrl(URI url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SpecGroupUrl that = (SpecGroupUrl) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return "SwaggerUrl{" + "url='" + url + '\'' + + ", name='" + name + '\'' + + '}'; + } + } + + /** + * Please @see Swagger UI OAuth 2.0 + * documentation + */ + @ConfigGroup + public static class OAuthConfig { + + /** + * Indicates whether to setup OAuth 2.0 + */ + @ConfigItem(name = ConfigItem.PARENT) + boolean enable; + + /** + * Application name, displayed in authorization popup. MUST be a string. + */ + @ConfigItem + Optional appName; + + /** + * Default clientId. MUST be a string. + */ + @ConfigItem + Optional clientId; + + /** + * Never use this parameter in your production environment. It exposes cruicial security information. + * This feature is intended for dev/test environments only. + * Default clientSecret. MUST be a string + */ + @ConfigItem + Optional clientSecret; + + /** + * Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl. MUST be a string. + */ + @ConfigItem + Optional realm; + + /** + * Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20). + * MUST be a string. + */ + @ConfigItem + Optional scopeSeparator; + + /** + * Additional query parameters added to authorizationUrl and tokenUrl. MUST be an object. + */ + @ConfigItem + Map additionalQueryStringParams; + + /** + * Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, + * pass the Client Password using the HTTP Basic Authentication scheme + * (Authorization header with Basic base64encode(client_id + client_secret)). The default is false. + */ + @ConfigItem + boolean useBasicAuthenticationWithAccessCodeGrant; + + /** + * Only applies to authorizatonCode flows. + * Proof Key for Code Exchange brings enhanced security for OAuth public clients. The default is false. + */ + @ConfigItem + boolean usePkceWithAuthorizationCodeGrant; + + public Map getConfigParameters() { + final Map params = new TreeMap<>(); + clientId.ifPresent(s -> params.put("clientId", s)); + clientSecret.ifPresent(s -> params.put("clientSecret", s)); + realm.ifPresent(s -> params.put("realm", s)); + appName.ifPresent(s -> params.put("appName", s)); + scopeSeparator.ifPresent(s -> params.put("scopeSeparator", s)); + params.put("additionalQueryStringParams", additionalQueryStringParams); + params.put("useBasicAuthenticationWithAccessCodeGrant", useBasicAuthenticationWithAccessCodeGrant); + params.put("usePkceWithAuthorizationCodeGrant", usePkceWithAuthorizationCodeGrant); + return params; + } + } +} diff --git a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java index 1b450ee4fbfb6c..9e7adf95e50faf 100644 --- a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java +++ b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java @@ -10,6 +10,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; @@ -19,6 +20,14 @@ import org.jboss.logging.Logger; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; + import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.bootstrap.model.AppArtifact; import io.quarkus.bootstrap.model.AppDependency; @@ -35,8 +44,6 @@ import io.quarkus.deployment.configuration.ConfigurationError; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.util.FileUtil; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; import io.quarkus.swaggerui.runtime.SwaggerUiRecorder; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; @@ -53,16 +60,22 @@ public class SwaggerUiProcessor { private static final String SWAGGER_UI_WEBJAR_ARTIFACT_ID = "swagger-ui"; private static final String SWAGGER_UI_WEBJAR_PREFIX = "META-INF/resources/webjars/swagger-ui"; private static final String SWAGGER_UI_FINAL_DESTINATION = "META-INF/swagger-ui-files"; - private static final Pattern SWAGGER_UI_DEFAULT_API_URL_PATTERN = Pattern.compile("(.* url: \")(.*)(\",.*)", + private static final Pattern SWAGGER_UI_CONFIG_PATTERN = Pattern.compile( + "(.*SwaggerUIBundle\\()(.*)(presets:.*)(layout:.*)(\\).*)", Pattern.DOTALL); private static final String TEMP_DIR_PREFIX = "quarkus-swagger-ui_" + System.nanoTime(); + private static ObjectMapper objectMapper; + /** * The configuration for Swagger UI. */ SwaggerUiConfig swaggerUiConfig; - SmallRyeOpenApiConfig openapi; + /** + * The configuration for OpenAPI. + */ + SmallRyeOpenApiConfig openApiConfig; @Inject private LaunchModeBuildItem launch; @@ -95,7 +108,7 @@ public void registerSwaggerUiServletExtension(SwaggerUiRecorder recorder, return; } - String openApiPath = httpRootPathBuildItem.adjustPath(openapi.path); + String openApiPath = httpRootPathBuildItem.adjustPath(openApiConfig.path); if (launch.getLaunchMode().isDevOrTest()) { CachedSwaggerUI cached = liveReloadBuildItem.getContextObject(CachedSwaggerUI.class); @@ -118,7 +131,7 @@ public void registerSwaggerUiServletExtension(SwaggerUiRecorder recorder, AppArtifact artifact = getSwaggerUiArtifact(curateOutcomeBuildItem); Path tempDir = Files.createTempDirectory(TEMP_DIR_PREFIX).toRealPath(); extractSwaggerUi(artifact, tempDir); - updateApiUrl(tempDir.resolve("index.html"), openApiPath); + updateSwaggerUiConfig(tempDir.resolve("index.html"), openApiPath); cached.cachedDirectory = tempDir.toAbsolutePath().toString(); cached.cachedOpenAPIPath = openApiPath; } catch (IOException e) { @@ -147,8 +160,10 @@ public void registerSwaggerUiServletExtension(SwaggerUiRecorder recorder, String filename = entry.getName().replace(versionedSwaggerUiWebjarPrefix, ""); byte[] content = FileUtil.readFileContents(inputStream); if (entry.getName().endsWith("index.html")) { - content = updateApiUrl(new String(content, StandardCharsets.UTF_8), openApiPath) - .getBytes(StandardCharsets.UTF_8); + final String html = updateConfig(new String(content, StandardCharsets.UTF_8), openApiPath); + if (html != null) { + content = html.getBytes(StandardCharsets.UTF_8); + } } String fileName = SWAGGER_UI_FINAL_DESTINATION + "/" + filename; generatedResources @@ -198,47 +213,60 @@ private void extractSwaggerUi(AppArtifact artifact, Path resourceDir) throws IOE } } - private void updateApiUrl(Path indexHtml, String openApiPath) throws IOException { + private void updateSwaggerUiConfig(Path indexHtml, String openApiPath) throws IOException { String content = new String(Files.readAllBytes(indexHtml), StandardCharsets.UTF_8); - String result = updateApiUrl(content, openApiPath); + String result = updateConfig(content, openApiPath); if (result != null) { Files.write(indexHtml, result.getBytes(StandardCharsets.UTF_8)); } } - public String updateApiUrl(String original, String openApiPath) { - - Matcher uriMatcher = SWAGGER_UI_DEFAULT_API_URL_PATTERN.matcher(original); - if (uriMatcher.matches()) { - return uriMatcher.replaceFirst("$1" + openApiPath + "$3"); + private String updateConfig(String original, String openApiPath) throws JsonProcessingException { + Matcher configMatcher = SWAGGER_UI_CONFIG_PATTERN.matcher(original); + if (configMatcher.matches()) { + objectMapper = initObjectMapper(); + String defaultConfig = configMatcher.group(3).trim(); + String config; + String html; + config = objectMapper.writeValueAsString(swaggerUiConfig.getSwaggerUiProps(openApiPath)); + html = configMatcher.replaceFirst("$1" + buildConfig(defaultConfig, config) + "$5"); + if (swaggerUiConfig.oauth.enable) { + html = addOauthConfig(html, swaggerUiConfig.oauth.getConfigParameters()); + } + return html; } else { - log.warn("Unable to replace the default URL of Swagger UI"); + log.warn("Unable to replace the default configuration of Swagger UI"); return null; } } - @ConfigRoot - static final class SwaggerUiConfig { - /** - * The path where Swagger UI is available. - *

- * The value `/` is not allowed as it blocks the application from serving anything else. - */ - @ConfigItem(defaultValue = "/swagger-ui") - String path; + private String buildConfig(String defaultConfig, String config) { + StringBuilder sb = new StringBuilder(config); + sb.insert(1, defaultConfig); + sb.insert(1, "\n"); + return sb.toString(); + } - /** - * If this should be included every time. By default this is only included when the application is running - * in dev mode. - */ - @ConfigItem - boolean alwaysInclude; + private String addOauthConfig(String html, Map oauthParams) throws JsonProcessingException { + if (oauthParams.isEmpty()) { + return html; + } + StringBuilder sb = new StringBuilder("window.ui = ui\n"); + sb.append("ui.initOAuth(\n"); + String json = objectMapper.writeValueAsString(oauthParams); + sb.append(json); + sb.append("\n)"); + return html.replace("window.ui = ui", sb.toString()); + } - /** - * If Swagger UI should be enabled. By default, Swagger UI is enabled. - */ - @ConfigItem(defaultValue = "true") - boolean enable; + private static ObjectMapper initObjectMapper() { + return JsonMapper.builder() + .serializationInclusion(JsonInclude.Include.NON_NULL) + .serializationInclusion(JsonInclude.Include.NON_ABSENT) + .disable(JsonWriteFeature.QUOTE_FIELD_NAMES) + .enable(SerializationFeature.INDENT_OUTPUT) + .addModule(new Jdk8Module()) + .build(); } private static final class CachedSwaggerUI implements Runnable { diff --git a/extensions/swagger-ui/deployment/src/main/resources/application-custom-config.properties b/extensions/swagger-ui/deployment/src/main/resources/application-custom-config.properties new file mode 100644 index 00000000000000..3b19f38a2582bf --- /dev/null +++ b/extensions/swagger-ui/deployment/src/main/resources/application-custom-config.properties @@ -0,0 +1,33 @@ +quarkus.swagger-ui.enable=true +quarkus.swagger-ui.config-url=/external +quarkus.swagger-ui.deep-linking=true +quarkus.swagger-ui.default-model-expand-depth=-1 +quarkus.swagger-ui.default-model-rendering=MODEL +quarkus.swagger-ui.default-models-expand-depth=-1 +quarkus.swagger-ui.display-operation-id=true +quarkus.swagger-ui.display-request-duration=true +quarkus.swagger-ui.doc-expansion=NONE +quarkus.swagger-ui.filter=true +quarkus.swagger-ui.groups-order=DESC +quarkus.swagger-ui.layout=BaseLayout +quarkus.swagger-ui.max-displayed-tags=5 +quarkus.swagger-ui.oauth2-redirect-url=/custom/oauth2-redirect.html +quarkus.swagger-ui.operations-sorter=METHOD +quarkus.swagger-ui.path=/custom +quarkus.swagger-ui.show-common-extensions=true +quarkus.swagger-ui.show-extensions=true +quarkus.swagger-ui.supported-submit-methods=OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PATCH, OTHER +quarkus.swagger-ui.tags-sorter=ALPHA +quarkus.swagger-ui.urls-primary-name=/openapi?group=test +quarkus.swagger-ui.urls=/openapi?group=test, /openapi?group=lahzouz +quarkus.swagger-ui.validator-url=https://validator.swagger.io/validator + +quarkus.swagger-ui.oauth.enable=true +quarkus.swagger-ui.oauth.app-name=quarkus +quarkus.swagger-ui.oauth.client-id=your-client-id +quarkus.swagger-ui.oauth.client-secret=your-client-secret-if-required +quarkus.swagger-ui.oauth.realm=your-realms +quarkus.swagger-ui.oauth.scope-separator=######## +quarkus.swagger-ui.oauth.use-basic-authentication-with-access-code-grant=true +quarkus.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true +quarkus.swagger-ui.oauth.additional-query-string-params.test=quarkus diff --git a/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java b/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java index 7f29285cbddf0c..e01e3eea6c43aa 100644 --- a/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java +++ b/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java @@ -1,9 +1,8 @@ package io.quarkus.swaggerui.deployment; -import static org.hamcrest.Matchers.containsString; - +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -15,12 +14,57 @@ public class CustomConfigTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-custom-config.properties") .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addAsResource(new StringAsset("quarkus.swagger-ui.path=/custom"), "application.properties")); + .addAsManifestResource("application.properties", "microprofile-config.properties")); @Test public void shouldUseCustomConfig() { - RestAssured.when().get("/custom").then().statusCode(200).body(containsString("/openapi")); - RestAssured.when().get("/custom/index.html").then().statusCode(200).body(containsString("/openapi")); + final Matcher stringContainsInOrder = Matchers.stringContainsInOrder( + "const ui = SwaggerUIBundle(", + "deepLinking : true", + "defaultModelExpandDepth : -1", + "defaultModelRendering : \"model\"", + "defaultModelsExpandDepth : -1", + "displayOperationId : true", + "displayRequestDuration : true", + "docExpansion : \"none\"", + "dom_id : \"#swagger-ui\"", + "filter : true", + "layout : \"BaseLayout\"", + "maxDisplayedTags : 5", + "oauth2RedirectUrl : \"/custom/oauth2-redirect.html\"", + "operationsSorter : \"method\"", + "showCommonExtensions : true", + "showExtensions : true", + "supportedSubmitMethods : [ \"options\", \"get\", \"head\", \"post\", \"put\", \"delete\", \"trace\", \"connect\", \"patch\", \"other\" ]", + "tagsSorter : \"alpha\"", + "url : \"/openapi\"", + "urls : [ {\n" + + " url : \"/openapi\",\n" + + " name : \"test\"\n" + + " }, {\n" + + " url : \"/openapi\",\n" + + " name : \"lahzouz\"\n" + + " } ],", + "urls.primaryName : \"/openapi?group=test\"", + "validatorUrl : \"https://validator.swagger.io/validator\""); + + RestAssured.when().get("/custom").then().statusCode(200).body(stringContainsInOrder); + RestAssured.when().get("/custom/index.html").then().statusCode(200).body(stringContainsInOrder); + } + + @Test + public void shouldUseOauthCustomConfig() { + RestAssured.when().get("/custom/index.html").then().statusCode(200) + .body(Matchers.stringContainsInOrder( + "appName : \"quarkus\"", + "clientId : \"your-client-id\"", + "clientSecret : \"your-client-secret-if-required\"", + "realm : \"your-realms\"", + "scopeSeparator : \"########\"", + "useBasicAuthenticationWithAccessCodeGrant : true", + "usePkceWithAuthorizationCodeGrant : true")); + RestAssured.when().get("/custom/oauth2-redirect.html").then().statusCode(200); } } diff --git a/extensions/swagger-ui/runtime/pom.xml b/extensions/swagger-ui/runtime/pom.xml index dcad2198150d26..60ee5e2210d18a 100644 --- a/extensions/swagger-ui/runtime/pom.xml +++ b/extensions/swagger-ui/runtime/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-arc + + io.quarkus + quarkus-jackson +