refactor: massive rename dap -> dat and fix scaffold syntax error
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package io.shinhanlife.dat.lib.adapter.test;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MockEimsHttpServerTest {
|
||||
|
||||
@Test
|
||||
void returnsTheScaffoldGeneratedJsonResponse() {
|
||||
MockEimsHttpServer server = new MockEimsHttpServer(new ObjectMapper());
|
||||
|
||||
var response = server.mockToolHttpResponse("cmm_memo_retriever", null);
|
||||
|
||||
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(response.getBody().path("resultCode").asText()).isEqualTo("SUCCESS");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.shinhanlife.dat.lib.common.adapter.sender;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.integration.mci.dto.MciRequestWrapper;
|
||||
import io.shinhanlife.dat.lib.integration.mci.dto.ShinhanCommonHeaderDto;
|
||||
import lombok.Data;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.lib.adapter.sender
|
||||
* @className ShinhanMciSenderTest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
class ShinhanMciSenderTest {
|
||||
|
||||
@Data
|
||||
static class SampleBody {
|
||||
private String msgCd;
|
||||
private String anxMsgCt;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJsonUnwrapped() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
ShinhanCommonHeaderDto header = new ShinhanCommonHeaderDto();
|
||||
header.setGlbId("20211115150135679009808815NCS17007887");
|
||||
header.setPgrsSriaNo("002");
|
||||
header.setItrIfId("NCSBACO00001");
|
||||
|
||||
SampleBody body = new SampleBody();
|
||||
body.setMsgCd("12345");
|
||||
body.setAnxMsgCt("Test Message");
|
||||
|
||||
MciRequestWrapper<SampleBody> wrapper = new MciRequestWrapper<>();
|
||||
wrapper.setTgrmCmnnhddValu(header);
|
||||
wrapper.setBody(body);
|
||||
|
||||
String json = mapper.writeValueAsString(wrapper);
|
||||
|
||||
System.out.println(json);
|
||||
|
||||
// 검증: body 필드가 json root 레벨에 평탄화되어 있는지 확인
|
||||
assertThat(json).contains("\"tgrmCmnnhddValu\":{");
|
||||
assertThat(json).contains("\"msgCd\":\"12345\"");
|
||||
assertThat(json).contains("\"anxMsgCt\":\"Test Message\"");
|
||||
assertThat(json).doesNotContain("\"body\":");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.shinhanlife.dat.lib.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
class AxhubHttpConfigurationTest {
|
||||
|
||||
@Test
|
||||
void registersGlowHttpComponentFromDapLibConfiguration() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class)) {
|
||||
assertThat(context.getBean(GlowHttpComponent.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(AxhubHttpConfiguration.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
RestClient.Builder restClientBuilder() {
|
||||
return RestClient.builder();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.shinhanlife.dat.lib.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.validation.ToolArgumentSchemaValidator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
class ToolSchemaConfigurationTest {
|
||||
|
||||
@Test
|
||||
void providesToolArgumentSchemaValidatorWithoutComponentScanning() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.registerBean(ObjectMapper.class);
|
||||
context.register(ToolSchemaConfiguration.class);
|
||||
context.refresh();
|
||||
|
||||
assertNotNull(context.getBean(ToolArgumentSchemaValidator.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package io.shinhanlife.dat.lib.integration.http.component;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.dat.lib.mcp.McpRequestHeaderContext;
|
||||
import io.shinhanlife.dat.lib.mcp.McpRequestHeaders;
|
||||
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
class AxhubHttpComponentTest {
|
||||
|
||||
@Test
|
||||
void callByApiNameBuildsGlowTransferAndDeserializesJsonResponse() {
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
|
||||
GlowHttpComponent glowHttpComponent = new GlowHttpComponent(builder);
|
||||
AxhubHttpProperties properties = new AxhubHttpProperties();
|
||||
properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition(
|
||||
"status", "https://api.example.test", "/v1", HttpMethod.GET, "application/json", false)));
|
||||
AxhubHttpComponent component = new AxhubHttpComponent(
|
||||
glowHttpComponent, new ObjectMapper(), new GlowCommunicationProperties(), properties);
|
||||
|
||||
server.expect(requestTo("https://api.example.test/v1/status"))
|
||||
.andExpect(header("X-ANONYMOUS-REQ", "AXHUB-TOOL"))
|
||||
.andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON));
|
||||
|
||||
SampleResponse response = component.call("status", "/status", null, SampleResponse.class);
|
||||
|
||||
assertThat(response.status()).isEqualTo("OK");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void callByApiNameUsesConfiguredUrlMethodContentTypeAndBizPodHeader() {
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
|
||||
GlowHttpComponent glowHttpComponent = new GlowHttpComponent(builder);
|
||||
AxhubHttpProperties properties = new AxhubHttpProperties();
|
||||
properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition(
|
||||
"employee", "https://employee.example.test", "/itrf/employee", HttpMethod.POST,
|
||||
"application/json;charset=UTF-8", true)));
|
||||
AxhubHttpComponent component = new AxhubHttpComponent(
|
||||
glowHttpComponent, new ObjectMapper(), new GlowCommunicationProperties(), properties);
|
||||
|
||||
server.expect(requestTo("https://employee.example.test/itrf/employee"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("Content-Type", "application/json;charset=UTF-8"))
|
||||
.andExpect(header("X-POD-TO-POD", "true"))
|
||||
.andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON));
|
||||
|
||||
SampleResponse response = component.call("employee", "{\"employeeId\":\"EMP10001\"}", SampleResponse.class);
|
||||
|
||||
assertThat(response.status()).isEqualTo("OK");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardsDapmsHeadersToTheConfiguredHttpService() throws Exception {
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
|
||||
AxhubHttpProperties properties = new AxhubHttpProperties();
|
||||
properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition(
|
||||
"status", "https://api.example.test", "/v1", HttpMethod.POST, "application/json", false)));
|
||||
AxhubHttpComponent component = new AxhubHttpComponent(
|
||||
new GlowHttpComponent(builder), new ObjectMapper(), new GlowCommunicationProperties(), properties);
|
||||
|
||||
server.expect(requestTo("https://api.example.test/v1"))
|
||||
.andExpect(header("x-request-id", "request-1"))
|
||||
.andExpect(header("guid", "guid-1"))
|
||||
.andExpect(header("mcp-session-id", "session-1"))
|
||||
.andExpect(header("employee-no", "ENC(employee)"))
|
||||
.andExpect(header("virtual-employee-no", "ENC(virtual)"))
|
||||
.andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON));
|
||||
|
||||
setRequestHeaders(headers(Map.of(
|
||||
"requestId", "request-1",
|
||||
"guid", "guid-1",
|
||||
"mcpSessionId", "session-1",
|
||||
"employeeNo", "ENC(employee)",
|
||||
"virtualEmployeeNo", "ENC(virtual)",
|
||||
"headerRequestId", "request-1",
|
||||
"traceId", "guid-1",
|
||||
"encryptedEmployeeId", "ENC(employee)")));
|
||||
try {
|
||||
assertThat(component.call("status", Map.of(), SampleResponse.class).status()).isEqualTo("OK");
|
||||
server.verify();
|
||||
} finally {
|
||||
clearRequestHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
private McpRequestHeaders headers(Map<String, String> values) {
|
||||
try {
|
||||
Class<?>[] types = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||
.map(component -> component.getType())
|
||||
.toArray(Class<?>[]::new);
|
||||
Object[] arguments = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||
.map(component -> values.get(component.getName()))
|
||||
.toArray();
|
||||
return McpRequestHeaders.class.getDeclaredConstructor(types).newInstance(arguments);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void setRequestHeaders(McpRequestHeaders headers) throws Exception {
|
||||
Method method = McpRequestHeaderContext.class.getDeclaredMethod("set", McpRequestHeaders.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(null, headers);
|
||||
}
|
||||
|
||||
private void clearRequestHeaders() throws Exception {
|
||||
Method method = McpRequestHeaderContext.class.getDeclaredMethod("clear");
|
||||
method.setAccessible(true);
|
||||
method.invoke(null);
|
||||
}
|
||||
|
||||
record SampleResponse(String status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class McpSdk2CompatibilityTest {
|
||||
|
||||
@Test
|
||||
void usesMcpJavaSdkTwo() {
|
||||
assertEquals("2.0.0", McpSchema.class.getPackage().getImplementationVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesJsonSchemaWithTheNetworkntVersionRequiredByMcpSdkTwo() {
|
||||
DefaultJsonSchemaValidator validator = new DefaultJsonSchemaValidator();
|
||||
|
||||
var response = validator.validateSchema(Map.of(
|
||||
"$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12,
|
||||
"type", "object",
|
||||
"properties", Map.of("employeeId", Map.of("type", "string"))));
|
||||
|
||||
assertTrue(response.valid(), response.errorMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import io.shinhanlife.dat.lib.config.McpProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
|
||||
class McpToolMethodRegistryTest {
|
||||
|
||||
@Test
|
||||
void indexesAnnotatedToolDuringInitialization() {
|
||||
StaticApplicationContext context = contextWith(new EchoTool());
|
||||
McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties());
|
||||
|
||||
registry.initialize();
|
||||
|
||||
McpToolMethodRegistry.RegisteredTool tool = registry.find("cmm_echo_search");
|
||||
assertNotNull(tool);
|
||||
assertEquals("execute", tool.method().getName());
|
||||
assertEquals("cmm_echo_search", tool.annotation().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateToolNamesDuringInitialization() {
|
||||
StaticApplicationContext context = contextWith(new EchoTool(), new DuplicateEchoTool());
|
||||
McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties());
|
||||
|
||||
assertThrows(IllegalStateException.class, registry::initialize);
|
||||
}
|
||||
|
||||
private StaticApplicationContext contextWith(Object... tools) {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
for (int index = 0; index < tools.length; index++) {
|
||||
context.getBeanFactory().registerSingleton("tool" + index, tools[index]);
|
||||
}
|
||||
context.refresh();
|
||||
return context;
|
||||
}
|
||||
|
||||
static class EchoTool {
|
||||
@McpTool(name = "cmm_echo_search")
|
||||
public String execute(String request) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
static class DuplicateEchoTool {
|
||||
@McpTool(name = "cmm_echo_search")
|
||||
public String execute(String request) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ToolMetadataMcpMapperTest {
|
||||
|
||||
@Test
|
||||
void includesUsageGuidanceInMcpMeta() {
|
||||
ToolMetadata metadata = new ToolMetadata();
|
||||
metadata.setWhenToUse("사용 시점");
|
||||
metadata.setWhenNotToUse("사용 제외");
|
||||
metadata.setIoLimits("입출력 제한");
|
||||
|
||||
assertThat(ToolMetadataMcpMapper.meta(metadata)).containsEntry("when_to_use", "사용 시점")
|
||||
.containsEntry("when_not_to_use", "사용 제외")
|
||||
.containsEntry("io_limits", "입출력 제한");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
|
||||
import io.shinhanlife.dat.lib.config.McpProperties;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinitionRepository;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDescription;
|
||||
import io.shinhanlife.dat.lib.util.ToolSchemaResolver;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
class ToolRegistryHeartbeatSenderTest {
|
||||
|
||||
@Test
|
||||
void excludesRegisterFalseToolFromGatewayRegistrationTargets() throws Exception {
|
||||
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
||||
when(applicationContext.getBeansOfType(Object.class)).thenReturn(Map.of("disabledTool", new DisabledTool()));
|
||||
ToolRegistryHeartbeatSender sender = new ToolRegistryHeartbeatSender(
|
||||
applicationContext, new ObjectMapper(), new McpProperties(), mock(ToolSchemaResolver.class));
|
||||
|
||||
setField(sender, "podUrl", "http://localhost:8084");
|
||||
|
||||
sender.init();
|
||||
|
||||
assertThat(sender.getAllScannedTools()).singleElement()
|
||||
.extracting(tool -> tool.getIsRegistered())
|
||||
.isEqualTo(false);
|
||||
assertThat(registeredTools(sender)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsOnlyFunctionDescriptionAndStoresUsageGuidanceSeparately() throws Exception {
|
||||
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
||||
when(applicationContext.getBeansOfType(Object.class)).thenReturn(Map.of("tool", new EnabledTool()));
|
||||
ToolDefinitionRepository definitions = mock(ToolDefinitionRepository.class);
|
||||
when(definitions.findByName("test_enabled_tool")).thenReturn(java.util.Optional.of(new ToolDefinition(
|
||||
"test_enabled_tool", "테스트 도구", "1.0.0", "test",
|
||||
new ToolDescription("기능 설명", "사용 시점", "사용 제외", "입출력 제한"),
|
||||
"도구 설명", List.of("질문 1", "질문 2", "질문 3"), true, false, true,
|
||||
Map.of("type", "object", "properties", Map.of(), "additionalProperties", false), null,
|
||||
List.of("test"), null, List.of(), "MCP_TOOL")));
|
||||
ToolRegistryHeartbeatSender sender = new ToolRegistryHeartbeatSender(applicationContext, new ObjectMapper(),
|
||||
new McpProperties(), mock(ToolSchemaResolver.class), definitions);
|
||||
|
||||
sender.init();
|
||||
|
||||
assertThat(sender.getAllScannedTools()).singleElement().satisfies(tool -> {
|
||||
assertThat(tool.getDescription()).isEqualTo("기능 설명");
|
||||
assertThat(tool.getWhenToUse()).isEqualTo("사용 시점");
|
||||
assertThat(tool.getWhenNotToUse()).isEqualTo("사용 제외");
|
||||
assertThat(tool.getIoLimits()).isEqualTo("입출력 제한");
|
||||
});
|
||||
}
|
||||
|
||||
private void setField(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<?> registeredTools(ToolRegistryHeartbeatSender sender) throws Exception {
|
||||
Field field = ToolRegistryHeartbeatSender.class.getDeclaredField("registeredTools");
|
||||
field.setAccessible(true);
|
||||
return (List<?>) field.get(sender);
|
||||
}
|
||||
|
||||
static class DisabledTool {
|
||||
|
||||
@McpTool(name = "test_disabled_tool")
|
||||
@GrowToolHint
|
||||
void execute() {
|
||||
}
|
||||
}
|
||||
|
||||
static class EnabledTool {
|
||||
@McpTool(name = "test_enabled_tool")
|
||||
void execute() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.shinhanlife.dat.lib.metadata;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
class ToolDefinitionRepositoryTest {
|
||||
|
||||
@Test
|
||||
void springCanCreateRepositoryWithoutDefaultConstructor() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.register(ToolDefinitionRepository.class);
|
||||
context.refresh();
|
||||
|
||||
assertTrue(context.getBean(ToolDefinitionRepository.class)
|
||||
.findByName("cmm_claim_search").isPresent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsAndCachesAValidV17DefinitionByToolName() {
|
||||
ToolDefinitionRepository repository = new ToolDefinitionRepository(
|
||||
new ObjectMapper(new YAMLFactory()), new DefaultResourceLoader(),
|
||||
"classpath*:tool-definitions/**/*.yml");
|
||||
|
||||
ToolDefinition definition = repository.findByName("cmm_claim_search").orElseThrow();
|
||||
|
||||
assertEquals("보험금 청구 상태 조회", definition.displayName());
|
||||
assertEquals("cmm", definition.categoryKey());
|
||||
assertEquals(3, definition.exampleQueries().size());
|
||||
assertEquals(false, definition.parametersSchema().get("additionalProperties"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDefinitionWithFewerThanThreeExampleQueries() {
|
||||
ToolDefinition invalid = validDefinition().withExampleQueries(java.util.List.of("청구 상태 알려줘"));
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> ToolDefinitionValidator.validate(invalid, "memory:invalid"));
|
||||
|
||||
assertTrue(error.getMessage().contains("example_queries"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonStandardToolName() {
|
||||
ToolDefinition invalid = validDefinition().withName("cmm_claim.Search");
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> ToolDefinitionValidator.validate(invalid, "memory:invalid"));
|
||||
|
||||
assertTrue(error.getMessage().contains("name"));
|
||||
}
|
||||
|
||||
private ToolDefinition validDefinition() {
|
||||
return new ToolDefinition(
|
||||
"cmm_claim_search", "보험금 청구 상태 조회", "1.0.0", "cmm",
|
||||
new ToolDescription("청구 상태를 조회한다.", "상태 확인 시 사용한다.",
|
||||
"청구 접수 시 사용하지 않는다.", "청구번호가 필요하다."),
|
||||
"보험금 청구 상태를 조회합니다.",
|
||||
java.util.List.of("청구 상태 알려줘", "심사 결과 조회해줘", "계약번호로 청구를 찾아줘"),
|
||||
true, false, true,
|
||||
java.util.Map.of("type", "object", "properties", java.util.Map.of(),
|
||||
"additionalProperties", false),
|
||||
null,
|
||||
java.util.List.of("보험금"), null, java.util.List.of(), "MCP_TOOL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
|
||||
import org.springaicommunity.mcp.annotation.McpToolParam;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class JsonSchemaGeneratorTest {
|
||||
|
||||
@Test
|
||||
void includesMcpParameterConstraintsInGeneratedSchema() {
|
||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class);
|
||||
Map<String, Map<String, Object>> properties = properties(schema);
|
||||
|
||||
assertTrue(((List<String>) schema.get("required")).contains("phoneNumber"));
|
||||
assertTrue(((List<String>) schema.get("required")).contains("approvalStatus"));
|
||||
assertEquals("^01[0-9]{8,9}$", properties.get("phoneNumber").get("pattern"));
|
||||
assertEquals(1L, properties.get("amount").get("minimum"));
|
||||
assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesExtendedMcpValidationConstraintsInGeneratedSchema() {
|
||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class);
|
||||
|
||||
assertEquals(50L, property(schema, "pageSize").get("maximum"));
|
||||
assertEquals(20L, property(schema, "pageSize").get("default"));
|
||||
assertEquals(1, property(schema, "reference").get("minLength"));
|
||||
assertEquals(30, property(schema, "reference").get("maxLength"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesNestedDtoConstraintsInGeneratedSchema() {
|
||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
|
||||
Map<String, Object> childSchema = property(schema, "child");
|
||||
|
||||
assertEquals("object", childSchema.get("type"));
|
||||
assertTrue(required(childSchema).contains("businessDate"));
|
||||
assertEquals("^\\\\d{8}$", property(childSchema, "businessDate").get("pattern"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void includesNestedDtoSchemaForListItems() {
|
||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ListRequest.class);
|
||||
Map<String, Object> itemSchema = map(property(schema, "items").get("items"));
|
||||
|
||||
assertEquals("object", itemSchema.get("type"));
|
||||
assertTrue(required(itemSchema).contains("businessDate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatorRejectsInvalidNestedValue() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonSchemaValidator validator = new DefaultJsonSchemaValidator();
|
||||
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
|
||||
Object arguments = objectMapper.convertValue(
|
||||
Map.of("child", Map.of("businessDate", "2026-07-28")), Object.class);
|
||||
JsonSchemaValidator.ValidationResponse result = validator.validate(schema, arguments);
|
||||
|
||||
assertFalse(result.valid());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
|
||||
return (Map<String, Map<String, Object>>) schema.get("properties");
|
||||
}
|
||||
|
||||
private Map<String, Object> property(Map<String, Object> schema, String name) {
|
||||
return properties(schema).get(name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> map(Object value) {
|
||||
return (Map<String, Object>) value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> required(Map<String, Object> schema) {
|
||||
return (List<String>) schema.get("required");
|
||||
}
|
||||
|
||||
private static class ValidatedRequest {
|
||||
@McpToolParam(description = "recipient phone number", required = true)
|
||||
@io.swagger.v3.oas.annotations.media.Schema(pattern = "^01[0-9]{8,9}$")
|
||||
private String phoneNumber;
|
||||
|
||||
@McpToolParam(description = "issue amount", required = true)
|
||||
@io.swagger.v3.oas.annotations.media.Schema(minimum = "1")
|
||||
private Long amount;
|
||||
|
||||
@McpToolParam(description = "approval result")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(
|
||||
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"APPROVE", "REJECT"})
|
||||
private String approvalStatus;
|
||||
|
||||
@McpToolParam(description = "page size")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(maximum = "50", defaultValue = "20")
|
||||
private Integer pageSize;
|
||||
|
||||
@McpToolParam(description = "reference")
|
||||
@io.swagger.v3.oas.annotations.media.Schema(minLength = 1, maxLength = 30)
|
||||
private String reference;
|
||||
}
|
||||
|
||||
private static class NestedRequest {
|
||||
private NestedChild child;
|
||||
}
|
||||
|
||||
private static class ListRequest {
|
||||
private List<NestedChild> items;
|
||||
}
|
||||
|
||||
private static class NestedChild {
|
||||
@io.swagger.v3.oas.annotations.media.Schema(
|
||||
requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED,
|
||||
pattern = "^\\\\d{8}$")
|
||||
private String businessDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class PodScaffolderTest {
|
||||
|
||||
@TempDir
|
||||
Path root;
|
||||
|
||||
@Test
|
||||
void generatesBuildGradleUsingRepositoryStandard() throws Exception {
|
||||
PodScaffolder.scaffoldPod(root, "dat-was-sample", "8099", "sample", "tester", "2026.08.07");
|
||||
|
||||
String buildGradle = Files.readString(root.resolve("dat-was-sample/build.gradle"));
|
||||
assertTrue(buildGradle.contains("id 'org.springframework.boot'"));
|
||||
assertTrue(buildGradle.contains("Spring Boot 3.5.11"));
|
||||
assertTrue(buildGradle.contains("implementation project(':dat-was-lib')"));
|
||||
assertFalse(buildGradle.contains("compileOnly 'org.projectlombok:lombok"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesOthStandardRuntimeResources() throws Exception {
|
||||
PodScaffolder.scaffoldPod(root, "dat-was-sample", "8099", "sample", "tester", "2026.08.07");
|
||||
|
||||
Path resources = root.resolve("dat-was-sample/src/main/resources");
|
||||
String dockerfile = Files.readString(root.resolve("dat-was-sample/Dockerfile"));
|
||||
|
||||
assertTrue(dockerfile.contains("RUN apk add --no-cache tzdata"));
|
||||
assertTrue(dockerfile.contains("ENV TZ=Asia/Seoul"));
|
||||
assertTrue(dockerfile.contains("COPY dat-was-sample/build/libs/*-SNAPSHOT.jar app.jar"));
|
||||
assertTrue(dockerfile.contains("EXPOSE 8099"));
|
||||
assertTrue(Files.exists(resources.resolve("application-test.yml")));
|
||||
assertTrue(Files.exists(resources.resolve("application-prod.yml")));
|
||||
assertTrue(Files.readString(resources.resolve("application-test.yml"))
|
||||
.contains("${AXHUB_GATEWAY_URL}"));
|
||||
assertTrue(Files.readString(resources.resolve("application-prod.yml"))
|
||||
.contains("${AXHUB_TOOL_URL}"));
|
||||
assertTrue(Files.readString(resources.resolve("application-local.yml"))
|
||||
.contains("classpath:glow/application-glow-local.yml"));
|
||||
assertTrue(Files.readString(resources.resolve("application-dev.yml"))
|
||||
.contains("classpath:glow/application-glow-dev.yml"));
|
||||
assertTrue(Files.readString(resources.resolve("application-test.yml"))
|
||||
.contains("classpath:glow/application-glow-test.yml"));
|
||||
assertTrue(Files.readString(resources.resolve("application-prod.yml"))
|
||||
.contains("classpath:glow/application-glow-prod.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesRedisOptionalPodComposeConfiguration() throws Exception {
|
||||
Files.writeString(root.resolve("settings.gradle"), "rootProject.name = 'test'\n");
|
||||
Files.writeString(root.resolve("docker-compose.yml"), "services:\n");
|
||||
|
||||
PodScaffolder.scaffoldPod(root, "dat-was-pro", "8085", "pro", "tester", "2026.08.13");
|
||||
|
||||
String compose = Files.readString(root.resolve("docker-compose.yml"));
|
||||
assertTrue(compose.contains("was-pro:"));
|
||||
assertFalse(compose.contains("depends_on:\n - redis"));
|
||||
assertFalse(compose.contains("SPRING_REDIS_HOST=redis"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinitionValidator;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ToolScaffolderTest {
|
||||
|
||||
@Test
|
||||
void exposesGroupedUseCaseScaffoldApi() {
|
||||
assertDoesNotThrow(() -> ToolScaffolder.class.getMethod(
|
||||
"scaffoldUseCase", String.class, String.class, String.class, String.class, List.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesOneUseCaseWithTwoMcpToolMethodsAndTypedClients() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-customer").toString();
|
||||
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.12", List.of(
|
||||
new ToolScaffolder.ToolMethodDefinition(
|
||||
"CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI",
|
||||
false, "NILD", null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)),
|
||||
List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", List.of("OPEN"), "", false)), null),
|
||||
new ToolScaffolder.ToolMethodDefinition(
|
||||
"CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI",
|
||||
false, "CNTD", null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)),
|
||||
List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", List.of("ACTIVE"), "", false)), null)));
|
||||
|
||||
Path sourceRoot = root.resolve("dat-was-customer/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
|
||||
String implementation = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
|
||||
String guidanceClient = Files.readString(sourceRoot.resolve("infra/itrf/mci/nild/CustomerGuidanceClient.java"));
|
||||
|
||||
assertTrue(useCase.contains("CustomerGuidanceResponse searchGuidance(CustomerGuidanceRequest req)"), useCase);
|
||||
assertTrue(useCase.contains("CustomerContractResponse searchContract(CustomerContractRequest req)"), useCase);
|
||||
assertTrue(implementation.contains("private final CustomerGuidanceClient customerGuidanceClient;"), implementation);
|
||||
assertTrue(implementation.contains("customerGuidanceClient.callCustomerGuidance(request)"), implementation);
|
||||
assertTrue(guidanceClient.contains("CustomerGuidance_O callCustomerGuidance(CustomerGuidance_I request)"), guidanceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupedHttpToolRegistersItsGlowApiCatalogEntry() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-http").toString();
|
||||
|
||||
ToolScaffolder.scaffoldUseCase("Employee", moduleName, "tester", "2026.08.13", List.of(
|
||||
new ToolScaffolder.ToolMethodDefinition(
|
||||
"EmployeeSearch", "searchEmployee", null, "Employee search", "Search employee", "smp", "HTTP",
|
||||
false, null, "employee-search",
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", List.of("10001"), "", true)),
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong"), "", false)), null)));
|
||||
|
||||
Path glowConfig = root.resolve("dat-was-http/src/main/resources/glow/application-glow-local.yml");
|
||||
assertTrue(Files.exists(glowConfig));
|
||||
assertTrue(Files.readString(glowConfig).contains("- name: employee-search"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-claim").toString();
|
||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("claimStatus", "Enum", "Claim status", List.of("OPEN"), "", true,
|
||||
List.of("OPEN", "CLOSED"), null, List.of()),
|
||||
new ToolScaffolder.FieldDefinition("customerIds", "List", "Customer IDs", List.of("C001"), "", false,
|
||||
List.of(), "String", List.of()));
|
||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", List.of(), "", false,
|
||||
List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", List.of("OPEN"), "", true))));
|
||||
|
||||
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
||||
"tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields);
|
||||
|
||||
Path dtoRoot = root.resolve("dat-was-claim/src/main/java/io/shinhanlife/dat/mcc/biz/cmm/dto");
|
||||
String request = Files.readString(dtoRoot.resolve("ClaimSearchRequest.java"));
|
||||
String response = Files.readString(dtoRoot.resolve("ClaimSearchResponse.java"));
|
||||
String definition = Files.readString(root.resolve("dat-was-claim/src/main/resources/tool-definitions/cmm/cmm_claim_search.yml"));
|
||||
String mock = Files.readString(root.resolve("dat-was-claim/src/main/resources/mock-responses/cmm_claim_search.json"));
|
||||
|
||||
assertTrue(request.contains("private ClaimStatus claimStatus;"), request);
|
||||
assertTrue(request.contains("private List<String> customerIds;"), request);
|
||||
assertTrue(Files.exists(dtoRoot.resolve("ClaimStatus.java")));
|
||||
assertTrue(response.contains("private List<GuidanceItemsItem> guidanceItems;"), response);
|
||||
assertTrue(response.contains("public static class GuidanceItemsItem"), response);
|
||||
assertFalse(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java")));
|
||||
assertTrue(definition.contains("enum: [OPEN, CLOSED]"), definition);
|
||||
assertTrue(definition.contains("type: array"), definition);
|
||||
assertTrue(mock.contains("\"guidanceItems\" : [{"), mock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesEveryToolSourceAsUtf8WithoutBrokenKoreanOrBom() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-korean").toString();
|
||||
|
||||
ToolScaffolder.scaffold("analysis data query", "CLYMCI00001", "분석 데이터 조회",
|
||||
"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.",
|
||||
"cmm", "MCI", moduleName, "테스터", "2026.08.12",
|
||||
false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", List.of("계약 분석"), "", true)),
|
||||
List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", List.of("정상"), "", false)));
|
||||
|
||||
Path sourceRoot = root.resolve("dat-was-korean/src/main/java/io/shinhanlife/dat/mcc");
|
||||
Path useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java");
|
||||
Path implementation = sourceRoot.resolve("biz/cmm/usecase/impl/AnalysisDataQueryUseCaseImpl.java");
|
||||
|
||||
String useCaseSource = Files.readString(useCase, StandardCharsets.UTF_8);
|
||||
String implementationSource = Files.readString(implementation, StandardCharsets.UTF_8);
|
||||
assertTrue(useCaseSource.contains("title = \"분석 데이터 조회\""), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("description = \"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.\""), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("AX HUB 시스템 처리 클래스"), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("개정이력"), useCaseSource);
|
||||
assertTrue(useCaseSource.contains("최초생성"), useCaseSource);
|
||||
assertTrue(implementationSource.contains("요청 수신"), implementationSource);
|
||||
assertTrue(implementationSource.contains("MapStruct를 이용한 자동 매핑"), implementationSource);
|
||||
assertTrue(implementationSource.contains("연동 중 오류 발생"), implementationSource);
|
||||
|
||||
try (var files = Files.walk(root.resolve("dat-was-korean"))) {
|
||||
for (Path file : files.filter(Files::isRegularFile).toList()) {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
assertFalse(bytes.length >= 3
|
||||
&& (bytes[0] & 0xff) == 0xef
|
||||
&& (bytes[1] & 0xff) == 0xbb
|
||||
&& (bytes[2] & 0xff) == 0xbf,
|
||||
"UTF-8 BOM must not be generated: " + file);
|
||||
String content = Files.readString(file, StandardCharsets.UTF_8);
|
||||
assertFalse(content.contains("<EFBFBD>"), "Invalid replacement character: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesV17ToolDefinitionTogetherWithToolSources() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-v17-definition").toString();
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회",
|
||||
"사번으로 재직 중인 직원을 조회한다.", "smp", "HTTP", moduleName,
|
||||
"tester", "2026.08.12", false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("09860000"), "", true)),
|
||||
List.of(), "employee");
|
||||
|
||||
Path definition = root.resolve("dat-was-v17-definition/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
String yaml = Files.readString(definition);
|
||||
|
||||
assertTrue(yaml.contains("name: smp_employee_search"), yaml);
|
||||
assertTrue(yaml.contains("when_to_use:"), yaml);
|
||||
assertTrue(yaml.contains("example_queries:"), yaml);
|
||||
assertTrue(yaml.contains("additionalProperties: false"), yaml);
|
||||
assertTrue(yaml.contains("owner_org: \"MCP_TOOL\""), yaml);
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(yaml, ToolDefinition.class);
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesV17MetadataEnteredByScaffoldUser() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-v17-options").toString();
|
||||
ToolScaffolder.ToolDefinitionOptions options = new ToolScaffolder.ToolDefinitionOptions(
|
||||
"사번으로 직원을 조회한다.",
|
||||
"직원 정보 조회 요청에 사용한다.",
|
||||
"사번이 없으면 사용하지 않는다.",
|
||||
"최대 1건만 반환한다.",
|
||||
"직원 기본 정보 조회",
|
||||
List.of("사번 10001을 조회해줘", "직원 10001 소속을 알려줘", "10001 직원을 찾아줘"),
|
||||
List.of("employee", "search"), "HR_TEAM");
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회", "직원을 조회한다.",
|
||||
"smp", "HTTP", moduleName, "tester", "2026.08.12", false, null, null, null,
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("10001"), "", true)),
|
||||
List.of(), "employee", options);
|
||||
|
||||
Path definition = root.resolve("dat-was-v17-options/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(Files.readString(definition), ToolDefinition.class);
|
||||
assertEquals("HR_TEAM", parsed.ownerOrg());
|
||||
assertEquals("10001 직원을 찾아줘", parsed.exampleQueries().get(2));
|
||||
assertEquals(2, parsed.tags().size());
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
}
|
||||
|
||||
@TempDir
|
||||
Path root;
|
||||
|
||||
@Test
|
||||
void generatesSpringAiToolAndResponseDto() throws Exception {
|
||||
String moduleName = "build/scaffold-manifest-test";
|
||||
|
||||
ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName,
|
||||
"tester", "2026.08.04", true, null);
|
||||
|
||||
Path root = Path.of(moduleName, "src/main/java/io/shinhanlife/dat/mcc/biz/cmm");
|
||||
String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java"));
|
||||
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
|
||||
|
||||
assertTrue(useCase.contains("name = \"cmm_claim_search\""));
|
||||
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")"));
|
||||
assertTrue(response.contains("private String resultCode;"));
|
||||
assertTrue(response.contains("private String resultMessage;"));
|
||||
}
|
||||
@Test
|
||||
void usesWasModuleNameAsToolPodPrefix() throws Exception {
|
||||
String moduleName = "build/dat-was-sal";
|
||||
|
||||
ToolScaffolder.scaffold("notification send", "SMS0001", "SMS 발송", "cmm", "HTTP", moduleName,
|
||||
"tester", "2026.08.05", true, null);
|
||||
|
||||
Path useCasePath = Path.of(moduleName,
|
||||
"src/main/java/io/shinhanlife/dat/mcc/biz/cmm/usecase/NotificationSendUseCase.java");
|
||||
String useCase = Files.readString(useCasePath);
|
||||
|
||||
assertTrue(useCase.contains("name = \"cmm_notification_send\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsSchemaResourcesInToolSchemaCategoryDirectory() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-sample").toString();
|
||||
|
||||
ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName,
|
||||
"tester", "2026.08.09", true, null,
|
||||
"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
|
||||
"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json");
|
||||
|
||||
Path schemas = root.resolve("dat-was-sample/src/main/resources/tool-schemas/cmm");
|
||||
assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json")));
|
||||
assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); String useCase = Files.readString(root.resolve("dat-was-sample/src/main/java/io/shinhanlife/dat/mcc/biz/cmm/usecase/ClaimSearchUseCase.java"));
|
||||
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\","));
|
||||
assertTrue(useCase.contains("inputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json\""));
|
||||
assertTrue(useCase.contains("outputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesRequestDtoFromDeclaredFields() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-pay").toString();
|
||||
|
||||
ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR 조회", "pay", "MCI", moduleName,
|
||||
"tester", "2026.08.09", true, "DFAG");
|
||||
|
||||
Path requestPath = root.resolve("dat-was-pay/src/main/java/io/shinhanlife/dat/mcc/biz/pay/dto/SearchHrRequest.java");
|
||||
String request = Files.readString(requestPath);
|
||||
|
||||
assertTrue(request.contains("import io.swagger.v3.oas.annotations.media.Schema;"));
|
||||
assertTrue(request.contains("@Schema(description = \"Search query\", example = \"example\")"));
|
||||
assertTrue(request.contains("private String query;"));
|
||||
assertFalse(request.contains("McpToolParam"));
|
||||
|
||||
Path implementationPath = root.resolve("dat-was-pay/src/main/java/io/shinhanlife/dat/mcc/biz/pay/usecase/impl/SearchHrUseCaseImpl.java");
|
||||
String implementation = Files.readString(implementationPath);
|
||||
assertTrue(implementation.contains("public SearchHrResponse execute(SearchHrRequest req)"));
|
||||
assertTrue(implementation.contains("response.setResultCode(\"SUCCESS\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesMciToolFromDeclaredInputAndOutputFields() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-pay").toString();
|
||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", List.of("EMP10001"), "", true),
|
||||
new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", List.of("1"), "", false));
|
||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong Gildong"), "", true));
|
||||
|
||||
ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR search", "pay", "MCI", moduleName,
|
||||
"tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields);
|
||||
|
||||
Path sourceRoot = root.resolve("dat-was-pay/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String request = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrRequest.java"));
|
||||
String response = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrResponse.java"));
|
||||
String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_I.java"));
|
||||
String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_O.java"));
|
||||
String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); String useCase = Files.readString(sourceRoot.resolve("biz/pay/usecase/SearchHrUseCase.java"));
|
||||
String implementation = Files.readString(sourceRoot.resolve("biz/pay/usecase/impl/SearchHrUseCaseImpl.java"));
|
||||
assertTrue(mciRequest.contains("package io.shinhanlife.dat.mcc.infra.itrf.mci.dfag.io;"), mciRequest); assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"pay\", mappingId = \"SHEARCH_01\")"));
|
||||
assertTrue(implementation.contains("import io.shinhanlife.dat.mcc.infra.itrf.mci.dfag.io.SHEARCH_01_O;"));
|
||||
|
||||
assertTrue(request.contains("private String employeeId;"));
|
||||
assertTrue(request.contains("private Integer page;"));
|
||||
assertFalse(request.contains("phoneNumber"));
|
||||
assertTrue(response.contains("private String employeeName;"));
|
||||
assertTrue(mciRequest.contains("private String employeeId;"));
|
||||
assertTrue(mciResponse.contains("private String employeeName;"));
|
||||
assertTrue(converter.contains("SearchHrResponse toResponse(SHEARCH_01_O mciRes);"));
|
||||
}
|
||||
@Test
|
||||
void generatesMockResponseAndUnitTestSkeletonFromOutputFields() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-cus").toString();
|
||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("status", "String", "Claim status", List.of("RECEIVED"), "", true));
|
||||
|
||||
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
||||
"tester", "2026.08.10", true, null, null, null, List.of(), outputFields);
|
||||
|
||||
Path mockResponse = root.resolve("dat-was-cus/src/main/resources/mock-responses/cmm_claim_search.json");
|
||||
Path useCaseTest = root.resolve("dat-was-cus/src/test/java/io/shinhanlife/dat/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java");
|
||||
|
||||
assertTrue(Files.exists(mockResponse));
|
||||
assertTrue(Files.readString(mockResponse).contains("\"status\" : \"RECEIVED\""));
|
||||
assertTrue(Files.exists(useCaseTest));
|
||||
assertTrue(Files.readString(useCaseTest).contains("class ClaimSearchUseCaseTest"));
|
||||
}
|
||||
@Test
|
||||
void createsWireMockMappingForHttpTool() {
|
||||
String mapping = ToolScaffolder.wireMockMappingContent("HR_EMPLOYEE_SEARCH", "smp_employee_search.json");
|
||||
|
||||
assertTrue(mapping.contains("\"method\" : \"POST\""), mapping);
|
||||
assertTrue(mapping.contains("\"urlPath\" : \"/HR_EMPLOYEE_SEARCH\""), mapping);
|
||||
assertTrue(mapping.contains("\"bodyFileName\" : \"smp_employee_search.json\""), mapping);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-http").toString();
|
||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", List.of("SUCCESS"), "", true),
|
||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong Gildong"), "", false),
|
||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", List.of("Duplicate"), "", false));
|
||||
|
||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", List.of("EMP10001"), "", true));
|
||||
String resultLog = ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "Employee search", "smp", "HTTP", moduleName,
|
||||
"tester", "2026.08.11", false, null, null, null, inputFields, outputFields);
|
||||
|
||||
Path dtoRoot = root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/biz/smp/dto");
|
||||
String request = Files.readString(dtoRoot.resolve("EmployeeSearchRequest.java"));
|
||||
String response = Files.readString(dtoRoot.resolve("EmployeeSearchResponse.java"));
|
||||
|
||||
assertTrue(request.contains("package io.shinhanlife.dat.mcc.biz.smp.dto;"), request);
|
||||
assertTrue(response.contains("package io.shinhanlife.dat.mcc.biz.smp.dto;"), response);
|
||||
assertTrue(response.indexOf("private String resultCode;") == response.lastIndexOf("private String resultCode;"), response);
|
||||
assertTrue(response.indexOf("private String employeeName;") == response.lastIndexOf("private String employeeName;"), response);
|
||||
String converter = Files.readString(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/biz/smp/converter/EmployeeSearchConverter.java"));
|
||||
String httpRequest = Files.readString(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/infra/itrf/http/employee_search/io/EmployeeSearchHttpRequest.java"));
|
||||
String httpResponse = Files.readString(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/infra/itrf/http/employee_search/io/EmployeeSearchHttpResponse.java"));
|
||||
String httpClient = Files.readString(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/infra/itrf/http/employee_search/EmployeeSearchClient.java"));
|
||||
assertFalse(converter.contains("phoneNumber"), converter);
|
||||
assertTrue(converter.contains("infra.itrf.http.employee_search.io.EmployeeSearchHttpRequest"), converter);
|
||||
assertFalse(converter.contains("io.shinhanlife.dat.mcc.io.shinhanlife.dat.mcc"), converter);
|
||||
assertTrue(converter.contains("// @Mapping(source = \"sourceField\", target = \"targetField\")"), converter);
|
||||
assertTrue(httpRequest.contains("private String employeeId;"), httpRequest);
|
||||
assertTrue(httpResponse.contains("private String employeeName;"), httpResponse);
|
||||
assertTrue(httpClient.contains("http.call(API_NAME, request, responseType)"), httpClient);
|
||||
assertFalse(Files.exists(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/biz/smp/legacy")));
|
||||
String implementation = Files.readString(root.resolve("dat-was-http/src/main/java/io/shinhanlife/dat/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java"));
|
||||
assertTrue(implementation.contains("public EmployeeSearchResponse execute(EmployeeSearchRequest req)"), implementation);
|
||||
assertTrue(implementation.contains("import io.shinhanlife.dat.mcc.infra.itrf.http.employee_search.EmployeeSearchClient;"), implementation);
|
||||
assertTrue(implementation.contains("private final EmployeeSearchClient employeeSearchClient;"), implementation);
|
||||
assertTrue(implementation.contains("employeeSearchClient.call(httpRequest, EmployeeSearchHttpResponse.class)"), implementation);
|
||||
assertFalse(implementation.contains("AxhubHttpComponent"), implementation);
|
||||
assertFalse(implementation.contains("executeLegacy(\"HTTP\""), implementation);
|
||||
Path wireMockResponse = root.resolve("mci-mock/__files/smp_employee_search.json");
|
||||
Path wireMockMapping = root.resolve("mci-mock/mappings/smp_employee_search.json");
|
||||
assertTrue(Files.exists(wireMockResponse), wireMockResponse.toString());
|
||||
assertTrue(Files.exists(wireMockMapping), wireMockMapping.toString());
|
||||
assertTrue(Files.readString(wireMockMapping).contains("\"urlPath\" : \"/HR_EMPLOYEE_SEARCH\""));
|
||||
Path localConfig = root.resolve("src/main/resources/glow/application-glow-local.yml");
|
||||
assertTrue(Files.exists(localConfig), localConfig.toString());
|
||||
assertTrue(Files.readString(localConfig).contains("name: employee-search"));
|
||||
assertTrue(Files.readString(localConfig).contains("url: ${AXHUB_EMPLOYEE_SEARCH_HTTP_URL:/api/mock/http/smp_employee_search}"));
|
||||
Path podMockResponse = root.resolve("dat-was-http/src/main/resources/mock-responses/smp_employee_search.json");
|
||||
assertTrue(Files.exists(podMockResponse), podMockResponse.toString());
|
||||
assertTrue(resultLog.contains("Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요."), resultLog);
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "Employee search", "smp", "HTTP", moduleName,
|
||||
"tester", "2026.08.11", false, null, null, null, inputFields, outputFields);
|
||||
long apiNameCount = Files.readAllLines(localConfig).stream()
|
||||
.filter(line -> line.trim().equals("- name: employee-search"))
|
||||
.count();
|
||||
assertEquals(1, apiNameCount);
|
||||
}
|
||||
@Test
|
||||
void generatesSeparateToolTitleAndDescription() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-title").toString();
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 정보 조회",
|
||||
"사번을 입력받아 재직 중인 직원의 기본 정보를 조회한다.", "smp", "HTTP", moduleName,
|
||||
"tester", "2026.08.11", false, null, null, null, List.of(), List.of());
|
||||
|
||||
Path useCasePath = root.resolve("dat-was-title/src/main/java/io/shinhanlife/dat/mcc/biz/smp/usecase/EmployeeSearchUseCase.java");
|
||||
String useCase = Files.readString(useCasePath);
|
||||
|
||||
assertTrue(useCase.contains("title = \"직원 정보 조회\""), useCase);
|
||||
assertTrue(useCase.contains("description = \"사번을 입력받아 재직 중인 직원의 기본 정보를 조회한다.\""), useCase);
|
||||
}
|
||||
@Test
|
||||
void generatesHttpToolUsingConfiguredApiNameAndConfiguredUrlOnly() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-http-api-name").toString();
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 정보 조회",
|
||||
"사번으로 직원을 조회한다.", "smp", "HTTP", moduleName, "tester", "2026.08.11",
|
||||
false, null, null, null, List.of(), List.of(), "employee");
|
||||
|
||||
Path implementationPath = root.resolve("dat-was-http-api-name/src/main/java/io/shinhanlife/dat/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java");
|
||||
String implementation = Files.readString(implementationPath);
|
||||
|
||||
assertTrue(implementation.contains("import io.shinhanlife.dat.mcc.infra.itrf.http.employee.EmployeeClient;"), implementation);
|
||||
assertTrue(implementation.contains("employeeClient.call(httpRequest, EmployeeSearchHttpResponse.class)"), implementation);
|
||||
assertFalse(implementation.contains("AxhubHttpDomain"), implementation);
|
||||
assertFalse(implementation.contains("\"/HR_EMPLOYEE_SEARCH\""), implementation);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addsHttpApiEntryOnItsOwnYamlLineBeforeMciConfiguration() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-http-yaml").toString();
|
||||
Path localConfig = root.resolve("src/main/resources/glow/application-glow-local.yml");
|
||||
Files.createDirectories(localConfig.getParent());
|
||||
Files.writeString(localConfig, """
|
||||
glow:
|
||||
communication:
|
||||
http:
|
||||
api-list:
|
||||
- name: memo
|
||||
biz-pod: false
|
||||
mci:
|
||||
host: localhost
|
||||
""");
|
||||
|
||||
ToolScaffolder.scaffold("insurance claim processor", "CLAIM0000001", "Insurance claim", "Insurance claim", "ins", "HTTP", moduleName,
|
||||
"tester", "2026.08.11", false, null, null, null, List.of(), List.of(), "insurance");
|
||||
|
||||
String yaml = Files.readString(localConfig);
|
||||
assertFalse(yaml.contains("biz-pod: false - name"), yaml);
|
||||
assertTrue(yaml.contains(" - name: insurance"), yaml);
|
||||
assertTrue(yaml.indexOf(" - name: insurance") < yaml.indexOf(" mci:"), yaml);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesObjectListAsNestedInnerClassWithoutSeparateItemSource() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-inner-list").toString();
|
||||
List<ToolScaffolder.FieldDefinition> fields = List.of(
|
||||
new ToolScaffolder.FieldDefinition("data", "List", "activity data", List.of(), "", false,
|
||||
List.of(), "Object", List.of(
|
||||
new ToolScaffolder.FieldDefinition("date", "String", "date", List.of("2026-08-13"), "", true),
|
||||
new ToolScaffolder.FieldDefinition("users", "Integer", "users", List.of("10"), "", false))));
|
||||
|
||||
ToolScaffolder.scaffold("ga activity status", "GA001", "GA status", "GA status", "ana", "HTTP",
|
||||
moduleName, "tester", "2026.08.13", false, null, null, null, List.of(), fields);
|
||||
|
||||
Path dtoDir = root.resolve("dat-was-inner-list/src/main/java/io/shinhanlife/dat/mcc/biz/ana/dto");
|
||||
String response = Files.readString(dtoDir.resolve("GaActivityStatusResponse.java"));
|
||||
assertTrue(response.contains("private List<DataItem> data;"), response);
|
||||
assertTrue(response.contains("public static class DataItem"), response);
|
||||
assertTrue(response.contains("private String date;"), response);
|
||||
assertFalse(Files.exists(dtoDir.resolve("GaActivityStatusResponseDataItem.java")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupedUseCaseSupportsHttpAndMciTools() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-mixed").toString();
|
||||
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
|
||||
new ToolScaffolder.ToolMethodDefinition("CustomerProfile", "getProfile", "CUST001", "Profile", "profile", "cmm", "MCI",
|
||||
false, "CSTM", null, List.of(), List.of(), null),
|
||||
new ToolScaffolder.ToolMethodDefinition("CustomerNotice", "getNotice", "NOTICE001", "Notice", "notice", "cmm", "HTTP",
|
||||
false, null, "customer-notice", List.of(), List.of(), null)));
|
||||
|
||||
Path sourceRoot = root.resolve("dat-was-mixed/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String impl = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
|
||||
assertTrue(impl.contains("getProfile(CustomerProfileRequest req)"), impl);
|
||||
assertTrue(impl.contains("getNotice(CustomerNoticeRequest req)"), impl);
|
||||
assertTrue(Files.exists(sourceRoot.resolve("infra/itrf/http/customer_notice/CustomerNoticeClient.java")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addsToolMethodToExistingUseCaseInsteadOfOverwritingIt() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-existing").toString();
|
||||
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
|
||||
new ToolScaffolder.ToolMethodDefinition("CustomerProfile", "getProfile", "CUST001", "Profile", "profile", "cmm", "MCI",
|
||||
false, "CSTM", null, List.of(), List.of(), null)));
|
||||
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
|
||||
new ToolScaffolder.ToolMethodDefinition("CustomerNotice", "getNotice", "NOTICE001", "Notice", "notice", "cmm", "HTTP",
|
||||
false, null, "customer-notice", List.of(), List.of(), null)));
|
||||
|
||||
Path sourceRoot = root.resolve("dat-was-existing/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
|
||||
String impl = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
|
||||
assertTrue(useCase.contains("getProfile(CustomerProfileRequest req)"), useCase);
|
||||
assertTrue(useCase.contains("getNotice(CustomerNoticeRequest req)"), useCase);
|
||||
assertTrue(impl.contains("private final CustomerProfileClient customerProfileClient;"), impl);
|
||||
assertTrue(impl.contains("private final CustomerNoticeClient customerNoticeClient;"), impl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.annotation.McpOutputSchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
|
||||
class ToolSchemaResolverTest {
|
||||
|
||||
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
|
||||
|
||||
@Test
|
||||
void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
||||
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||
|
||||
Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpTool.class), null, AutomaticRequest.class);
|
||||
|
||||
assertTrue(properties(schema).containsKey("differentField"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesOutputSchemaFromMarkedResponseDto() throws Exception {
|
||||
Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||
|
||||
Map<String, Object> schema = resolver.resolveOutput(
|
||||
method.getAnnotation(McpTool.class), SimpleResponse.class);
|
||||
|
||||
assertEquals(List.of("resultCode"), schema.get("required"));
|
||||
assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
|
||||
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||
|
||||
Map<String, Object> schema = resolver.resolveOutput(
|
||||
method.getAnnotation(McpTool.class), AutomaticRequest.class);
|
||||
|
||||
assertTrue(schema.isEmpty());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> properties(Map<String, Object> schema) {
|
||||
return (Map<String, Object>) schema.get("properties");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> property(Map<String, Object> schema, String name) {
|
||||
return (Map<String, Object>) properties(schema).get(name);
|
||||
}
|
||||
|
||||
static class AutomaticSchemaTool {
|
||||
@McpTool(name = "oth_test_automatic_search")
|
||||
void search(AutomaticRequest request) {
|
||||
}
|
||||
}
|
||||
|
||||
static class AutomaticOutputSchemaTool {
|
||||
@McpTool(name = "oth_test_output_search")
|
||||
SimpleResponse search(AutomaticRequest request) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@McpOutputSchema
|
||||
static class SimpleResponse {
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"SUCCESS", "FAILURE"})
|
||||
private String resultCode;
|
||||
|
||||
private String message;
|
||||
}
|
||||
|
||||
static class AutomaticRequest {
|
||||
private String differentField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ToolSourceUpdaterTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryRoot;
|
||||
|
||||
@Test
|
||||
void updatesSdkAndProjectOwnedMetadataOnTheSameToolMethod() throws Exception {
|
||||
Path source = temporaryRoot.resolve("dat-was-cus/src/main/java/example/SampleUseCase.java");
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(source, """
|
||||
package example;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
|
||||
interface SampleUseCase {
|
||||
@McpTool(name = "cmm_sample_search", description = "old")
|
||||
@GrowToolHint(requiresApproval = false)
|
||||
void search();
|
||||
}
|
||||
""");
|
||||
|
||||
ToolSourceUpdater.updateToolSource(temporaryRoot, "cmm_sample_search", "customer", "new", true, true);
|
||||
|
||||
String updated = Files.readString(source);
|
||||
assertTrue(updated.contains("@McpTool(name = \"cmm_sample_search\", description = \"new\")"));
|
||||
assertTrue(updated.contains("register = true"), updated);
|
||||
assertTrue(updated.contains("requiresApproval = true"), updated);
|
||||
assertTrue(updated.contains("categoryKey = \"customer\""), updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package io.shinhanlife.dat.lib.validation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.lib.validation
|
||||
* @className McpToolNameValidatorTest
|
||||
* @description MCP tool name uniqueness validation test
|
||||
* @author 0986406
|
||||
* @create 2026.07.27
|
||||
* <pre>
|
||||
* ---------- revision history ----------
|
||||
* date author description
|
||||
* ---------- --------- ---------------------------
|
||||
* 2026.07.27 0986406 initial creation
|
||||
* </pre>
|
||||
*/
|
||||
class McpToolNameValidatorTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryRoot;
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateMcpToolNamesAcrossToolModules() throws IOException {
|
||||
writeToolSource("dat-was-first", "FirstTool.java", "first", "oth_sms_notification_send");
|
||||
writeToolSource("dat-was-second", "SecondTool.java", "second", "oth_sms_notification_send");
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
|
||||
assertTrue(exception.getMessage().contains("oth_sms_notification_send"));
|
||||
assertTrue(exception.getMessage().contains("dat-was-first"));
|
||||
assertTrue(exception.getMessage().contains("dat-was-second"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationRunnerRejectsDuplicateMcpToolNamesBeforePackaging() throws IOException {
|
||||
writeToolSource("dat-was-first", "FirstTool.java", "first", "oth_sms_notification_send");
|
||||
writeToolSource("dat-was-second", "SecondTool.java", "second", "oth_sms_notification_send");
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidationRunner.validate(temporaryRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsToolNameOutsideConfiguredPattern() throws IOException {
|
||||
writeToolSource("dat-was-first", "FirstTool.java", "first", "bond.issue");
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Invalid MCP tool name(s)"));
|
||||
assertTrue(exception.getMessage().contains("bond.issue"));
|
||||
}
|
||||
@Test
|
||||
void acceptsLettersDigitsUnderscoresAndDashesWithin128Characters() throws IOException {
|
||||
String validName = "Tool_Name-" + "a".repeat(118);
|
||||
writeToolSource("dat-was-first", "FirstTool.java", "first", validName);
|
||||
|
||||
assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsToolNameLongerThan128Characters() throws IOException {
|
||||
String invalidName = "a".repeat(129);
|
||||
writeToolSource("dat-was-first", "FirstTool.java", "first", invalidName);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
||||
() -> McpToolNameValidator.assertUnique(temporaryRoot));
|
||||
|
||||
assertTrue(exception.getMessage().contains(invalidName));
|
||||
}
|
||||
@Test
|
||||
void acceptsCurrentProjectToolNames() {
|
||||
assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(findProjectRoot()));
|
||||
}
|
||||
|
||||
private void writeToolSource(String moduleName, String fileName, String className, String toolName) throws IOException {
|
||||
Path source = temporaryRoot.resolve(moduleName).resolve("src/main/java/example").resolve(fileName);
|
||||
Files.createDirectories(source.getParent());
|
||||
Files.writeString(source, """
|
||||
package example;
|
||||
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
|
||||
class %s {
|
||||
@McpTool(name = "%s")
|
||||
void execute() { }
|
||||
}
|
||||
""".formatted(className, toolName));
|
||||
}
|
||||
|
||||
private Path findProjectRoot() {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Could not locate Gradle project root");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.shinhanlife.dat.mcc.mcp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import io.shinhanlife.dat.lib.mcp.McpRequestHeaderContext;
|
||||
import io.shinhanlife.dat.lib.mcp.McpRequestHeaderFilter;
|
||||
import io.shinhanlife.dat.lib.mcp.McpRequestHeaders;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
class McpRequestHeaderFilterTest {
|
||||
|
||||
@Test
|
||||
void capturesDapmsHeadersOnlyForTheCurrentRequest() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
||||
request.addHeader("x-request-id", "request-001");
|
||||
request.addHeader("guid", "guid-001");
|
||||
request.addHeader("mcp-session-id", "session-001");
|
||||
request.addHeader("employee-no", "ENC(employee)");
|
||||
request.addHeader("virtual-employee-no", "ENC(virtual)");
|
||||
|
||||
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) ->
|
||||
assertThat(asMap(McpRequestHeaderContext.current())).containsExactly(
|
||||
Map.entry("requestId", "request-001"),
|
||||
Map.entry("guid", "guid-001"),
|
||||
Map.entry("mcpSessionId", "session-001"),
|
||||
Map.entry("employeeNo", "ENC(employee)"),
|
||||
Map.entry("virtualEmployeeNo", "ENC(virtual)")));
|
||||
|
||||
assertNull(McpRequestHeaderContext.current());
|
||||
}
|
||||
|
||||
private Map<String, Object> asMap(McpRequestHeaders headers) {
|
||||
try {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
for (RecordComponent component : McpRequestHeaders.class.getRecordComponents()) {
|
||||
values.put(component.getName(), component.getAccessor().invoke(headers));
|
||||
}
|
||||
return values;
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.shinhanlife.dat.mcc.mcp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.shinhanlife.dat.lib.mcp.ToolMcpServerConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
|
||||
class ToolMcpServerConfigurationTest {
|
||||
|
||||
private final ToolMcpServerConfiguration configuration = new ToolMcpServerConfiguration();
|
||||
|
||||
@Test
|
||||
void exposesOnlyExactMcpEndpointSoLegacyMcpApiPathsRemainAvailable() {
|
||||
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
||||
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration = configuration.toolMcpServlet(transport);
|
||||
|
||||
assertThat(registration.getUrlMappings()).containsExactlyInAnyOrder("/mcp", "/mcp/message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsMcpSdkTwoSyncServerWithoutSpringAiAutoConfiguration() {
|
||||
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
||||
|
||||
McpSyncServer server = configuration.toolMcpServer(transport);
|
||||
|
||||
assertEquals("dap-tool-pod", server.getServerInfo().name());
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.shinhanlife.dat.mcc.presentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
class BusinessToolControllerHeaderContractTest {
|
||||
|
||||
@Test
|
||||
void receivesTheHeadersForwardedByDapms() {
|
||||
Method method = Arrays.stream(BusinessToolController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals("executeDynamicTool"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
List<String> headerNames = Arrays.stream(method.getParameters())
|
||||
.map(parameter -> parameter.getAnnotation(RequestHeader.class))
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.peek(header -> assertThat(header.required()).isFalse())
|
||||
.map(RequestHeader::value)
|
||||
.toList();
|
||||
|
||||
assertThat(headerNames).containsExactly(
|
||||
"x-request-id", "guid", "mcp-session-id", "employee-no", "virtual-employee-no");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.shinhanlife.dat.mcc.presentation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||
import io.shinhanlife.dat.lib.validation.ToolArgumentSchemaValidator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ToolArgumentSchemaValidatorTest {
|
||||
|
||||
private final ToolArgumentSchemaValidator validator =
|
||||
new ToolArgumentSchemaValidator(new ObjectMapper(), new DefaultJsonSchemaValidator());
|
||||
|
||||
@Test
|
||||
void validatesJsonSchema202012WithTheMcpSdkValidator() {
|
||||
Map<String, Object> schema = Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of("name", Map.of("type", "string")),
|
||||
"required", List.of("name"));
|
||||
|
||||
assertTrue(validator.validate(schema, Map.of("name", "Hong")).valid());
|
||||
assertFalse(validator.validate(schema, Map.of()).valid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.shinhanlife.dat.mcc.presentation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ToolTestConsoleResourceTest {
|
||||
|
||||
@Test
|
||||
void publishesManifestDrivenToolTestConsole() throws Exception {
|
||||
try (InputStream resource = getClass().getResourceAsStream("/static/tool-test-console.html")) {
|
||||
assertNotNull(resource);
|
||||
String html = new String(resource.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(html.contains("/tool-manifest"));
|
||||
assertTrue(html.contains("Run saved cases"));
|
||||
assertTrue(html.contains("localStorage"));
|
||||
assertTrue(html.contains("request-id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.shinhanlife.glow.communication.annotation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class GlowTrgmFieldContractTest {
|
||||
|
||||
private static class StandardTrgmDto {
|
||||
@GlowTrgmField(order = 1, length = 8, decimal = 2, description = "금액", target = "body", type = "gs")
|
||||
private String amount;
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesTheStandardGlowTrgmFieldMetadata() throws Exception {
|
||||
Field field = StandardTrgmDto.class.getDeclaredField("amount");
|
||||
GlowTrgmField metadata = field.getAnnotation(GlowTrgmField.class);
|
||||
|
||||
assertEquals(1, metadata.order());
|
||||
assertEquals(8, metadata.length());
|
||||
assertEquals(2, metadata.decimal());
|
||||
assertEquals("금액", metadata.description());
|
||||
assertEquals("body", metadata.target());
|
||||
assertEquals("gs", metadata.type());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package io.shinhanlife.glow.util;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.glow.util
|
||||
* @className GlowMciParserTest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.glow.GlowMciFieldInfo;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class GlowMciParserTest {
|
||||
|
||||
public static class DummyMciDto {
|
||||
@GlowMciFieldInfo(order = 1, length = 10)
|
||||
private String customerId;
|
||||
|
||||
@GlowMciFieldInfo(order = 2, length = 15)
|
||||
private String name;
|
||||
|
||||
@GlowMciFieldInfo(order = 3, length = 3)
|
||||
private int age;
|
||||
|
||||
public String getCustomerId() { return customerId; }
|
||||
public String getName() { return name; }
|
||||
public int getAge() { return age; }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("고정 길이 MCI 문자열을 DTO로 파싱하는 테스트")
|
||||
public void testParseFixedLengthString() {
|
||||
// given: 고정 길이 텍스트 (총 28자리)
|
||||
// ID(10) + Name(15) + Age(3)
|
||||
String rawMciString = "CUST000001KIM SHINHAN 035";
|
||||
|
||||
// when
|
||||
DummyMciDto result = GlowMciParser.parse(rawMciString, DummyMciDto.class);
|
||||
|
||||
// then
|
||||
System.out.println("==================================================");
|
||||
System.out.println(" [원본 MCI 전문] : [" + rawMciString + "]");
|
||||
System.out.println(" [파싱된 ID (10자리)] : [" + result.getCustomerId() + "]");
|
||||
System.out.println(" [파싱된 Name (15자리)] : [" + result.getName() + "]");
|
||||
System.out.println(" [파싱된 Age (3자리)] : [" + result.getAge() + "]");
|
||||
System.out.println("==================================================");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("CUST000001", result.getCustomerId());
|
||||
assertEquals("KIM SHINHAN", result.getName());
|
||||
assertEquals(35, result.getAge());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"resultCode": "SUCCESS"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
name: cmm_claim_search
|
||||
display_name: 보험금 청구 상태 조회
|
||||
version: 1.0.0
|
||||
category_key: cmm
|
||||
description:
|
||||
function: 청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.
|
||||
when_to_use: 사용자가 보험금 청구 진행 상태나 심사 결과를 확인하려는 경우 사용한다.
|
||||
when_not_to_use: 보험금 청구를 새로 접수하거나 기존 청구를 변경하려는 경우에는 사용하지 않는다.
|
||||
io_limits: 청구번호 또는 계약번호 중 하나 이상이 필요하며 조회 결과만 반환한다.
|
||||
display_description: 보험금 청구 상태와 심사 결과를 조회합니다.
|
||||
example_queries:
|
||||
- 내 보험금 청구가 어디까지 진행됐는지 알려줘
|
||||
- 계약번호로 최근 청구 상태를 확인해줘
|
||||
- 청구 심사 결과가 나왔는지 조회해줘
|
||||
read_only: true
|
||||
destructive: false
|
||||
idempotent: true
|
||||
parameters_schema:
|
||||
type: object
|
||||
properties:
|
||||
claimNo:
|
||||
type: string
|
||||
description: 조회할 보험금 청구번호
|
||||
required:
|
||||
- claimNo
|
||||
additionalProperties: false
|
||||
tags:
|
||||
- 보험금
|
||||
- 청구조회
|
||||
owner_org: MCP_TOOL
|
||||
Reference in New Issue
Block a user