Skip to content

Commit f75d741

Browse files
Copilotvsct-jburet
andauthored
Add validation for reserved characters in FeatureType names and categories (#1991)
* Initial plan * Add validation for forbidden characters in feature names and categories Co-authored-by: vsct-jburet <12413454+vsct-jburet@users.noreply.github.com> * Add validation tests for enable and disable methods Co-authored-by: vsct-jburet <12413454+vsct-jburet@users.noreply.github.com> * Simplify validation tests as requested Co-authored-by: vsct-jburet <12413454+vsct-jburet@users.noreply.github.com> * Add missing comma validation test for category Co-authored-by: vsct-jburet <12413454+vsct-jburet@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vsct-jburet <12413454+vsct-jburet@users.noreply.github.com>
1 parent 66b269c commit f75d741

File tree

5 files changed

+154
-0
lines changed

5 files changed

+154
-0
lines changed

bot/engine/src/main/kotlin/engine/feature/FeatureType.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ package ai.tock.bot.engine.feature
1919
/**
2020
* A feature with a name and a category.
2121
* Usually implemented by an enum.
22+
*
23+
* **Important:** Feature names and categories must not contain the following characters:
24+
* - `+` (plus sign) - reserved for internal use as applicationId separator
25+
* - `,` (comma) - reserved for internal use as category/name separator
26+
*
27+
* Attempting to use these characters will result in an [IllegalArgumentException]
28+
* when the feature is added or modified.
2229
*/
2330
interface FeatureType {
2431
val name: String
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (C) 2017/2025 SNCF Connect & Tech
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package ai.tock.bot.engine.feature
18+
19+
/**
20+
* Characters that are forbidden in feature names and categories.
21+
* These characters are used internally as separators in the storage layer:
22+
* - '+' is used to separate the applicationId from the feature identifier
23+
* - ',' is used to separate the category from the name
24+
*/
25+
private val FORBIDDEN_CHARACTERS = setOf('+', ',')
26+
27+
/**
28+
* Validates that a feature name does not contain forbidden characters.
29+
*
30+
* @param name the feature name to validate
31+
* @throws IllegalArgumentException if the name contains forbidden characters
32+
*/
33+
fun validateFeatureName(name: String) {
34+
val invalidChars = name.filter { it in FORBIDDEN_CHARACTERS }
35+
if (invalidChars.isNotEmpty()) {
36+
throw IllegalArgumentException(
37+
"Feature name must not contain the following characters: ${FORBIDDEN_CHARACTERS.joinToString(", ")}. " +
38+
"Found: ${invalidChars.toSet().joinToString(", ")}",
39+
)
40+
}
41+
}
42+
43+
/**
44+
* Validates that a feature category does not contain forbidden characters.
45+
*
46+
* @param category the feature category to validate
47+
* @throws IllegalArgumentException if the category contains forbidden characters
48+
*/
49+
fun validateFeatureCategory(category: String) {
50+
val invalidChars = category.filter { it in FORBIDDEN_CHARACTERS }
51+
if (invalidChars.isNotEmpty()) {
52+
throw IllegalArgumentException(
53+
"Feature category must not contain the following characters: ${FORBIDDEN_CHARACTERS.joinToString(", ")}. " +
54+
"Found: ${invalidChars.toSet().joinToString(", ")}",
55+
)
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (C) 2017/2025 SNCF Connect & Tech
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package ai.tock.bot.engine.feature
18+
19+
import org.junit.jupiter.api.Test
20+
import org.junit.jupiter.api.assertThrows
21+
22+
class FeatureValidationTest {
23+
@Test
24+
fun `validateFeatureName accepts valid names`() {
25+
// Should not throw
26+
validateFeatureName("validName")
27+
validateFeatureName("valid_name")
28+
validateFeatureName("valid-name")
29+
}
30+
31+
@Test
32+
fun `validateFeatureName rejects forbidden characters`() {
33+
assertThrows<IllegalArgumentException> {
34+
validateFeatureName("invalid+name")
35+
}
36+
assertThrows<IllegalArgumentException> {
37+
validateFeatureName("invalid,name")
38+
}
39+
}
40+
41+
@Test
42+
fun `validateFeatureCategory accepts valid categories`() {
43+
// Should not throw
44+
validateFeatureCategory("valid.category")
45+
validateFeatureCategory("ai.tock.bot.Feature")
46+
}
47+
48+
@Test
49+
fun `validateFeatureCategory rejects forbidden characters`() {
50+
assertThrows<IllegalArgumentException> {
51+
validateFeatureCategory("invalid+category")
52+
}
53+
assertThrows<IllegalArgumentException> {
54+
validateFeatureCategory("invalid,category")
55+
}
56+
}
57+
}

bot/storage-mongo/src/main/kotlin/FeatureMongoDAO.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package ai.tock.bot.mongo
1818

1919
import ai.tock.bot.engine.feature.FeatureDAO
2020
import ai.tock.bot.engine.feature.FeatureState
21+
import ai.tock.bot.engine.feature.validateFeatureCategory
22+
import ai.tock.bot.engine.feature.validateFeatureName
2123
import ai.tock.bot.mongo.Feature_.Companion.BotId
2224
import ai.tock.bot.mongo.Feature_.Companion.Namespace
2325
import ai.tock.bot.mongo.Feature_.Companion._id
@@ -160,6 +162,8 @@ internal class FeatureMongoDAO(
160162
applicationId: String?,
161163
graduation: Int?,
162164
) {
165+
validateFeatureCategory(category)
166+
validateFeatureName(name)
163167
val id = calculateId(botId, namespace, category, name, applicationId)
164168
val feature = Feature(id, "$category,$name", true, botId, namespace, startDate, endDate, graduation)
165169

@@ -173,6 +177,8 @@ internal class FeatureMongoDAO(
173177
name: String,
174178
applicationId: String?,
175179
) {
180+
validateFeatureCategory(category)
181+
validateFeatureName(name)
176182
val id = calculateId(botId, namespace, category, name, applicationId)
177183
val feature = Feature(id, "$category,$name", false, botId, namespace)
178184
col.save(feature)
@@ -209,6 +215,8 @@ internal class FeatureMongoDAO(
209215
applicationId: String?,
210216
graduation: Int?,
211217
) {
218+
validateFeatureCategory(category)
219+
validateFeatureName(name)
212220
val id = calculateId(botId, namespace, category, name, applicationId)
213221
val feature = Feature(id, "$category,$name", enabled, botId, namespace, startDate, endDate, graduation)
214222
col.save(feature)

bot/storage-mongo/src/test/kotlin/FeatureMongoDAOTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.BeforeEach
3838
import org.junit.jupiter.api.DisplayName
3939
import org.junit.jupiter.api.Nested
4040
import org.junit.jupiter.api.Test
41+
import org.junit.jupiter.api.assertThrows
4142
import org.litote.kmongo.coroutine.CoroutineCollection
4243
import org.litote.kmongo.coroutine.CoroutineFindPublisher
4344
import org.litote.kmongo.coroutine.coroutine
@@ -314,6 +315,30 @@ internal class FeatureMongoDAOTest {
314315
`assert that feature is persisted with`(id, false)
315316
}
316317
}
318+
319+
@Test
320+
fun `addFeature rejects forbidden characters in name`() {
321+
runBlocking {
322+
assertThrows<IllegalArgumentException> {
323+
featureDAO.addFeature(botId, namespace, true, "category", "name+invalid", null, null)
324+
}
325+
assertThrows<IllegalArgumentException> {
326+
featureDAO.addFeature(botId, namespace, true, "category", "name,invalid", null, null)
327+
}
328+
}
329+
}
330+
331+
@Test
332+
fun `addFeature rejects forbidden characters in category`() {
333+
runBlocking {
334+
assertThrows<IllegalArgumentException> {
335+
featureDAO.addFeature(botId, namespace, true, "category+invalid", "name", null, null)
336+
}
337+
assertThrows<IllegalArgumentException> {
338+
featureDAO.addFeature(botId, namespace, true, "category,invalid", "name", null, null)
339+
}
340+
}
341+
}
317342
}
318343

319344
@Test

0 commit comments

Comments
 (0)