|
4 | 4 |
|
5 | 5 | package io.modelcontextprotocol.util; |
6 | 6 |
|
| 7 | +import java.util.List; |
| 8 | +import java.util.function.Consumer; |
| 9 | + |
| 10 | +import ch.qos.logback.classic.Level; |
| 11 | +import ch.qos.logback.classic.Logger; |
| 12 | +import ch.qos.logback.classic.spi.ILoggingEvent; |
| 13 | +import ch.qos.logback.core.read.ListAppender; |
| 14 | +import org.junit.jupiter.api.AfterEach; |
| 15 | +import org.junit.jupiter.api.BeforeEach; |
7 | 16 | import org.junit.jupiter.api.Test; |
8 | 17 | import org.junit.jupiter.params.ParameterizedTest; |
9 | 18 | import org.junit.jupiter.params.provider.ValueSource; |
| 19 | +import org.slf4j.LoggerFactory; |
10 | 20 |
|
| 21 | +import static org.assertj.core.api.Assertions.assertThat; |
11 | 22 | import static org.assertj.core.api.Assertions.assertThatCode; |
12 | 23 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
13 | 24 |
|
|
16 | 27 | */ |
17 | 28 | class ToolNameValidatorTests { |
18 | 29 |
|
| 30 | + private final Logger logger = (Logger) LoggerFactory.getLogger(ToolNameValidator.class); |
| 31 | + |
| 32 | + private final ListAppender<ILoggingEvent> logAppender = new ListAppender<>(); |
| 33 | + |
| 34 | + @BeforeEach |
| 35 | + void setUp() { |
| 36 | + logAppender.start(); |
| 37 | + logger.addAppender(logAppender); |
| 38 | + } |
| 39 | + |
| 40 | + @AfterEach |
| 41 | + void tearDown() { |
| 42 | + logger.detachAppender(logAppender); |
| 43 | + logAppender.stop(); |
| 44 | + } |
| 45 | + |
19 | 46 | @ParameterizedTest |
20 | 47 | @ValueSource(strings = { "getUser", "DATA_EXPORT_v2", "admin.tools.list", "my-tool", "Tool123", "a", "A", |
21 | 48 | "_private", "tool_name", "tool-name", "tool.name", "UPPERCASE", "lowercase", "MixedCase123" }) |
22 | | - void validToolNames_strictMode(String name) { |
| 49 | + void validToolNames(String name) { |
23 | 50 | assertThatCode(() -> ToolNameValidator.validate(name, true)).doesNotThrowAnyException(); |
| 51 | + ToolNameValidator.validate(name, false); |
| 52 | + assertThat(logAppender.list).isEmpty(); |
24 | 53 | } |
25 | 54 |
|
26 | 55 | @Test |
27 | | - void validToolName_maxLength() { |
| 56 | + void validToolNameMaxLength() { |
28 | 57 | String name = "a".repeat(128); |
29 | 58 | assertThatCode(() -> ToolNameValidator.validate(name, true)).doesNotThrowAnyException(); |
| 59 | + ToolNameValidator.validate(name, false); |
| 60 | + assertThat(logAppender.list).isEmpty(); |
30 | 61 | } |
31 | 62 |
|
32 | 63 | @Test |
33 | | - void invalidToolName_null_strictMode() { |
| 64 | + void nullOrEmpty() { |
34 | 65 | assertThatThrownBy(() -> ToolNameValidator.validate(null, true)).isInstanceOf(IllegalArgumentException.class) |
35 | 66 | .hasMessageContaining("null or empty"); |
36 | | - } |
37 | | - |
38 | | - @Test |
39 | | - void invalidToolName_empty_strictMode() { |
40 | 67 | assertThatThrownBy(() -> ToolNameValidator.validate("", true)).isInstanceOf(IllegalArgumentException.class) |
41 | 68 | .hasMessageContaining("null or empty"); |
42 | 69 | } |
43 | 70 |
|
44 | 71 | @Test |
45 | | - void invalidToolName_tooLong_strictMode() { |
| 72 | + void strictLength() { |
46 | 73 | String name = "a".repeat(129); |
47 | 74 | assertThatThrownBy(() -> ToolNameValidator.validate(name, true)).isInstanceOf(IllegalArgumentException.class) |
48 | 75 | .hasMessageContaining("128 characters"); |
@@ -79,18 +106,42 @@ void invalidToolName_tooLong_strictMode() { |
79 | 106 | "tööl", // non-ASCII |
80 | 107 | "工具" // unicode |
81 | 108 | }) |
82 | | - void invalidToolNames_specialCharacters_strictMode(String name) { |
| 109 | + void strictInvalidCharacters(String name) { |
83 | 110 | assertThatThrownBy(() -> ToolNameValidator.validate(name, true)).isInstanceOf(IllegalArgumentException.class) |
84 | 111 | .hasMessageContaining("invalid characters"); |
85 | 112 | } |
86 | 113 |
|
87 | 114 | @Test |
88 | | - void invalidToolName_nonStrictMode_doesNotThrow() { |
89 | | - // strict=false means warn only, should not throw |
90 | | - assertThatCode(() -> ToolNameValidator.validate("invalid name", false)).doesNotThrowAnyException(); |
| 115 | + void lenientNull() { |
91 | 116 | assertThatCode(() -> ToolNameValidator.validate(null, false)).doesNotThrowAnyException(); |
| 117 | + assertThat(logAppender.list).satisfies(hasWarning("null or empty")); |
| 118 | + } |
| 119 | + |
| 120 | + @Test |
| 121 | + void lenientEmpty() { |
92 | 122 | assertThatCode(() -> ToolNameValidator.validate("", false)).doesNotThrowAnyException(); |
| 123 | + assertThat(logAppender.list).satisfies(hasWarning("null or empty")); |
| 124 | + } |
| 125 | + |
| 126 | + @Test |
| 127 | + void lenientLength() { |
93 | 128 | assertThatCode(() -> ToolNameValidator.validate("a".repeat(129), false)).doesNotThrowAnyException(); |
| 129 | + assertThat(logAppender.list).satisfies(hasWarning("128 characters")); |
| 130 | + } |
| 131 | + |
| 132 | + @Test |
| 133 | + void lenientInvalidCharacters() { |
| 134 | + assertThatCode(() -> ToolNameValidator.validate("invalid name", false)).doesNotThrowAnyException(); |
| 135 | + assertThat(logAppender.list).satisfies(hasWarning("invalid characters")); |
| 136 | + } |
| 137 | + |
| 138 | + private Consumer<List<? extends ILoggingEvent>> hasWarning(String errorMessage) { |
| 139 | + return logs -> { |
| 140 | + assertThat(logs).hasSize(1).first().satisfies(log -> { |
| 141 | + assertThat(log.getLevel()).isEqualTo(Level.WARN); |
| 142 | + assertThat(log.getFormattedMessage()).contains(errorMessage); |
| 143 | + }); |
| 144 | + }; |
94 | 145 | } |
95 | 146 |
|
96 | 147 | } |
0 commit comments