- 메시지 발송 내역 화면 추가: 조회/상세/상태변경(PENDING→FAILED)
eapim-admin CI / build (push) Has been cancelled

- 본문·수신자 마스킹 유틸(MessagePatternMaskingUtils) 개발
- 안전 HTML 생성 마스킹/검증 테스트 및 JSP 바인딩
This commit is contained in:
Rinjae
2026-06-19 19:40:15 +09:00
parent efd1c748ed
commit 655c35b1e1
11 changed files with 940 additions and 1 deletions
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 조회용 admin 전용 리포지토리.
*
* <p>elink-portal-common 의 {@code template.repository.MessageRequestRepository}(JpaRepository) 와
* <b>단순 클래스명이 겹치면 Spring Data 빈 이름이 충돌</b>하므로(둘 다 admin 컨텍스트에 스캔됨)
* 이름을 분리한다. 또한 QueryDSL {@code findAll(predicate, pageable)} 를 쓰기 위해
* {@link BaseRepository} 를 상속한다.</p>
*/
@EMSDataSource
public interface MessageRequestEmsRepository extends BaseRepository<MessageRequest, String> {
}
@@ -0,0 +1,65 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.entity.QMessageRequest;
import com.eactive.eai.rms.common.util.StringUtils;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.eactive.eai.rms.onl.common.exception.BizException;
import com.querydsl.core.BooleanBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class MessageRequestService
extends AbstractEMSDataSerivce<MessageRequest, String, MessageRequestEmsRepository> {
public Page<MessageRequest> findAll(Pageable pageable, MessageRequestUISearch search) {
QMessageRequest q = QMessageRequest.messageRequest;
BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(search.getSearchRequestStatus())) {
predicate.and(q.requestStatus.eq(search.getSearchRequestStatus()));
}
if (StringUtils.isNotBlank(search.getSearchMessageCode())) {
MessageCode.findServiceId(search.getSearchMessageCode())
.ifPresent(code -> predicate.and(q.messageCode.eq(code)));
}
if (StringUtils.isNotBlank(search.getSearchMessageType())) {
predicate.and(q.messageType.eq(search.getSearchMessageType()));
}
if (StringUtils.isNotBlank(search.getSearchRecipient())) {
predicate.and(q.username.containsIgnoreCase(search.getSearchRecipient()));
}
// requestDate 는 yyyyMMddHHmmss 14자리 문자열로 저장(LocalDateTimeToStringConverter14)되므로
// 고정폭 비교로 기간 필터가 동작한다.
if (search.getSearchFromDate() != null) {
predicate.and(q.requestDate.goe(search.getSearchFromDate().atStartOfDay()));
}
if (search.getSearchToDate() != null) {
predicate.and(q.requestDate.loe(search.getSearchToDate().atTime(23, 59, 59)));
}
// 발송요청 역순 고정 (Pageable 에 정렬이 없을 때의 안전장치)
Sort base = Sort.by(Sort.Direction.DESC, "requestDate");
Sort sort = pageable.getSort().isSorted() ? pageable.getSort() : base;
Pageable fixed = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
return repository.findAll(predicate, fixed);
}
/** PENDING 상태인 건만 FAILED 로 전환한다. */
public void updateStatusToFailedIfPending(String id) {
MessageRequest entity = getById(id);
if (!"PENDING".equals(entity.getRequestStatus())) {
throw new BizException("PENDING 상태인 건만 변경할 수 있습니다.");
}
entity.setRequestStatus("FAILED");
save(entity);
}
}
@@ -0,0 +1,29 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
public class MessageRequestUISearch {
/* 발송 상태 (PENDING/SENT/FAILED) */
private String searchRequestStatus;
/* 메세지 코드 (MessageCode enum name) */
private String searchMessageCode;
/* 메세지 유형 (EMAIL/SMS 등) */
private String searchMessageType;
/* 수신자 이름 (MessageRequest.username 평문 컬럼 대상, LIKE 검색) */
private String searchRecipient;
/* 요청일시 기간 검색 (email/phone 은 암호화 저장이라 검색 제외) */
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate searchFromDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate searchToDate;
}
@@ -0,0 +1,117 @@
package com.eactive.eai.rms.onl.apim.masking;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 메세지 본문/메신저ID 패턴 기반 마스킹 유틸 (발송 내역 화면 전용).
*
* <p>본문(message) 안에 섞여 있는 개인정보/인증정보를 패턴으로 찾아 마스킹하고,
* 패턴 종류별로 색상 강조 {@code <span>} 으로 감싼 <b>안전한 HTML</b> 을 만든다.
* 매칭되지 않은 영역과 마스킹 결과 모두 HTML escape 하므로 본문에 스크립트가 있어도 무력화된다.</p>
*
* <pre>
* 이메일 test@gmail.com → &lt;span class="mask-email"&gt;**st@gmail.com&lt;/span&gt;
* 휴대폰(하이픈) 010-1234-5678 → &lt;span class="mask-phone"&gt;010-12**-56**&lt;/span&gt;
* 6자리+ 숫자 123456 → &lt;span class="mask-digits"&gt;123***&lt;/span&gt; (앞 3자리 제외)
* 메신저ID kakao_abcd → kakao_ab** (뒤 2자리)
* </pre>
*
* <p>실제 이메일/휴대폰 마스킹 규칙은 표준 구현
* {@link com.eactive.eai.common.util.MaskingUtils}(elink-online-core)에 위임한다.</p>
*/
public final class MessagePatternMaskingUtils {
private MessagePatternMaskingUtils() {}
private static final int DIGITS_KEEP_HEAD = 3;
/**
* 매칭 우선순위(이메일 &gt; 하이픈 휴대폰 &gt; 6자리+ 숫자)를 단일 패스로 보장하는 결합 정규식.
* 휴대폰 하이픈 형식이 "6자리 이상 숫자" 규칙에 이중 적용되지 않도록 한 Matcher 로 순회한다.
*/
private static final Pattern COMBINED = Pattern.compile(
"(?<email>[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})"
+ "|(?<phone>01[016789]-\\d{3,4}-\\d{4})"
+ "|(?<digits>\\d{6,})");
/** 본문 → escape + 패턴별 마스킹/색상강조 span 이 적용된 안전 HTML 반환. */
public static String maskAndHighlight(String raw) {
if (raw == null || raw.isEmpty()) {
return "";
}
Matcher m = COMBINED.matcher(raw);
StringBuilder sb = new StringBuilder();
int last = 0;
while (m.find()) {
sb.append(escapeHtml(raw.substring(last, m.start())));
String token = m.group();
if (m.group("email") != null) {
sb.append(span("mask-email", com.eactive.eai.common.util.MaskingUtils.maskEmail(token)));
} else if (m.group("phone") != null) {
sb.append(span("mask-phone", com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(token)));
} else {
sb.append(span("mask-digits", maskKeepHead(token, DIGITS_KEEP_HEAD)));
}
last = m.end();
}
sb.append(escapeHtml(raw.substring(last)));
return sb.toString();
}
/** 메신저 ID 마스킹: 뒤 2자리(2자 이하는 전체) 마스킹. */
public static String maskMessengerId(String id) {
if (id == null || id.isEmpty()) {
return id;
}
if (id.length() <= 2) {
return repeat('*', id.length());
}
return id.substring(0, id.length() - 2) + "**";
}
/** 앞 head 자리만 남기고 나머지를 '*' 로 마스킹. */
private static String maskKeepHead(String digits, int head) {
if (digits.length() <= head) {
return digits;
}
return digits.substring(0, head) + repeat('*', digits.length() - head);
}
private static String span(String cls, String inner) {
return "<span class=\"" + cls + "\">" + escapeHtml(inner) + "</span>";
}
/**
* 최소 HTML escape. {@code < > & " '} 만 변환하고 한글 등 비-ASCII 문자는 보존한다.
* (commons-lang {@code escapeHtml} 은 0x7F 초과 문자를 {@code &#NNN;} 로 변환해 한글이 깨지므로 직접 구현)
*/
private static String escapeHtml(String s) {
if (s == null || s.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&': sb.append("&amp;"); break;
case '<': sb.append("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '"': sb.append("&quot;"); break;
case '\'': sb.append("&#39;"); break;
default: sb.append(c);
}
}
return sb.toString();
}
private static String repeat(char c, int n) {
if (n <= 0) {
return "";
}
char[] a = new char[n];
Arrays.fill(a, c);
return new String(a);
}
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import com.eactive.eai.rms.common.base.BaseAnnotationController;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestUISearch;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.SortDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.HashMap;
import java.util.Map;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 조회 화면 컨트롤러.
*
* <p>조회 전용 + PENDING→FAILED 상태 전환만 제공한다. (등록/수정/삭제 없음)</p>
*/
@Controller
@RequiredArgsConstructor
public class MessageRequestManController extends BaseAnnotationController {
private final MessageRequestManService messageRequestManService;
@GetMapping(value = "/onl/apim/messagerequest/messageRequestMan.view")
public void view() {
// 목록 view
}
@GetMapping(value = "/onl/apim/messagerequest/messageRequestMan.view", params = "cmd=DETAIL")
public String detailView() {
return "/onl/apim/messagerequest/messageRequestManDetail";
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<MessageRequestUI>> selectList(
@SortDefault(sort = "requestDate", direction = Sort.Direction.DESC) Pageable pageable,
MessageRequestUISearch search) {
Page<MessageRequestUI> page = messageRequestManService.selectList(pageable, search);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=LIST_INIT_COMBO")
public ResponseEntity<Map<String, Object>> initCombo() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("statusList", messageRequestManService.statusCombo());
resultMap.put("messageCodeList", messageRequestManService.messageCodeCombo());
return ResponseEntity.ok(resultMap);
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=DETAIL")
public ResponseEntity<MessageRequestUI> selectDetail(String id) {
return ResponseEntity.ok(messageRequestManService.selectDetail(id));
}
/** PENDING 건을 FAILED 로 전환 (감사 포인트: UPDATE_STATUS). */
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=UPDATE_STATUS")
public ResponseEntity<Void> updateStatus(String id) {
messageRequestManService.updateStatusToFailed(id);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,106 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.combo.ComboVo;
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestUISearch;
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
import com.eactive.eai.rms.onl.apim.masking.MessagePatternMaskingUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class MessageRequestManService extends BaseService {
// KST 한글 요일 포맷 (표시용 / 툴팁용)
private static final DateTimeFormatter FMT_SHORT = DateTimeFormatter.ofPattern("MM-dd(E) HH:mm", Locale.KOREA);
private static final DateTimeFormatter FMT_FULL = DateTimeFormatter.ofPattern("yyyy-MM-dd (EEE) HH:mm:ss", Locale.KOREA);
private final com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService messageRequestService;
@Autowired
public MessageRequestManService(com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService messageRequestService) {
this.messageRequestService = messageRequestService;
}
public Page<MessageRequestUI> selectList(Pageable pageable, MessageRequestUISearch search) {
return messageRequestService.findAll(pageable, search).map(this::convertToUI);
}
public MessageRequestUI selectDetail(String id) {
return convertToUI(messageRequestService.getById(id));
}
/** PENDING → FAILED 전환. */
public void updateStatusToFailed(String id) {
messageRequestService.updateStatusToFailedIfPending(id);
}
/** 엔티티 → 마스킹/포맷 적용된 UI 변환 (목록·상세 공통). */
private MessageRequestUI convertToUI(MessageRequest e) {
MessageRequestUI ui = new MessageRequestUI();
ui.setId(e.getId());
ui.setMessageCode(e.getMessageCode() != null ? e.getMessageCode().name() : null);
ui.setMessageCodeName(e.getMessageCode() != null ? e.getMessageCode().getDescription() : null);
ui.setSubject(e.getSubject());
ui.setMessageType(e.getMessageType());
// 본문: 패턴 마스킹 + 색상강조 안전 HTML
ui.setMessageHtml(MessagePatternMaskingUtils.maskAndHighlight(e.getMessage()));
// 수신자 마스킹 (email/phone 은 @Convert 로 이미 복호화된 평문)
ui.setUsername(MaskingUtils.maskName(e.getUsername()));
ui.setEmail(MaskingUtils.maskEmailId(e.getEmail()));
ui.setPhone(MaskingUtils.maskPhoneNumber(e.getPhone()));
ui.setMessengerId(MessagePatternMaskingUtils.maskMessengerId(e.getMessengerId()));
ui.setUmsUid(e.getUmsUid());
ui.setRequestStatus(e.getRequestStatus());
ui.setEaiInterfaceId(e.getEaiInterfaceId());
ui.setServiceId(e.getServiceId());
ui.setRequestDateShort(fmt(e.getRequestDate(), FMT_SHORT));
ui.setRequestDateFull(fmt(e.getRequestDate(), FMT_FULL));
ui.setSentDateShort(fmt(e.getSentDate(), FMT_SHORT));
ui.setSentDateFull(fmt(e.getSentDate(), FMT_FULL));
return ui;
}
private static String fmt(LocalDateTime t, DateTimeFormatter f) {
return t == null ? "" : t.format(f);
}
/** 상태 검색 콤보 (PENDING/SENT/FAILED). */
public List<ComboVo> statusCombo() {
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED"})
.map(s -> {
ComboVo v = new ComboVo();
v.setCode(s);
v.setCodename(s);
return v;
}).collect(Collectors.toList());
}
/** 메세지 코드 검색 콤보 (enum name → 한글 설명). */
public List<ComboVo> messageCodeCombo() {
return Arrays.stream(MessageCode.values())
.map(c -> {
ComboVo v = new ComboVo();
v.setCode(c.name());
v.setCodename(c.getDescription());
return v;
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,42 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import lombok.Data;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 화면 응답 DTO.
*
* <p>개인정보는 모두 마스킹된 값으로 채워지며, 본문(messageHtml)은 패턴 마스킹 + 색상강조가 적용된
* 안전한 HTML 이다. 날짜는 표시용(short)/툴팁용(full) 두 가지 포맷 문자열로 제공한다.</p>
*/
@Data
public class MessageRequestUI {
private String id;
/* 제목/코드 — subject 가 없으면 화면에서 messageCode 로 대체 표기 */
private String messageCode; // MessageCode enum name (제목 마우스오버/대체용)
private String messageCodeName; // MessageCode.getDescription() 한글명
private String subject;
private String messageType; // EMAIL/SMS 등
/* 본문 — 패턴 마스킹 + span 색상강조된 안전 HTML */
private String messageHtml;
/* 수신자 (마스킹) */
private String username;
private String email;
private String phone;
private String messengerId;
/* 발송 메타 */
private String umsUid;
private String requestStatus; // PENDING/SENT/FAILED
private String eaiInterfaceId;
private String serviceId;
/* 요청/발송 시간 — 표시용(short) / 툴팁용(full) */
private String requestDateShort; // MM-dd(E) HH:mm
private String requestDateFull; // yyyy-MM-dd (EEE) HH:mm:ss
private String sentDateShort;
private String sentDateFull;
}
@@ -0,0 +1,135 @@
package com.eactive.eai.rms.onl.apim.masking;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* 메세지 본문/메신저ID 패턴 마스킹 + 색상강조 + XSS escape 검증.
*/
class MessagePatternMaskingUtilsTest {
// --- 이메일 ---
@Test
@DisplayName("본문 이메일 — mask-email span + 표준 이메일 마스킹")
void maskAndHighlight_email() {
assertEquals("<span class=\"mask-email\">**st1**@test.com</span>",
MessagePatternMaskingUtils.maskAndHighlight("test123@test.com"));
}
// --- 휴대폰(하이픈) ---
@Test
@DisplayName("본문 휴대폰(하이픈) — mask-phone span + 세그먼트별 뒤2자리 마스킹")
void maskAndHighlight_phoneHyphen() {
assertEquals("<span class=\"mask-phone\">010-12**-56**</span>",
MessagePatternMaskingUtils.maskAndHighlight("010-1234-5678"));
}
// --- 6자리 이상 숫자 (앞 3자리 제외 전체 마스킹) ---
@Test
@DisplayName("본문 6자리 숫자 — 앞 3자리 제외 전체 마스킹(mask-digits)")
void maskAndHighlight_digits6() {
assertEquals("<span class=\"mask-digits\">123***</span>",
MessagePatternMaskingUtils.maskAndHighlight("123456"));
}
@Test
@DisplayName("본문 하이픈 없는 휴대폰(11자리) — 6자리+ 숫자 규칙 적용")
void maskAndHighlight_phoneNoHyphen() {
assertEquals("<span class=\"mask-digits\">010********</span>",
MessagePatternMaskingUtils.maskAndHighlight("01012345678"));
}
@Test
@DisplayName("5자리 이하 숫자는 마스킹하지 않음")
void maskAndHighlight_digitsUnder6() {
assertEquals("12345", MessagePatternMaskingUtils.maskAndHighlight("12345"));
}
// --- 우선순위 / 혼합 ---
@Test
@DisplayName("하이픈 휴대폰이 숫자 규칙에 이중 적용되지 않음 (phone 우선)")
void maskAndHighlight_priority() {
String result = MessagePatternMaskingUtils.maskAndHighlight("010-1234-5678");
assertTrue(result.contains("mask-phone"));
assertFalse(result.contains("mask-digits"));
}
@Test
@DisplayName("텍스트 + 이메일/휴대폰/숫자 혼합 — 각 패턴별 span 생성, 일반 텍스트는 보존")
void maskAndHighlight_mixed() {
String result = MessagePatternMaskingUtils.maskAndHighlight(
"인증요청 test123@test.com 전화 010-1234-5678 코드 123456 끝");
assertTrue(result.contains("<span class=\"mask-email\">**st1**@test.com</span>"));
assertTrue(result.contains("<span class=\"mask-phone\">010-12**-56**</span>"));
assertTrue(result.contains("<span class=\"mask-digits\">123***</span>"));
assertTrue(result.contains("인증요청"));
assertTrue(result.contains(""));
// 원문 일부가 그대로 노출되지 않아야 함
assertFalse(result.contains("test123@"));
assertFalse(result.contains("010-1234-5678"));
assertFalse(result.contains("123456"));
}
// --- XSS / HTML escape ---
@Test
@DisplayName("매칭 외 영역의 HTML 태그는 escape 되어 무력화")
void maskAndHighlight_escapesHtml() {
assertEquals("&lt;b&gt;hi&lt;/b&gt;",
MessagePatternMaskingUtils.maskAndHighlight("<b>hi</b>"));
}
@Test
@DisplayName("script 태그 escape")
void maskAndHighlight_escapesScript() {
String result = MessagePatternMaskingUtils.maskAndHighlight("<script>alert(1)</script>");
assertFalse(result.contains("<script>"));
assertTrue(result.contains("&lt;script&gt;"));
}
@Test
@DisplayName("앰퍼샌드 escape")
void maskAndHighlight_escapesAmp() {
assertEquals("a&amp;b", MessagePatternMaskingUtils.maskAndHighlight("a&b"));
}
// --- null / empty ---
@Test
@DisplayName("null·빈문자 본문은 빈문자 반환")
void maskAndHighlight_nullEmpty() {
assertEquals("", MessagePatternMaskingUtils.maskAndHighlight(null));
assertEquals("", MessagePatternMaskingUtils.maskAndHighlight(""));
}
// --- 메신저 ID ---
@Test
@DisplayName("메신저ID — 뒤 2자리 마스킹")
void maskMessengerId_normal() {
assertEquals("kakao_ab**", MessagePatternMaskingUtils.maskMessengerId("kakao_abcd"));
}
@Test
@DisplayName("메신저ID — 2자 이하 전체 마스킹")
void maskMessengerId_short() {
assertEquals("**", MessagePatternMaskingUtils.maskMessengerId("ab"));
assertEquals("*", MessagePatternMaskingUtils.maskMessengerId("a"));
}
@Test
@DisplayName("메신저ID — null·빈문자 방어")
void maskMessengerId_nullEmpty() {
assertNull(MessagePatternMaskingUtils.maskMessengerId(null));
assertEquals("", MessagePatternMaskingUtils.maskMessengerId(""));
}
}