ReflectHeaderFilter 필터 추가
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+549
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user