- 본문·수신자 마스킹 유틸(MessagePatternMaskingUtils) 개발 - 안전 HTML 생성 마스킹/검증 테스트 및 JSP 바인딩
This commit is contained in:
+17
@@ -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> {
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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 → <span class="mask-email">**st@gmail.com</span>
|
||||
* 휴대폰(하이픈) 010-1234-5678 → <span class="mask-phone">010-12**-56**</span>
|
||||
* 6자리+ 숫자 123456 → <span class="mask-digits">123***</span> (앞 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;
|
||||
|
||||
/**
|
||||
* 매칭 우선순위(이메일 > 하이픈 휴대폰 > 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("&"); break;
|
||||
case '<': sb.append("<"); break;
|
||||
case '>': sb.append(">"); break;
|
||||
case '"': sb.append("""); break;
|
||||
case '\'': sb.append("'"); 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);
|
||||
}
|
||||
}
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
+135
@@ -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("<b>hi</b>",
|
||||
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("<script>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("앰퍼샌드 escape")
|
||||
void maskAndHighlight_escapesAmp() {
|
||||
assertEquals("a&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(""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user