feat: add scaffold HTTP mock support
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled

This commit is contained in:
jade
2026-08-11 23:12:30 +09:00
parent 45e4332adb
commit 0e27937687
18 changed files with 347 additions and 5 deletions

View File

@@ -0,0 +1,61 @@
package io.shinhanlife.dap.lib.adapter.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Local HTTP mock server for scaffolded HTTP Tools.
*
* <p>Each Tool Pod returns the JSON generated under
* {@code src/main/resources/mock-responses/{toolName}.json}. It is enabled only
* when {@code axhub.mock.http.enabled=true}, which the HTTP Scaffold adds to local configuration.</p>
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "axhub.mock.http", name = "enabled", havingValue = "true")
@RequestMapping("/api")
public class MockEimsHttpServer {
private final ObjectMapper objectMapper;
@PostMapping("/mock/http/{toolName:[a-z0-9_-]+}")
public ResponseEntity<JsonNode> mockToolHttpResponse(
@PathVariable String toolName,
@RequestBody(required = false) JsonNode request) {
ClassPathResource resource = new ClassPathResource("mock-responses/" + toolName + ".json");
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
try {
log.info("[MockEimsHttpServer] HTTP mock request. toolName={}, body={}", toolName, request);
return ResponseEntity.ok(objectMapper.readTree(resource.getInputStream()));
} catch (Exception e) {
log.warn("[MockEimsHttpServer] Unable to read mock response. toolName={}", toolName, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/gateway")
public ResponseEntity<?> mockEimsReceiver(
@RequestHeader(value = "X-Trace-Id", required = false) String traceId,
@RequestBody Map<String, Object> request) {
String interfaceId = String.valueOf(request.getOrDefault("interfaceId", ""));
log.info("[MockEimsHttpServer] Legacy gateway mock request. traceId={}, interfaceId={}", traceId, interfaceId);
return ResponseEntity.ok(Map.of(
"status", "404",
"message", "MOCK data is not defined for interfaceId: " + interfaceId));
}
}

View File

@@ -853,12 +853,17 @@ public class ToolScaffolder {
Path projectRoot = moduleRoot.getParent();
Path wireMockBodyPath = projectRoot.resolve(Paths.get("mci-mock", "__files", toolName + ".json"));
Path wireMockMappingPath = projectRoot.resolve(Paths.get("mci-mock", "mappings", toolName + ".json"));
Path podMockResponsePath = moduleRoot.resolve(Paths.get("src/main/resources/mock-responses", toolName + ".json"));
Files.createDirectories(wireMockBodyPath.getParent());
Files.createDirectories(wireMockMappingPath.getParent());
Files.createDirectories(podMockResponsePath.getParent());
Files.writeString(wireMockBodyPath, mockResponse);
Files.writeString(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString()));
Files.writeString(podMockResponsePath, mockResponse);
ensureLocalHttpApiConfiguration(moduleRoot, httpApiName, toolName);
log.append("[WireMock Response] ").append(wireMockBodyPath).append("\n");
log.append("[WireMock Mapping] ").append(wireMockMappingPath).append("\n");
log.append("[Pod Mock Response] ").append(podMockResponsePath).append("\n");
} else {
Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json"));
Files.createDirectories(mockResponsePath.getParent());
@@ -884,6 +889,36 @@ public class ToolScaffolder {
.toLowerCase(Locale.ROOT);
}
private static void ensureLocalHttpApiConfiguration(Path moduleRoot, String httpApiName, String toolName) throws IOException {
Path localConfigPath = moduleRoot.resolve("src/main/resources/application-local.yml");
Files.createDirectories(localConfigPath.getParent());
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath) : "";
if (java.util.regex.Pattern.compile("(?m)^\\s*-\\s+name:\\s*"
+ java.util.regex.Pattern.quote(httpApiName) + "\\s*$").matcher(existing).find()) {
return;
}
String environmentKey = toPackageSegment(httpApiName).toUpperCase(Locale.ROOT).replace('-', '_');
String config = """
axhub:
mock:
http:
enabled: true
glow:
communication:
http:
api-list:
- name: %s
domain: ${AXHUB_%s_HTTP_DOMAIN:http://localhost:${server.port}}
url: ${AXHUB_%s_HTTP_URL:/api/mock/http/%s}
method: POST
content-type: application/json;charset=UTF-8
biz-pod: false
""".formatted(httpApiName, environmentKey, environmentKey, toolName);
Files.writeString(localConfigPath, existing + config);
}
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,
String author, String createDate, boolean request) {
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"));
@@ -999,21 +1034,24 @@ public class ToolScaffolder {
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.%s.io.%sHttpRequest;
import %s.%s.io.%sHttpResponse;
import %s.io.%sHttpRequest;
import %s.io.%sHttpResponse;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface %sConverter {
// Field names differ? Add mappings like this before the method.
// @Mapping(source = "sourceField", target = "targetField")
%sHttpRequest toHttpRequest(%sRequest request);
%sResponse toResponse(%sHttpResponse httpResponse);
}
""".formatted(bizPackage,
bizPackage, baseName,
bizPackage, baseName,
BASE_PACKAGE, httpPackage, baseName,
BASE_PACKAGE, httpPackage, baseName,
httpPackage, baseName,
httpPackage, baseName,
baseName, baseName, baseName, baseName, baseName);
}
@@ -1032,9 +1070,13 @@ public class ToolScaffolder {
import %s.%s.io.%s_I;
import %s.%s.io.%s_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface %sConverter {
// Field names differ? Add mappings like this before the method.
// @Mapping(source = "sourceField", target = "targetField")
%s_I toLegacyRequest(%sRequest request);
%sRequest toRequest(%s_I mciRequest);
%sResponse toResponse(%s_O mciRes);
@@ -1053,10 +1095,13 @@ public class ToolScaffolder {
import %s.legacy.%sLegacyRequest;
import %s.legacy.%sLegacyResponse;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface %sConverter {
// Field names differ? Add mappings like this before the method.
// @Mapping(source = "sourceField", target = "targetField")
%sLegacyRequest toLegacyRequest(%sRequest request);
%sRequest toRequest(%sLegacyRequest legacyRequest);
%sResponse toResponse(%sLegacyResponse legacyResponse);

View File

@@ -0,0 +1,19 @@
package io.shinhanlife.dap.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

@@ -1,6 +1,7 @@
package io.shinhanlife.dap.lib.util;
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 java.nio.file.Files;
@@ -166,6 +167,8 @@ class ToolScaffolderTest {
String httpClient = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleClient.java"));
assertFalse(converter.contains("phoneNumber"), converter);
assertTrue(converter.contains("infra.itrf.http.sample.io.EmployeeSearchHttpRequest"), converter);
assertFalse(converter.contains("io.shinhanlife.dap.mcc.io.shinhanlife.dap.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);
@@ -182,6 +185,19 @@ class ToolScaffolderTest {
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("dap-was-http/src/main/resources/application-local.yml");
assertTrue(Files.exists(localConfig), localConfig.toString());
assertTrue(Files.readString(localConfig).contains("name: sample"));
assertTrue(Files.readString(localConfig).contains("url: ${AXHUB_SAMPLE_HTTP_URL:/api/mock/http/smp_employee_search}"));
Path podMockResponse = root.resolve("dap-was-http/src/main/resources/mock-responses/smp_employee_search.json");
assertTrue(Files.exists(podMockResponse), podMockResponse.toString());
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: sample"))
.count();
assertEquals(1, apiNameCount);
}
@Test
void generatesSeparateToolTitleAndDescription() throws Exception {

View File

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