Skip to content

Commit 3027d56

Browse files
committed
Added validation error on parsing enum values outside of valid enum values
1 parent 19e2b92 commit 3027d56

File tree

4 files changed

+221
-4
lines changed

4 files changed

+221
-4
lines changed

src/main/kotlin/com/papsign/ktor/openapigen/parameters/parsers/converters/primitive/EnumConverter.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.converters.primitive
22

3+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
34
import com.papsign.ktor.openapigen.parameters.parsers.converters.Converter
45
import com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelector
56
import kotlin.reflect.KType
67
import kotlin.reflect.jvm.jvmErasure
78

8-
class EnumConverter(type: KType): Converter {
9+
class EnumConverter(val type: KType) : Converter {
910

1011
private val enumMap = type.jvmErasure.java.enumConstants.associateBy { it.toString() }
1112

1213
override fun convert(value: String): Any? {
13-
return enumMap[value]
14+
if (enumMap.containsKey(value)) {
15+
return enumMap[value]
16+
} else {
17+
throw OpenAPIBadContentException(
18+
"Invalid value [$value] for enum parameter of type ${type.jvmErasure.simpleName}. Expected: [${
19+
enumMap.values.joinToString(
20+
","
21+
)
22+
}]"
23+
)
24+
}
1425
}
1526

