From 078331b3b818a2751f632a64ace4372af341cfc0 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 6 Aug 2026 09:29:54 +0900 Subject: [PATCH] =?UTF-8?q?In=20=ED=95=84=ED=84=B0=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B6=80=EC=97=90=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=BD=94=EB=93=9C=20=EA=B0=92=EC=9D=84=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20HTTP=20=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EC=84=A4=EC=A0=95=ED=95=98=EB=8A=94=20=ED=95=84?= =?UTF-8?q?=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dynamic/filter/JsonToSetStatusFilter.java | 67 ++++ .../filter/JsonToSetStatusFilterTest.java | 319 ++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 src/main/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilter.java create mode 100644 src/test/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilterTest.java diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilter.java new file mode 100644 index 0000000..9830c23 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilter.java @@ -0,0 +1,67 @@ +package com.eactive.eai.adapter.http.dynamic.filter; + +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.util.JsonPathUtil; +import com.fasterxml.jackson.databind.JsonNode; + +public class JsonToSetStatusFilter implements HttpAdapterFilter { + private static final String PROPGROUP = "JsonToSetStatusFilter"; + static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + /** HTTP 상태코드 유효 범위 (RFC 7231) */ + private static final int MIN_HTTP_STATUS = 100; + private static final int MAX_HTTP_STATUS = 599; + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // do nothing + return message; + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + String fieldName = ""; + String statusParam = ""; + try { + JsonNode rootNode = parseJson(adptGrpName, resultMessage); + fieldName = getFieldName(adptGrpName); + if (rootNode != null && StringUtils.isNotBlank(fieldName) && rootNode.has(fieldName)) { + statusParam = rootNode.get(fieldName).asText(); + int httpStatus = Integer.parseInt(statusParam); + if (isValidHttpStatus(httpStatus)) { + response.setStatus(httpStatus); + } else { + logger.warn("유효하지 않은 HTTP 상태코드. 상태코드를 설정하지 않음. fieldName={}, value={}", fieldName, statusParam); + } + } else { + logger.warn("설정과 맞지 않는 메시지. 상태코드를 설정하지 않음. fieldName={}", fieldName); + } + } catch (Exception e) { + logger.warn("상태코드 추출 실패. fieldName={}, value={}", fieldName, statusParam, e); + } + + return resultMessage; + } + + /** HTTP 상태코드로 사용 가능한 값인지 확인한다. (100 ~ 599) */ + private boolean isValidHttpStatus(int httpStatus) { + return httpStatus >= MIN_HTTP_STATUS && httpStatus <= MAX_HTTP_STATUS; + } + + private String getFieldName(String adptGrpName) { + return PropManager.getInstance().getProperty(PROPGROUP, adptGrpName); + } + + private JsonNode parseJson(String adptGrpName, Object message) throws Exception { + return JsonPathUtil.toTree(message); + } +} diff --git a/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilterTest.java b/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilterTest.java new file mode 100644 index 0000000..eb456c6 --- /dev/null +++ b/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/JsonToSetStatusFilterTest.java @@ -0,0 +1,319 @@ +package com.eactive.eai.adapter.http.dynamic.filter; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.simple.JSONObject; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.context.support.GenericApplicationContext; + +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.ApplicationContextProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * JsonToSetStatusFilter 단위 테스트. + * + * 상태코드 필드명은 PropManager(그룹: JsonToSetStatusFilter, 키: 어댑터그룹명)에서 조회하므로 + * PropManager 를 Mock 으로 등록하고 필터 로직만 검증한다. + * + * 4번 그룹은 "현재 구현의 동작을 그대로 고정(characterization)"한 테스트로, + * 개선 여부 판단용이다. 구현을 보완하면 해당 테스트도 함께 수정해야 한다. + */ +@TestMethodOrder(MethodOrderer.DisplayName.class) +class JsonToSetStatusFilterTest { + + private static final String GRP = "TEST_GRP"; + private static final String ADPT = "TEST_ADPT"; + private static final String FIELD = "apiRsltCd"; + private static final String PROP_GROUP = "JsonToSetStatusFilter"; + + private static GenericApplicationContext ctx; + private static PropManager mockPropManager; + private static JsonToSetStatusFilter filter; + + private HttpServletRequest mockRequest; + private HttpServletResponse mockResponse; + private Properties prop; + + @BeforeAll + static void setUpClass() { + mockPropManager = mock(PropManager.class); + + ctx = new GenericApplicationContext(); + ctx.getBeanFactory().registerSingleton("propManager", mockPropManager); + ctx.registerBeanDefinition("applicationContextProvider", + BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class) + .getBeanDefinition()); + ctx.refresh(); + + filter = new JsonToSetStatusFilter(); + } + + @AfterAll + static void tearDownClass() { + if (ctx != null) ctx.close(); + } + + @BeforeEach + void setUp() { + reset(mockPropManager); + mockRequest = mock(HttpServletRequest.class); + mockResponse = mock(HttpServletResponse.class); + prop = new Properties(); + + when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(FIELD); + } + + private String body(String rsltCd) { + return "{\"" + FIELD + "\":\"" + rsltCd + "\",\"msg\":\"OK\"}"; + } + + // ========================================================================= + // 1. 상태코드 정상 반영 + // ========================================================================= + + @Test + @DisplayName("1-1. 문자열 body의 상태코드 필드값을 HTTP 상태코드로 설정한다") + void testPostFilter_string_setsStatus() throws Exception { + Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse); + + verify(mockResponse).setStatus(404); + assertEquals(body("404"), result, "원 메시지를 그대로 반환해야 한다"); + } + + @Test + @DisplayName("1-2. 숫자 타입 필드값도 상태코드로 설정한다") + void testPostFilter_numericNode_setsStatus() throws Exception { + filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":503}", prop, mockRequest, mockResponse); + + verify(mockResponse).setStatus(503); + } + + @Test + @DisplayName("1-3. 업무팀 주 사용 케이스 - 2xx 상태코드를 설정한다") + void testPostFilter_successStatusCodes() throws Exception { + for (int status : new int[] { 200, 201, 202, 204 }) { + reset(mockResponse); + + filter.doPostFilter(GRP, ADPT, body(String.valueOf(status)), prop, mockRequest, mockResponse); + + verify(mockResponse).setStatus(status); + } + } + + @Test + @DisplayName("1-4. 어댑터 그룹마다 다른 필드명을 사용할 수 있다") + void testPostFilter_perGroupFieldName() throws Exception { + when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn("rspCd"); + + filter.doPostFilter(GRP, ADPT, "{\"rspCd\":\"403\",\"" + FIELD + "\":\"500\"}", prop, + mockRequest, mockResponse); + + verify(mockResponse).setStatus(403); + verify(mockResponse, never()).setStatus(500); + } + + @Test + @DisplayName("1-5. JSONObject 타입 응답도 처리한다") + void testPostFilter_jsonObjectMessage_setsStatus() throws Exception { + JSONObject json = new JSONObject(); + json.put(FIELD, "404"); + + Object result = filter.doPostFilter(GRP, ADPT, json, prop, mockRequest, mockResponse); + + verify(mockResponse).setStatus(404); + assertSame(json, result); + } + + @Test + @DisplayName("1-6. JsonNode 타입 응답도 처리한다") + void testPostFilter_jsonNodeMessage_setsStatus() throws Exception { + JsonNode node = new ObjectMapper().readTree(body("401")); + + Object result = filter.doPostFilter(GRP, ADPT, node, prop, mockRequest, mockResponse); + + verify(mockResponse).setStatus(401); + assertSame(node, result); + } + + @Test + @DisplayName("1-7. 경계값 100 / 599는 설정한다") + void testPostFilter_boundaryValues() throws Exception { + filter.doPostFilter(GRP, ADPT, body("100"), prop, mockRequest, mockResponse); + verify(mockResponse).setStatus(100); + + reset(mockResponse); + filter.doPostFilter(GRP, ADPT, body("599"), prop, mockRequest, mockResponse); + verify(mockResponse).setStatus(599); + } + + // ========================================================================= + // 2. 상태코드를 변경하지 않는 경우 + // ========================================================================= + + @Test + @DisplayName("2-1. 상태코드 필드가 없으면 setStatus를 호출하지 않는다") + void testPostFilter_fieldAbsent_noStatusChange() throws Exception { + filter.doPostFilter(GRP, ADPT, "{\"msg\":\"OK\"}", prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("2-2. 상태코드 필드값이 빈 문자열이면 setStatus를 호출하지 않는다") + void testPostFilter_emptyValue_noStatusChange() throws Exception { + filter.doPostFilter(GRP, ADPT, body(""), prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("2-3. 프로퍼티에 필드명 설정이 없으면(null) 예외 없이 통과한다") + void testPostFilter_noConfiguredField_noStatusChange() throws Exception { + when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(null); + + Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + assertEquals(body("404"), result); + } + + @Test + @DisplayName("2-4. 상태코드 필드값이 숫자가 아니면 로그만 남기고 상태코드를 변경하지 않는다") + void testPostFilter_nonNumericValue_noStatusChange() throws Exception { + Object result = filter.doPostFilter(GRP, ADPT, body("E0001"), prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + assertEquals(body("E0001"), result, "파싱 실패해도 원 메시지는 그대로 반환되어야 한다"); + } + + @Test + @DisplayName("2-5. 업무결과코드 0000은 유효한 상태코드가 아니므로 설정하지 않는다") + void testPostFilter_businessCode0000_noStatusChange() throws Exception { + filter.doPostFilter(GRP, ADPT, body("0000"), prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("2-6. HTTP 상태코드 범위(100~599)를 벗어난 값은 설정하지 않는다") + void testPostFilter_outOfRangeStatus_noStatusChange() throws Exception { + for (String value : new String[] { "0", "99", "600", "9999", "-200" }) { + reset(mockResponse); + + filter.doPostFilter(GRP, ADPT, body(value), prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + } + + @Test + @DisplayName("2-7. JSON 배열 응답이면 상태코드를 변경하지 않는다") + void testPostFilter_jsonArray_noStatusChange() throws Exception { + filter.doPostFilter(GRP, ADPT, "[{\"" + FIELD + "\":\"404\"}]", prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("2-8. 필드값이 JSON null이면 상태코드를 변경하지 않는다") + void testPostFilter_jsonNullValue_noStatusChange() throws Exception { + filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":null}", prop, mockRequest, mockResponse); + + verify(mockResponse, never()).setStatus(anyInt()); + } + + // ========================================================================= + // 3. 비정상 입력에서도 예외를 던지지 않는다 (거래 실패 방지) + // ========================================================================= + + @Test + @DisplayName("3-1. JSON 형식이 아닌 응답이어도 예외 없이 통과한다") + void testPostFilter_nonJsonBody_noException() throws Exception { + String xml = "0000"; + + Object result = assertDoesNotThrow( + () -> filter.doPostFilter(GRP, ADPT, xml, prop, mockRequest, mockResponse)); + + assertSame(xml, result); + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("3-2. 깨진 JSON 응답이어도 예외 없이 통과한다") + void testPostFilter_malformedJson_noException() { + String broken = "{\"" + FIELD + "\":\"404\""; + + assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, broken, prop, mockRequest, mockResponse)); + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("3-3. null 응답이어도 예외 없이 null을 그대로 반환한다") + void testPostFilter_nullMessage_noException() { + Object result = assertDoesNotThrow( + () -> filter.doPostFilter(GRP, ADPT, null, prop, mockRequest, mockResponse)); + + assertNull(result); + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("3-4. 빈 문자열 응답이어도 예외 없이 통과한다") + void testPostFilter_emptyBody_noException() { + assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, "", prop, mockRequest, mockResponse)); + verify(mockResponse, never()).setStatus(anyInt()); + } + + @Test + @DisplayName("3-5. 프로퍼티 그룹 미등록(PropManager가 null 반환)이어도 예외 없이 통과한다") + void testPostFilter_propGroupMissing_noException() { + when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null); + + assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse)); + verify(mockResponse, never()).setStatus(anyInt()); + } + + // ========================================================================= + // 4. doPreFilter / 현재 동작 고정 (개선 검토 대상) + // ========================================================================= + + @Test + @DisplayName("4-1. doPreFilter는 아무 것도 하지 않고 요청 메시지를 그대로 반환한다") + void testPreFilter_doesNothing() throws Exception { + String message = body("404"); + + Object result = filter.doPreFilter(GRP, ADPT, message, prop, mockRequest, mockResponse); + + assertSame(message, result); + verify(mockResponse, never()).setStatus(anyInt()); + verifyNoInteractions(mockPropManager); + } + + @Test + @DisplayName("4-2. [확인필요] byte[] 응답은 JSON으로 파싱되지 않아 상태코드가 설정되지 않는다") + void testPostFilter_byteArray_notSupported() throws Exception { + byte[] msg = body("404").getBytes(StandardCharsets.UTF_8); + + Object result = filter.doPostFilter(GRP, ADPT, msg, prop, mockRequest, mockResponse); + + // JsonPathUtil.toTree 가 byte[] 를 toString() 처리하므로 "[B@..." 가 되어 파싱에 실패한다. + verify(mockResponse, never()).setStatus(anyInt()); + assertSame(msg, result); + } +}