ReflectHeaderFilter 필터 추가

This commit is contained in:
curry772
2026-08-14 15:19:46 +09:00
parent 23bdca4777
commit cc016d9d81
2 changed files with 812 additions and 0 deletions
@@ -0,0 +1,263 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
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;
/**
* 수신 Http Header 중 <b>허용 목록(white list)에 등록된 헤더만</b> 응답 헤더로 복사하는 Inbound Adapter Filter.
*
* <p>모든 헤더를 복사하고 제외 목록으로 걸러내는 {@link ReflectAllHeaderFilter} 와 달리,
* 복사 대상을 어댑터 그룹별로 명시하는 방식이다.
*
* <p>설정은 <code>HttpHeaderFilter</code> property 그룹에 등록하며, 아래 순서로 가장 먼저 찾은 키 하나만 사용한다.
* (합집합이 아니라 override 이므로 어댑터 그룹 키를 지정하면 전역 키는 무시된다.)
*
* <pre>
* 1. ReflectHeaderFilter.whiteList.{어댑터그룹명} 어댑터 그룹 단위
* 2. ReflectHeaderFilter.whiteList 전역 기본값
* </pre>
*
* <p>값은 콤마로 구분한 헤더명 목록이며 대소문자를 구분하지 않는다.
* 헤더명 끝에 <code>*</code> 를 붙이면 접두사 일치로 처리한다.
*
* <pre>
* HttpHeaderFilter.ReflectHeaderFilter.whiteList = x-elink-client-id
* HttpHeaderFilter.ReflectHeaderFilter.whiteList.djbTrans = x-obp-txid, x-obp-partnercode, X-KKB-*
* </pre>
*
* <p>설정이 없거나 비어 있으면 어떤 헤더도 복사하지 않는다.
*/
public class ReflectHeaderFilter implements HttpAdapterFilter {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
public static final String HEADER_KEY_NAMES = "ReflectHeaderFilter.whiteList";
/**
* 허용 목록에 등록되어 있어도 복사하지 않는 헤더.
* 요청측 값이 응답 본문/커넥션과 불일치하면 응답 자체가 깨지므로 설정으로 열 수 없게 한다.
*/
private static final Set<String> NEVER_REFLECT = createHeaderSet(
"Content-Length",
"Transfer-Encoding",
"Connection",
"Keep-Alive",
"Upgrade",
"TE",
"Trailer");
/** 어댑터 그룹별 허용 목록 캐시. 필터 인스턴스는 HttpAdapterFilterFactory 에서 싱글톤으로 공유된다. */
private final ConcurrentHashMap<String, CachedWhiteList> whiteListCache = new ConcurrentHashMap<String, CachedWhiteList>();
public ReflectHeaderFilter() {
super();
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPreFilter ReflectHeaderFilter Start.");
reflectHeaders(adptGrpName, request, response);
logger.debug("doPreFilter ReflectHeaderFilter End.");
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPostFilter ReflectHeaderFilter Start.");
reflectHeaders(adptGrpName, request, response);
logger.debug("doPostFilter ReflectHeaderFilter End.");
return resultMessage;
}
private void reflectHeaders(String adptGrpName, HttpServletRequest request, HttpServletResponse response) {
WhiteList whiteList = getWhiteList(adptGrpName);
if (whiteList.isEmpty()) {
logger.debug("No reflect header configured for adapter group [" + adptGrpName + "]. Skip all.");
return;
}
try {
for (String headerName : whiteList.getNames()) {
String headerValue = request.getHeader(headerName);
if (headerValue == null) {
continue;
}
setHeader(headerName, headerValue, response);
}
// 접두사(*) 설정이 있을 때만 수신 헤더를 순회한다.
if (whiteList.hasPrefix()) {
java.util.Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames != null && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!whiteList.matchesPrefix(headerName)) {
continue;
}
setHeader(headerName, request.getHeader(headerName), response);
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
private void setHeader(String headerName, String headerValue, HttpServletResponse response) {
if (NEVER_REFLECT.contains(headerName)) {
logger.debug("Skip Processing Key [" + headerName + "] in NEVER_REFLECT list");
return;
}
// 헤더명/값에 CR, LF 가 있으면 응답 분할(response splitting) 위험이 있다.
if (containsCrLf(headerName) || containsCrLf(headerValue)) {
logger.error("Skip Processing Key [" + headerName + "] - CR/LF detected in header name or value");
return;
}
response.setHeader(headerName, headerValue);
logger.debug("Processing Key [" + headerName + "], value [" + headerValue + "]");
}
private boolean containsCrLf(String value) {
return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0);
}
/**
* 어댑터 그룹 → 전역 순으로 property 를 찾아 허용 목록을 반환한다.
* property 값이 바뀌지 않는 동안은 파싱 결과를 재사용한다.
*/
private WhiteList getWhiteList(String adptGrpName) {
String cacheKey = StringUtils.defaultString(adptGrpName);
String propValue = findPropValue(adptGrpName);
CachedWhiteList cached = whiteListCache.get(cacheKey);
if (cached != null && StringUtils.equals(cached.propValue, propValue)) {
return cached.whiteList;
}
WhiteList whiteList = parse(propValue);
whiteListCache.put(cacheKey, new CachedWhiteList(propValue, whiteList));
return whiteList;
}
private String findPropValue(String adptGrpName) {
PropManager propManager = PropManager.getInstance();
if (StringUtils.isNotBlank(adptGrpName)) {
String value = propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES + "." + adptGrpName, "");
if (StringUtils.isNotBlank(value)) {
return StringUtils.trimToEmpty(value);
}
}
return StringUtils.trimToEmpty(propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, ""));
}
private static WhiteList parse(String propValue) {
Set<String> names = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
List<String> prefixes = new ArrayList<String>();
if (StringUtils.isNotBlank(propValue)) {
for (String token : propValue.split(",")) {
String name = StringUtils.trimToEmpty(token);
if (name.isEmpty()) {
continue;
}
if (name.endsWith("*")) {
String prefix = name.substring(0, name.length() - 1);
if (!prefix.isEmpty()) {
prefixes.add(prefix);
}
continue;
}
names.add(name);
}
}
return new WhiteList(names, prefixes);
}
private static Set<String> createHeaderSet(String... names) {
Set<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
Collections.addAll(set, names);
return Collections.unmodifiableSet(set);
}
/** 파싱된 허용 목록. 헤더명 완전일치 목록과 접두사(*) 목록으로 구성된다. */
private static class WhiteList {
private final Set<String> names;
private final List<String> prefixes;
WhiteList(Set<String> names, List<String> prefixes) {
this.names = Collections.unmodifiableSet(names);
this.prefixes = Collections.unmodifiableList(prefixes);
}
boolean isEmpty() {
return names.isEmpty() && prefixes.isEmpty();
}
Set<String> getNames() {
return names;
}
boolean hasPrefix() {
return !prefixes.isEmpty();
}
boolean matchesPrefix(String headerName) {
for (String prefix : prefixes) {
if (StringUtils.startsWithIgnoreCase(headerName, prefix)) {
return true;
}
}
return false;
}
}
private static class CachedWhiteList {
private final String propValue;
private final WhiteList whiteList;
CachedWhiteList(String propValue, WhiteList whiteList) {
this.propValue = propValue;
this.whiteList = whiteList;
}
}
}
@@ -0,0 +1,549 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;
/**
* ReflectHeaderFilter 단위 테스트.
*
* 복사 대상 헤더 목록은 PropManager(그룹: HttpHeaderFilter,
* 키: ReflectHeaderFilter.whiteList[.어댑터그룹명])에서 조회하므로
* PropManager 를 Mock 으로 등록하고 필터 로직만 검증한다.
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class ReflectHeaderFilterTest {
private static final String GRP = "TEST_GRP";
private static final String ADPT = "TEST_ADPT";
private static final String PROP_GROUP = ReflectHeaderFilter.PROPERTIES_GROUP_NAME;
private static final String KEY = ReflectHeaderFilter.HEADER_KEY_NAMES;
private static final String KEY_GRP = KEY + "." + GRP;
private static GenericApplicationContext ctx;
private static PropManager mockPropManager;
private ReflectHeaderFilter 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();
}
@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();
// 미설정 키는 실제 PropManager 와 동일하게 기본값을 반환한다.
when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
.thenAnswer(inv -> inv.getArgument(2));
// 필터별 캐시가 테스트간에 섞이지 않도록 매번 새 인스턴스를 사용한다.
filter = new ReflectHeaderFilter();
}
/** 프로퍼티 설정 - 어댑터 그룹 단위 키. */
private void givenGroupWhiteList(String value) {
when(mockPropManager.getProperty(PROP_GROUP, KEY_GRP, "")).thenReturn(value);
}
/** 프로퍼티 설정 - 전역 키. */
private void givenGlobalWhiteList(String value) {
when(mockPropManager.getProperty(PROP_GROUP, KEY, "")).thenReturn(value);
}
/** 수신 요청 헤더 설정. getHeader 는 서블릿 스펙대로 대소문자를 구분하지 않는다. */
private void givenRequestHeaders(String... nameValuePairs) {
Map<String, String> headers = new LinkedHashMap<>();
for (int i = 0; i < nameValuePairs.length; i += 2) {
headers.put(nameValuePairs[i], nameValuePairs[i + 1]);
}
when(mockRequest.getHeader(anyString())).thenAnswer(inv -> {
String wanted = inv.getArgument(0);
for (Map.Entry<String, String> e : headers.entrySet()) {
if (e.getKey().equalsIgnoreCase(wanted)) return e.getValue();
}
return null;
});
// doPreFilter/doPostFilter 가 각각 순회할 수 있도록 호출마다 새 Enumeration 을 반환한다.
when(mockRequest.getHeaderNames())
.thenAnswer(inv -> Collections.enumeration(new ArrayList<>(headers.keySet())));
}
// =========================================================================
// 1. 허용 목록에 등록된 헤더만 복사
// =========================================================================
@Test
@DisplayName("1-1. 허용 목록에 등록된 헤더만 응답으로 복사한다")
void testWhiteListedHeadersOnly() throws Exception {
givenGroupWhiteList("x-obp-txid, x-elink-client-id");
givenRequestHeaders(
"x-obp-txid", "TX-001",
"x-elink-client-id", "CLIENT-A",
"Authorization", "Bearer secret",
"Cookie", "JSESSIONID=abc");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-001");
verify(mockResponse).setHeader("x-elink-client-id", "CLIENT-A");
verify(mockResponse, never()).setHeader(eq("Authorization"), anyString());
verify(mockResponse, never()).setHeader(eq("Cookie"), anyString());
verifyNoMoreInteractions(mockResponse);
}
@Test
@DisplayName("1-2. 헤더명 대소문자를 구분하지 않고 매칭한다")
void testCaseInsensitiveMatch() throws Exception {
givenGroupWhiteList("X-OBP-TXID");
givenRequestHeaders("x-obp-txid", "TX-002");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("X-OBP-TXID", "TX-002");
}
@Test
@DisplayName("1-3. 허용 목록에 있어도 수신 요청에 없는 헤더는 복사하지 않는다")
void testAbsentHeaderNotReflected() throws Exception {
givenGroupWhiteList("x-obp-txid, x-not-sent");
givenRequestHeaders("x-obp-txid", "TX-003");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-003");
verify(mockResponse, never()).setHeader(eq("x-not-sent"), any());
}
@Test
@DisplayName("1-4. 값이 빈 문자열인 헤더도 복사한다")
void testEmptyValueReflected() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "");
}
@Test
@DisplayName("1-5. 목록의 공백/빈 항목은 무시한다")
void testBlankTokensIgnored() throws Exception {
givenGroupWhiteList(" x-obp-txid , , , ");
givenRequestHeaders("x-obp-txid", "TX-005");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-005");
verifyNoMoreInteractions(mockResponse);
}
// =========================================================================
// 2. 프로퍼티 조회 우선순위 (어댑터 그룹 → 전역)
// =========================================================================
@Test
@DisplayName("2-1. 어댑터 그룹 키가 있으면 전역 키를 무시한다(override)")
void testGroupKeyOverridesGlobal() throws Exception {
givenGlobalWhiteList("x-global-only");
givenGroupWhiteList("x-group-only");
givenRequestHeaders(
"x-global-only", "G",
"x-group-only", "S");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-group-only", "S");
verify(mockResponse, never()).setHeader(eq("x-global-only"), anyString());
}
@Test
@DisplayName("2-2. 어댑터 그룹 키가 없으면 전역 키를 사용한다")
void testFallbackToGlobalKey() throws Exception {
givenGlobalWhiteList("x-global-only");
givenRequestHeaders("x-global-only", "G");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-global-only", "G");
}
@Test
@DisplayName("2-3. 어댑터 그룹 키가 공백이면 전역 키로 폴백한다")
void testBlankGroupKeyFallsBackToGlobal() throws Exception {
givenGroupWhiteList(" ");
givenGlobalWhiteList("x-global-only");
givenRequestHeaders("x-global-only", "G");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-global-only", "G");
}
@Test
@DisplayName("2-4. 어댑터 그룹마다 다른 목록을 적용한다")
void testPerGroupWhiteList() throws Exception {
when(mockPropManager.getProperty(PROP_GROUP, KEY + ".GRP_A", "")).thenReturn("x-a");
when(mockPropManager.getProperty(PROP_GROUP, KEY + ".GRP_B", "")).thenReturn("x-b");
givenRequestHeaders("x-a", "A", "x-b", "B");
filter.doPreFilter("GRP_A", ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-a", "A");
verify(mockResponse, never()).setHeader(eq("x-b"), anyString());
reset(mockResponse);
filter.doPreFilter("GRP_B", ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-b", "B");
verify(mockResponse, never()).setHeader(eq("x-a"), anyString());
}
@Test
@DisplayName("2-5. 어댑터 그룹명이 null 이어도 전역 키로 동작한다")
void testNullGroupName() throws Exception {
givenGlobalWhiteList("x-global-only");
givenRequestHeaders("x-global-only", "G");
filter.doPreFilter(null, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-global-only", "G");
}
@Test
@DisplayName("2-6. 어댑터명은 프로퍼티 키 조회에 사용하지 않는다")
void testAdapterNameNotUsedInKeyLookup() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-006");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockPropManager, never()).getProperty(eq(PROP_GROUP), contains(ADPT), anyString());
}
// =========================================================================
// 3. 미설정 시 아무 헤더도 복사하지 않음
// =========================================================================
@Test
@DisplayName("3-1. 설정이 없으면 어떤 헤더도 복사하지 않는다")
void testNoConfigReflectsNothing() throws Exception {
givenRequestHeaders("x-obp-txid", "TX-007", "Authorization", "Bearer secret");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
@Test
@DisplayName("3-2. 설정값이 콤마뿐이어도 예외 없이 아무것도 복사하지 않는다")
void testCommaOnlyConfigReflectsNothing() throws Exception {
givenGroupWhiteList(",,,");
givenRequestHeaders("x-obp-txid", "TX-008");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
// =========================================================================
// 4. 접두사(*) 매칭
// =========================================================================
@Test
@DisplayName("4-1. 접두사(*) 설정은 해당 접두사로 시작하는 수신 헤더를 모두 복사한다")
void testPrefixMatch() throws Exception {
givenGroupWhiteList("X-KKB-*");
givenRequestHeaders(
"X-KKB-API-NAME", "transfer",
"X-KKB-API-TX-ID", "TX-009",
"X-OBP-TXID", "OTHER");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
verify(mockResponse).setHeader("X-KKB-API-TX-ID", "TX-009");
verify(mockResponse, never()).setHeader(eq("X-OBP-TXID"), anyString());
}
@Test
@DisplayName("4-2. 접두사 매칭도 대소문자를 구분하지 않는다")
void testPrefixMatchCaseInsensitive() throws Exception {
givenGroupWhiteList("x-kkb-*");
givenRequestHeaders("X-KKB-API-NAME", "transfer");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
}
@Test
@DisplayName("4-3. 완전일치와 접두사를 함께 설정할 수 있다")
void testExactAndPrefixTogether() throws Exception {
givenGroupWhiteList("x-obp-txid, X-KKB-*");
givenRequestHeaders(
"x-obp-txid", "TX-010",
"X-KKB-API-NAME", "transfer",
"Authorization", "Bearer secret");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-010");
verify(mockResponse).setHeader("X-KKB-API-NAME", "transfer");
verify(mockResponse, never()).setHeader(eq("Authorization"), anyString());
}
@Test
@DisplayName("4-4. 접두사 설정이 없으면 수신 헤더 전체를 순회하지 않는다")
void testNoPrefixSkipsHeaderNamesScan() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-011");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockRequest, never()).getHeaderNames();
}
@Test
@DisplayName("4-5. '*' 단독 설정은 접두사가 비어 무시된다")
void testBareAsteriskIgnored() throws Exception {
givenGroupWhiteList("*");
givenRequestHeaders("x-obp-txid", "TX-012");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
@Test
@DisplayName("4-6. getHeaderNames() 가 null 이어도 예외가 발생하지 않는다")
void testNullHeaderNamesEnumeration() throws Exception {
givenGroupWhiteList("X-KKB-*");
when(mockRequest.getHeaderNames()).thenReturn(null);
assertDoesNotThrow(() -> filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse));
verifyNoInteractions(mockResponse);
}
// =========================================================================
// 5. 복사 금지 헤더 / CRLF 방어
// =========================================================================
@Test
@DisplayName("5-1. 응답 프레이밍 헤더는 허용 목록에 있어도 복사하지 않는다")
void testNeverReflectHeaders() throws Exception {
givenGroupWhiteList("Content-Length, Transfer-Encoding, Connection, Keep-Alive, Upgrade, TE, Trailer, x-obp-txid");
givenRequestHeaders(
"Content-Length", "100",
"Transfer-Encoding", "chunked",
"Connection", "keep-alive",
"Keep-Alive", "timeout=5",
"Upgrade", "websocket",
"TE", "trailers",
"Trailer", "Expires",
"x-obp-txid", "TX-013");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-013");
verifyNoMoreInteractions(mockResponse);
}
@Test
@DisplayName("5-2. 복사 금지 헤더 판정도 대소문자를 구분하지 않는다")
void testNeverReflectCaseInsensitive() throws Exception {
givenGroupWhiteList("content-length");
givenRequestHeaders("content-length", "100");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
@Test
@DisplayName("5-3. 접두사 매칭으로 걸린 헤더도 복사 금지 목록이 우선한다")
void testNeverReflectAppliesToPrefixMatch() throws Exception {
givenGroupWhiteList("Content-*");
givenRequestHeaders("Content-Length", "100", "Content-MD5", "abc");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse, never()).setHeader(eq("Content-Length"), anyString());
verify(mockResponse).setHeader("Content-MD5", "abc");
}
@Test
@DisplayName("5-4. 값에 CR/LF 가 있으면 응답 분할 방지를 위해 복사하지 않는다")
void testCrLfInValueBlocked() throws Exception {
givenGroupWhiteList("x-obp-txid, x-safe");
givenRequestHeaders(
"x-obp-txid", "TX\r\nX-Injected: 1",
"x-safe", "OK");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse, never()).setHeader(eq("x-obp-txid"), anyString());
verify(mockResponse).setHeader("x-safe", "OK");
}
@Test
@DisplayName("5-5. LF 단독, CR 단독 값도 차단한다")
void testLoneCrOrLfBlocked() throws Exception {
givenGroupWhiteList("x-lf, x-cr");
givenRequestHeaders("x-lf", "a\nb", "x-cr", "a\rb");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
// =========================================================================
// 6. doPreFilter / doPostFilter 반환값
// =========================================================================
@Test
@DisplayName("6-1. doPreFilter 는 원 메시지를 그대로 반환한다")
void testDoPreFilterReturnsMessage() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-014");
Object message = "request-body";
Object result = filter.doPreFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
assertSame(message, result);
}
@Test
@DisplayName("6-2. doPostFilter 도 헤더를 복사하고 원 메시지를 그대로 반환한다")
void testDoPostFilterReflectsAndReturnsMessage() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-015");
Object message = "response-body";
Object result = filter.doPostFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-015");
assertSame(message, result);
}
@Test
@DisplayName("6-3. setHeader 사용으로 pre/post 연속 호출 시 헤더가 중복되지 않는다")
void testPreAndPostUseSetHeader() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-016");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
filter.doPostFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse, times(2)).setHeader("x-obp-txid", "TX-016");
verify(mockResponse, never()).addHeader(anyString(), anyString());
}
@Test
@DisplayName("6-4. 응답 헤더 설정 중 예외가 발생해도 필터는 메시지를 반환한다")
void testExceptionSwallowed() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-017");
doThrow(new IllegalStateException("response already committed"))
.when(mockResponse).setHeader(anyString(), anyString());
Object result = filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
assertEquals("msg", result);
}
// =========================================================================
// 7. 프로퍼티 캐시
// =========================================================================
@Test
@DisplayName("7-1. 같은 설정으로 반복 호출해도 동작이 동일하다(캐시 재사용)")
void testCacheReuse() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-018");
for (int i = 0; i < 3; i++) {
reset(mockResponse);
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-018");
verifyNoMoreInteractions(mockResponse);
}
}
@Test
@DisplayName("7-2. 프로퍼티가 변경되면 다음 호출에 즉시 반영된다")
void testCacheInvalidatedOnPropertyChange() throws Exception {
givenGroupWhiteList("x-first");
givenRequestHeaders("x-first", "F", "x-second", "S");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-first", "F");
reset(mockResponse);
givenGroupWhiteList("x-second");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-second", "S");
verify(mockResponse, never()).setHeader(eq("x-first"), anyString());
}
@Test
@DisplayName("7-3. 설정이 제거되면 다음 호출부터 아무것도 복사하지 않는다")
void testCacheInvalidatedOnPropertyRemoval() throws Exception {
givenGroupWhiteList("x-obp-txid");
givenRequestHeaders("x-obp-txid", "TX-019");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verify(mockResponse).setHeader("x-obp-txid", "TX-019");
reset(mockResponse);
givenGroupWhiteList("");
filter.doPreFilter(GRP, ADPT, "msg", prop, mockRequest, mockResponse);
verifyNoInteractions(mockResponse);
}
}