16-
companion object: ConverterSelector {
27+
companion object : ConverterSelector {
1728
override fun canHandle(type: KType): Boolean {
1829
return type.jvmErasure.java.isEnum
1930
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package com.papsign.ktor.openapigen
2+
3+
import com.papsign.ktor.openapigen.annotations.Path
4+
import com.papsign.ktor.openapigen.annotations.parameters.QueryParam
5+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
6+
import com.papsign.ktor.openapigen.exceptions.OpenAPIRequiredFieldException
7+
import com.papsign.ktor.openapigen.route.apiRouting
8+
import com.papsign.ktor.openapigen.route.path.normal.get
9+
import com.papsign.ktor.openapigen.route.response.respond
10+
import io.ktor.application.*
11+
import io.ktor.features.*
12+
import io.ktor.http.*
13+
import io.ktor.response.*
14+
import io.ktor.server.testing.*
15+
import kotlin.test.*
16+
17+
enum class TestEnum {
18+
VALID,
19+
ALSO_VALID,
20+
}
21+
22+
@Path("/")
23+
data class NullableEnumParams(@QueryParam("") val type: TestEnum? = null)
24+
25+
@Path("/")
26+
data class NonNullableEnumParams(@QueryParam("") val type: TestEnum)
27+
28+
class EnumTestServer {
29+
30+
companion object {
31+
// test server for nullable enums
32+
private fun Application.nullableEnum() {
33+
install(OpenAPIGen)
34+
install(StatusPages) {
35+
exception<OpenAPIBadContentException> { e ->
36+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
37+
}
38+
}
39+
apiRouting {
40+
get<NullableEnumParams, String> { params ->
41+
if (params.type != null)
42+
assertTrue { TestEnum.values().contains(params.type) }
43+
respond(params.type?.toString() ?: "null")
44+
}
45+
}
46+
}
47+
48+
// test server for non-nullable enums
49+
private fun Application.nonNullableEnum() {
50+
install(OpenAPIGen)
51+
install(StatusPages) {
52+
exception<OpenAPIRequiredFieldException> { e ->
53+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
54+
}
55+
exception<OpenAPIBadContentException> { e ->
56+
call.respond(HttpStatusCode.BadRequest, e.localizedMessage)
57+
}
58+
}
59+
apiRouting {
60+
get<NonNullableEnumParams, String> { params ->
61+
assertTrue { TestEnum.values().contains(params.type) }
62+
respond(params.type.toString())
63+
}
64+
}
65+
}
66+
}
67+
68+
@Test
69+
fun `nullable enum could be omitted and it will be null`() {
70+
withTestApplication({ nullableEnum() }) {
71+
handleRequest(HttpMethod.Get, "/").apply {
72+
assertEquals(HttpStatusCode.OK, response.status())
73+
assertEquals("null", response.content)
74+
}
75+
}
76+
}
77+
78+
@Test
79+
fun `nullable enum should be parsed correctly`() {
80+
withTestApplication({ nullableEnum() }) {
81+
handleRequest(HttpMethod.Get, "/?type=VALID").apply {
82+
assertEquals(HttpStatusCode.OK, response.status())
83+
assertEquals("VALID", response.content)
84+
}
85+
handleRequest(HttpMethod.Get, "/?type=ALSO_VALID").apply {
86+
assertEquals(HttpStatusCode.OK, response.status())
87+
assertEquals("ALSO_VALID", response.content)
88+
}
89+
}
90+
}
91+
92+
@Test
93+
fun `nullable enum parsing should be case-sensitive and should throw on passing wrong case`() {
94+
withTestApplication({ nullableEnum() }) {
95+
handleRequest(HttpMethod.Get, "/?type=valid").apply {
96+
assertEquals(HttpStatusCode.BadRequest, response.status())
97+
assertEquals(
98+
"Invalid value [valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
99+
response.content
100+
)
101+
}
102+
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
103+
assertEquals(HttpStatusCode.BadRequest, response.status())
104+
assertEquals(
105+
"Invalid value [also_valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
106+
response.content
107+
)
108+
}
109+
}
110+
}
111+
112+
@Test
113+
fun `nullable enum parsing should not parse values outside of enum`() {
114+
withTestApplication({ nullableEnum() }) {
115+
handleRequest(HttpMethod.Get, "/?type=what").apply {
116+
assertEquals(HttpStatusCode.BadRequest, response.status())
117+
assertEquals(
118+
"Invalid value [what] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
119+
response.content
120+
)
121+
}
122+
}
123+
}
124+
125+
@Test
126+
fun `non-nullable enum cannot be omitted`() {
127+
withTestApplication({ nonNullableEnum() }) {
128+
handleRequest(HttpMethod.Get, "/").apply {
129+
assertEquals(HttpStatusCode.BadRequest, response.status())
130+
assertEquals("The field type is required", response.content)
131+
}
132+
}
133+
}
134+
135+
@Test
136+
fun `non-nullable enum should be parsed correctly`() {
137+
withTestApplication({ nonNullableEnum() }) {
138+
handleRequest(HttpMethod.Get, "/?type=VALID").apply {
139+
assertEquals(HttpStatusCode.OK, response.status())
140+
assertEquals("VALID", response.content)
141+
}
142+
handleRequest(HttpMethod.Get, "/?type=ALSO_VALID").apply {
143+
assertEquals(HttpStatusCode.OK, response.status())
144+
assertEquals("ALSO_VALID", response.content)
145+
}
146+
}
147+
}
148+
149+
@Test
150+
fun `non-nullable enum parsing should be case-sensitive and should throw on passing wrong case`() {
151+
withTestApplication({ nonNullableEnum() }) {
152+
handleRequest(HttpMethod.Get, "/?type=valid").apply {
153+
assertEquals(HttpStatusCode.BadRequest, response.status())
154+
assertEquals(
155+
"Invalid value [valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
156+
response.content
157+
)
158+
}
159+
handleRequest(HttpMethod.Get, "/?type=also_valid").apply {
160+
assertEquals(HttpStatusCode.BadRequest, response.status())
161+
assertEquals(
162+
"Invalid value [also_valid] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
163+
response.content
164+
)
165+
}
166+
}
167+
}
168+
169+
@Test
170+
fun `non-nullable enum parsing should not parse values outside of enum`() {
171+
withTestApplication({ nonNullableEnum() }) {
172+
handleRequest(HttpMethod.Get, "/?type=what").apply {
173+
assertEquals(HttpStatusCode.BadRequest, response.status())
174+
assertEquals(
175+
"Invalid value [what] for enum parameter of type TestEnum. Expected: [VALID,ALSO_VALID]",
176+
response.content
177+
)
178+
}
179+
}
180+
}
181+
}

src/test/kotlin/com/papsign/ktor/openapigen/parameters/parsers/builder/query/form/EnumBuilderTest.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.builder.query.form
22

3-
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.deepobject.DeepBuilderFactory
3+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
4+
import com.papsign.ktor.openapigen.getKType
45
import com.papsign.ktor.openapigen.parameters.parsers.builders.query.form.FormBuilderFactory
56
import com.papsign.ktor.openapigen.parameters.parsers.testSelector
67
import org.junit.Test
8+
import kotlin.test.assertFailsWith
9+
import kotlin.test.assertNotNull
710

811
class EnumBuilderTest {
912

@@ -20,4 +23,13 @@ class EnumBuilderTest {
2023
)
2124
FormBuilderFactory.testSelector(expected, key, parse, true)
2225
}
26+
27+
@Test
28+
fun `should throw on enum value outside of enum`() {
29+
val type = getKType<TestEnum>()
30+
val builder = assertNotNull(FormBuilderFactory.buildBuilder(type, true))
31+
assertFailsWith<OpenAPIBadContentException> {
32+
builder.build("key", mapOf("key" to listOf("XXX")))
33+
}
34+
}
2335
}

src/test/kotlin/com/papsign/ktor/openapigen/parameters/parsers/builders/query/deepobject/EnumBuilderTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.papsign.ktor.openapigen.parameters.parsers.builders.query.deepobject
22

3+
import com.papsign.ktor.openapigen.exceptions.OpenAPIBadContentException
4+
import com.papsign.ktor.openapigen.getKType
35
import com.papsign.ktor.openapigen.parameters.parsers.testSelector
46
import org.junit.Test
7+
import kotlin.test.assertFailsWith
8+
import kotlin.test.assertNotNull
59

610
class EnumBuilderTest {
711

@@ -18,4 +22,13 @@ class EnumBuilderTest {
1822
)
1923
DeepBuilderFactory.testSelector(expected, key, parse, true)
2024
}
25+
26+
@Test
27+
fun `should throw on enum value outside of enum`() {
28+
val type = getKType<TestEnum>()
29+
val builder = assertNotNull(DeepBuilderFactory.buildBuilder(type, true))
30+
assertFailsWith<OpenAPIBadContentException> {
31+
builder.build("key", mapOf("key" to listOf("XXX")))
32+
}
33+
}
2134
}

0 commit comments

Comments
 (0)