refactor: massive rename dap -> dat and fix scaffold syntax error
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s

This commit is contained in:
jade
2026-08-20 10:41:37 +09:00
parent 64c422378f
commit 0085ec6919
765 changed files with 1151 additions and 1151 deletions

View File

@@ -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");
}
}

View File

@@ -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\":");
}
}

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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", "입출력 제한");
}
}

View File

@@ -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() {
}
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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"));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}

View File

@@ -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());
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,3 @@
{
"resultCode": "SUCCESS"
}

View File

@@ -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