From 72d90e252514c316b019cf718fc86fd0511e688d Mon Sep 17 00:00:00 2001 From: Anthony Legay Date: Sun, 3 Nov 2024 20:14:00 +0100 Subject: [PATCH] feat: handle root security requirements on root DSL (#101) --- .../api/tegral-openapi-dsl.api | 30 +++++-- .../tegral/openapi/dsl/OperationDsl.kt | 32 ++------ .../zoroark/tegral/openapi/dsl/PathDsl.kt | 7 ++ .../zoroark/tegral/openapi/dsl/RootDsl.kt | 20 ++++- .../zoroark/tegral/openapi/dsl/SecurityDsl.kt | 79 +++++++++++++++++++ .../openapi/dsl/OperationBuilderTest.kt | 8 ++ .../tegral/openapi/dsl/PathsBuilderTest.kt | 7 +- .../api/tegral-openapi-ktor.api | 4 + .../zoroark/tegral/openapi/ktor/PluginTest.kt | 27 +++++++ 9 files changed, 179 insertions(+), 35 deletions(-) create mode 100644 tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/SecurityDsl.kt diff --git a/tegral-openapi/tegral-openapi-dsl/api/tegral-openapi-dsl.api b/tegral-openapi/tegral-openapi-dsl/api/tegral-openapi-dsl.api index ee5f6030..65e006c9 100644 --- a/tegral-openapi/tegral-openapi-dsl/api/tegral-openapi-dsl.api +++ b/tegral-openapi/tegral-openapi-dsl/api/tegral-openapi-dsl.api @@ -199,16 +199,18 @@ public final class guru/zoroark/tegral/openapi/dsl/OperationBuilder : guru/zoroa public fun response (ILkotlin/jvm/functions/Function1;)V public fun security (Ljava/lang/String;)V public fun security (Ljava/lang/String;[Ljava/lang/String;)V + public fun security (Lkotlin/jvm/functions/Function1;)V public fun setDeprecated (Ljava/lang/Boolean;)V public fun setDescription (Ljava/lang/String;)V public fun setExternalDocsDescription (Ljava/lang/String;)V public fun setExternalDocsUrl (Ljava/lang/String;)V public fun setOperationId (Ljava/lang/String;)V public fun setRequestBody (Lguru/zoroark/tegral/openapi/dsl/RequestBodyBuilder;)V + public fun setSecurityRequirements (Ljava/util/List;)V public fun setSummary (Ljava/lang/String;)V } -public abstract interface class guru/zoroark/tegral/openapi/dsl/OperationDsl { +public abstract interface class guru/zoroark/tegral/openapi/dsl/OperationDsl : guru/zoroark/tegral/openapi/dsl/SecurityDsl { public abstract fun body (Lkotlin/jvm/functions/Function1;)V public abstract fun cookieParameter (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public abstract fun getDeprecated ()Ljava/lang/Boolean; @@ -219,15 +221,12 @@ public abstract interface class guru/zoroark/tegral/openapi/dsl/OperationDsl { public abstract fun getParameters ()Ljava/util/List; public abstract fun getRequestBody ()Lguru/zoroark/tegral/openapi/dsl/RequestBodyBuilder; public abstract fun getResponses ()Ljava/util/Map; - public abstract fun getSecurityRequirements ()Ljava/util/List; public abstract fun getSummary ()Ljava/lang/String; public abstract fun getTags ()Ljava/util/List; public abstract fun headerParameter (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public abstract fun pathParameter (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public abstract fun queryParameter (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public abstract fun response (ILkotlin/jvm/functions/Function1;)V - public abstract fun security (Ljava/lang/String;)V - public abstract fun security (Ljava/lang/String;[Ljava/lang/String;)V public abstract fun setDeprecated (Ljava/lang/Boolean;)V public abstract fun setDescription (Ljava/lang/String;)V public abstract fun setExternalDocsDescription (Ljava/lang/String;)V @@ -332,6 +331,7 @@ public final class guru/zoroark/tegral/openapi/dsl/PathBuilder : guru/zoroark/te public fun response (ILkotlin/jvm/functions/Function1;)V public fun security (Ljava/lang/String;)V public fun security (Ljava/lang/String;[Ljava/lang/String;)V + public fun security (Lkotlin/jvm/functions/Function1;)V public fun setDelete (Lguru/zoroark/tegral/openapi/dsl/OperationBuilder;)V public fun setDeprecated (Ljava/lang/Boolean;)V public fun setDescription (Ljava/lang/String;)V @@ -479,6 +479,7 @@ public final class guru/zoroark/tegral/openapi/dsl/RootBuilder : guru/zoroark/te public fun getLicenseIdentifier ()Ljava/lang/String; public fun getLicenseName ()Ljava/lang/String; public fun getLicenseUrl ()Ljava/lang/String; + public fun getSecurityRequirements ()Ljava/util/List; public fun getSummary ()Ljava/lang/String; public fun getTermsOfService ()Ljava/lang/String; public fun getTitle ()Ljava/lang/String; @@ -489,6 +490,9 @@ public final class guru/zoroark/tegral/openapi/dsl/RootBuilder : guru/zoroark/te public fun patch (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun post (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun put (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public fun security (Ljava/lang/String;)V + public fun security (Ljava/lang/String;[Ljava/lang/String;)V + public fun security (Lkotlin/jvm/functions/Function1;)V public fun securityScheme (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun server (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun setContactEmail (Ljava/lang/String;)V @@ -500,6 +504,7 @@ public final class guru/zoroark/tegral/openapi/dsl/RootBuilder : guru/zoroark/te public fun setLicenseIdentifier (Ljava/lang/String;)V public fun setLicenseName (Ljava/lang/String;)V public fun setLicenseUrl (Ljava/lang/String;)V + public fun setSecurityRequirements (Ljava/util/List;)V public fun setSummary (Ljava/lang/String;)V public fun setTermsOfService (Ljava/lang/String;)V public fun setTitle (Ljava/lang/String;)V @@ -507,7 +512,7 @@ public final class guru/zoroark/tegral/openapi/dsl/RootBuilder : guru/zoroark/te public fun tag (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V } -public abstract interface class guru/zoroark/tegral/openapi/dsl/RootDsl : guru/zoroark/tegral/openapi/dsl/InfoDsl, guru/zoroark/tegral/openapi/dsl/PathsDsl, guru/zoroark/tegral/openapi/dsl/TagsDsl { +public abstract interface class guru/zoroark/tegral/openapi/dsl/RootDsl : guru/zoroark/tegral/openapi/dsl/InfoDsl, guru/zoroark/tegral/openapi/dsl/PathsDsl, guru/zoroark/tegral/openapi/dsl/SecurityDsl, guru/zoroark/tegral/openapi/dsl/TagsDsl { public abstract fun getExternalDocsDescription ()Ljava/lang/String; public abstract fun getExternalDocsUrl ()Ljava/lang/String; public abstract fun securityScheme (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V @@ -516,6 +521,21 @@ public abstract interface class guru/zoroark/tegral/openapi/dsl/RootDsl : guru/z public abstract fun setExternalDocsUrl (Ljava/lang/String;)V } +public abstract interface class guru/zoroark/tegral/openapi/dsl/SecurityDsl { + public abstract fun getSecurityRequirements ()Ljava/util/List; + public abstract fun security (Ljava/lang/String;)V + public abstract fun security (Ljava/lang/String;[Ljava/lang/String;)V + public abstract fun security (Lkotlin/jvm/functions/Function1;)V +} + +public final class guru/zoroark/tegral/openapi/dsl/SecurityRequirementsBuilder : guru/zoroark/tegral/core/Buildable { + public fun ()V + public fun build ()Lio/swagger/v3/oas/models/security/SecurityRequirement; + public synthetic fun build ()Ljava/lang/Object; + public final fun requirement (Ljava/lang/String;)V + public final fun requirement (Ljava/lang/String;[Ljava/lang/String;)V +} + public final class guru/zoroark/tegral/openapi/dsl/SecuritySchemeBuilder : guru/zoroark/tegral/core/Buildable, guru/zoroark/tegral/openapi/dsl/SecuritySchemeDsl { public fun ()V public fun build ()Lio/swagger/v3/oas/models/security/SecurityScheme; diff --git a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/OperationDsl.kt b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/OperationDsl.kt index 9c88a2bf..a7e8faf7 100644 --- a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/OperationDsl.kt +++ b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/OperationDsl.kt @@ -29,7 +29,7 @@ import io.swagger.v3.oas.models.security.SecurityRequirement * Note that the `externalDocs` object is embedded in this DSL. */ @TegralDsl -interface OperationDsl { +interface OperationDsl : SecurityDsl { /** * A short summary of what the operation does. */ @@ -86,16 +86,6 @@ interface OperationDsl { @TegralDsl val parameters: MutableList> - /** - * A declaration of which security mechanisms can be used for this operation. - * - * - This list behaves like an "OR", only one needs to be fulfilled for the operation. - * - Requirements defined in the individual `SecurityRequirement` objects behave like an "AND", and all of them need - * to be fulfilled. - */ - @TegralDsl - val securityRequirements: MutableList - /** * The list of possible responses as they are returned from executing this operation. */ @@ -109,18 +99,6 @@ interface OperationDsl { @TegralDsl val tags: MutableList - /** - * Adds a security requirement object to this operation with the given key. - */ - @TegralDsl - fun security(key: String) - - /** - * Adds a security requirement object to this operation with the given key and scopes. - */ - @TegralDsl - fun security(key: String, vararg scopes: String) - /** * Creates a response for the given response code (passed as an integer value). */ @@ -172,14 +150,12 @@ class OperationBuilder(private val context: OpenApiDslContext) : OperationDsl, B override var requestBody: RequestBodyBuilder? = null override var deprecated: Boolean? = null override var operationId: String? = null + override var securityRequirements = mutableListOf() // TODO callbacks, servers override val tags = mutableListOf() override val parameters = mutableListOf>() - override val securityRequirements = mutableListOf() - - // TODO properly support AND scenarios between security requirements (right now it's OR only) override fun security(key: String) { securityRequirements.add(SecurityRequirement().addList(key)) @@ -189,6 +165,10 @@ class OperationBuilder(private val context: OpenApiDslContext) : OperationDsl, B securityRequirements.add(SecurityRequirement().addList(key, scopes.toList())) } + override fun security(builder: SecurityRequirementsBuilder.() -> Unit) { + securityRequirements.add(SecurityRequirementsBuilder().apply(builder).build()) + } + override infix fun Int.response(builder: ResponseDsl.() -> Unit) { responses[this] = ResponseBuilder(context).apply(builder) } diff --git a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/PathDsl.kt b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/PathDsl.kt index 37b2f643..0543f304 100644 --- a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/PathDsl.kt +++ b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/PathDsl.kt @@ -215,10 +215,13 @@ class PathBuilder(private val context: OpenApiDslContext) : PathDsl, Buildable

> get() = error(WRITE_ONLY_ERROR_MSG) + override val securityRequirements: MutableList get() = error(WRITE_ONLY_ERROR_MSG) + override val responses: MutableMap> get() = error(WRITE_ONLY_ERROR_MSG) + override val tags: MutableList = mutableListOf() override fun security(key: String) { @@ -229,6 +232,10 @@ class PathBuilder(private val context: OpenApiDslContext) : PathDsl, Buildable

Unit) { + addOperationDefault { security(builder) } + } + override fun Int.response(builder: ResponseDsl.() -> Unit) { addOperationDefault { this@response.response(builder) } } diff --git a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/RootDsl.kt b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/RootDsl.kt index 8b724ab6..61638fad 100644 --- a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/RootDsl.kt +++ b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/RootDsl.kt @@ -20,6 +20,7 @@ import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.ExternalDocumentation import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.servers.Server /** @@ -32,12 +33,13 @@ import io.swagger.v3.oas.models.servers.Server * - [Info][InfoDsl] (embedded) * - [Tags][TagsDsl] (embedded) * - [Paths][PathsDsl] (embedded) + * - [Security][SecurityDsl] (embedded) * - External documentation ([description][externalDocsDescription] and [url][externalDocsUrl]) * * (Items marked as embedded are separate DSL interfaces that are available in [RootDsl] and can be used directly). */ @TegralDsl -interface RootDsl : InfoDsl, TagsDsl, PathsDsl { +interface RootDsl : InfoDsl, TagsDsl, PathsDsl, SecurityDsl { /** * Adds a security scheme to this OpenAPI document with the given string as the name, using the lambda to configure * further options. @@ -76,7 +78,7 @@ class RootBuilder( ) : RootDsl, InfoDsl by infoBuilder, PathsDsl by paths, Buildable { private val tags = mutableListOf() private val servers = mutableListOf>() - + override var securityRequirements = mutableListOf() override var externalDocsDescription: String? = null override var externalDocsUrl: String? = null @@ -93,6 +95,18 @@ class RootBuilder( servers.add(serverBuilder) } + override fun security(key: String) { + securityRequirements.add(SecurityRequirement().addList(key)) + } + + override fun security(key: String, vararg scopes: String) { + securityRequirements.add(SecurityRequirement().addList(key, scopes.toList())) + } + + override fun security(builder: SecurityRequirementsBuilder.() -> Unit) { + securityRequirements.add(SecurityRequirementsBuilder().apply(builder).build()) + } + override fun build(): OpenAPI = OpenAPI().apply { tags = this@RootBuilder.tags.map { it.build() }.ifEmpty { null } // In case the info part is completely empty, output 'null' to avoid getting an empty, useless object. @@ -114,7 +128,7 @@ class RootBuilder( description = externalDocsDescription } } - + security = this@RootBuilder.securityRequirements.ifEmpty { null } servers = this@RootBuilder.servers.map { it.build() }.ifEmpty { null } } } diff --git a/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/SecurityDsl.kt b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/SecurityDsl.kt new file mode 100644 index 00000000..08ba44c6 --- /dev/null +++ b/tegral-openapi/tegral-openapi-dsl/src/main/kotlin/guru/zoroark/tegral/openapi/dsl/SecurityDsl.kt @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package guru.zoroark.tegral.openapi.dsl + +import guru.zoroark.tegral.core.Buildable +import guru.zoroark.tegral.core.TegralDsl +import io.swagger.v3.oas.models.security.SecurityRequirement + +/** + * DSL for the [security item object](https://spec.openapis.org/oas/v3.1.0#security-requirement-object). + * + * Can be defined at root level (see [RootDsl]) and operation level (see [OperationDsl]). + */ +@TegralDsl +interface SecurityDsl { + /** + * A declaration of which security mechanisms can be used for this operation. + * + * - This list behaves like an "OR", only one needs to be fulfilled for the operation. + * - Requirements defined in the individual `SecurityRequirement` objects behave like an "AND", and all of them need + * to be fulfilled. + */ + @TegralDsl + val securityRequirements: MutableList + + /** + * Adds a security requirement object to this operation with the given key. + */ + @TegralDsl + fun security(key: String) + + /** + * Adds a security requirement object to this operation with the given key and scopes. + */ + @TegralDsl + fun security(key: String, vararg scopes: String) + + /** + * Adds a security requirement object using the provided builder. + * Allows to define multiple requirements (which behave like an "AND", and all of them need to be fulfilled). + */ + @TegralDsl + fun security(builder: SecurityRequirementsBuilder.() -> Unit) +} + +/** + * Builder for [SecurityRequirement] + */ +class SecurityRequirementsBuilder : Buildable { + private val securityRequirement = SecurityRequirement() + + /** + * Adds a security requirement object with the given key. + */ + fun requirement(key: String) { + securityRequirement.addList(key) + } + + /** + * Adds a security requirement object with the given key and scopes. + */ + fun requirement(key: String, vararg scopes: String) { + securityRequirement.addList(key, scopes.asList()) + } + + override fun build(): SecurityRequirement = securityRequirement +} diff --git a/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/OperationBuilderTest.kt b/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/OperationBuilderTest.kt index f9b3224e..998f68ad 100644 --- a/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/OperationBuilderTest.kt +++ b/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/OperationBuilderTest.kt @@ -40,6 +40,10 @@ class OperationBuilderTest { operationId = "myOperation" security("sec-one") security("sec-two", "scope-a", "scope-b") + security { + requirement("sec-three", "scope-c") + requirement("sec-four", "scope-d") + } tags += "tag-alpha" "pathParam" pathParameter {} "headerParam" headerParameter {} @@ -64,6 +68,10 @@ class OperationBuilderTest { }, SecurityRequirement().apply { addList("sec-two", listOf("scope-a", "scope-b")) + }, + SecurityRequirement().apply { + addList("sec-three", listOf("scope-c")) + addList("sec-four", listOf("scope-d")) } ) tags = listOf("tag-alpha") diff --git a/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/PathsBuilderTest.kt b/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/PathsBuilderTest.kt index 36efab5b..d58104a2 100644 --- a/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/PathsBuilderTest.kt +++ b/tegral-openapi/tegral-openapi-dsl/src/test/kotlin/guru/zoroark/tegral/openapi/dsl/PathsBuilderTest.kt @@ -231,6 +231,7 @@ class PathsBuilderTest { assertEquals(expected, paths) } + @Suppress("LongMethod") @Test fun `Add everything via definition at path level`() { val paths = PathsBuilder(mockk()).apply { @@ -242,6 +243,8 @@ class PathsBuilderTest { operationId = "up" deprecated = true security("never") + security("gonna", "let") + security { requirement("you", "down") } 200 response { description = "gonna" } @@ -276,7 +279,7 @@ class PathsBuilderTest { assertNotNull(path.post) ) for (op in operations) { - assertEquals(1, op.security.size) + assertEquals(3, op.security.size) assertEquals("Never", op.summary) assertEquals("gonna", op.description) @@ -284,6 +287,8 @@ class PathsBuilderTest { assertEquals("you", op.externalDocs.url) assertEquals("up", op.operationId) assertEquals(emptyList(), op.security[0]["never"]) + assertEquals(listOf("let"), op.security[1]["gonna"]) + assertEquals(listOf("down"), op.security[2]["you"]) assertTrue(op.deprecated) assertEquals("gonna", op.responses["200"]?.description) assertEquals(4, op.parameters.size) diff --git a/tegral-openapi/tegral-openapi-ktor/api/tegral-openapi-ktor.api b/tegral-openapi/tegral-openapi-ktor/api/tegral-openapi-ktor.api index 06b0ac72..173e20f2 100644 --- a/tegral-openapi/tegral-openapi-ktor/api/tegral-openapi-ktor.api +++ b/tegral-openapi/tegral-openapi-ktor/api/tegral-openapi-ktor.api @@ -38,6 +38,7 @@ public final class guru/zoroark/tegral/openapi/ktor/TegralOpenApiKtor$Configurat public fun getLicenseIdentifier ()Ljava/lang/String; public fun getLicenseName ()Ljava/lang/String; public fun getLicenseUrl ()Ljava/lang/String; + public fun getSecurityRequirements ()Ljava/util/List; public fun getSummary ()Ljava/lang/String; public fun getTermsOfService ()Ljava/lang/String; public fun getTitle ()Ljava/lang/String; @@ -48,6 +49,9 @@ public final class guru/zoroark/tegral/openapi/ktor/TegralOpenApiKtor$Configurat public fun patch (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun post (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun put (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public fun security (Ljava/lang/String;)V + public fun security (Ljava/lang/String;[Ljava/lang/String;)V + public fun security (Lkotlin/jvm/functions/Function1;)V public fun securityScheme (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun server (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun setContactEmail (Ljava/lang/String;)V diff --git a/tegral-openapi/tegral-openapi-ktor/src/test/kotlin/guru/zoroark/tegral/openapi/ktor/PluginTest.kt b/tegral-openapi/tegral-openapi-ktor/src/test/kotlin/guru/zoroark/tegral/openapi/ktor/PluginTest.kt index 9084e83a..09a83208 100644 --- a/tegral-openapi/tegral-openapi-ktor/src/test/kotlin/guru/zoroark/tegral/openapi/ktor/PluginTest.kt +++ b/tegral-openapi/tegral-openapi-ktor/src/test/kotlin/guru/zoroark/tegral/openapi/ktor/PluginTest.kt @@ -18,6 +18,7 @@ import io.ktor.server.application.MissingApplicationPluginException import io.ktor.server.testing.testApplication import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.SecurityRequirement import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.Test import kotlin.test.assertEquals @@ -68,4 +69,30 @@ class PluginTest { } } } + + @Test + fun `Add security requirements on root`() { + testApplication { + install(TegralOpenApiKtor) { + security("scheme1") + security("scheme2", "scope1", "scope2") + security { + requirement("scheme3") + requirement("scheme4", "scope3") + } + } + + application { + val document = openApi.buildOpenApiDocument() + val expected = OpenAPI().apply { + security = listOf( + SecurityRequirement().addList("scheme1"), + SecurityRequirement().addList("scheme2", listOf("scope1", "scope2")), + SecurityRequirement().addList("scheme3").addList("scheme4", listOf("scope3")), + ) + } + assertEquals(expected, document) + } + } + } }