Merge branch 'master' into feats/security

# Conflicts:
#	src/main/java/com/eactive/apim/portal/djb/webhook/dto/WebhookRegistrationDTO.java
#	src/main/resources/templates/views/apps/webhook/webhookList.html
#	src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
#	src/main/resources/templates/views/apps/webhook/webhookRegisterStep1.html
This commit is contained in:
Rinjae
2026-08-27 13:23:15 +09:00
150 changed files with 7280 additions and 558 deletions
@@ -0,0 +1,143 @@
package com.eactive.apim.portal.apps.auth.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
import com.eactive.apim.portal.apps.user.dto.PortalOrgDTO;
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
import com.eactive.apim.portal.common.util.SecurityUtil;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
/**
* org 축·role 축은 각자 독립적으로 선택적 제한이며 AND 로 합쳐야 한다.
*
* <p>과거 OR 합성 결함: displayRoleCode 를 비워두면(=역할 제한 없음) roleMatch 가
* "로그인만 했으면" 무조건 true 가 되어, displayOrg 만 설정한 법인전용 API 가 다른 법인
* 로그인 사용자에게도 그대로 보였다 — 오직 비로그인 사용자만 걸러졌다.</p>
*/
class ApiPermissionFilterTest {
private static final String ORG_A = "org-a";
private static final String ORG_B = "org-b";
private static final String ROLE_MANAGER = "ROLE_CORP_MANAGER";
private static final String ROLE_USER = "ROLE_CORP_USER";
// ── checkApiPermissions (ApiSpecInfoDto) ──────────────────────────
@Test
void checkApiPermissions_noRestriction_visibleToAnonymous() {
ApiSpecInfoDto spec = apiSpec(null, null);
assertTrue(ApiPermissionFilter.checkApiPermissions(spec, false, null, null));
}
@Test
void checkApiPermissions_orgOnlyRestriction_hiddenFromAnonymous() {
ApiSpecInfoDto spec = apiSpec(ORG_A, null);
assertFalse(ApiPermissionFilter.checkApiPermissions(spec, false, null, null));
}
@Test
void checkApiPermissions_orgOnlyRestriction_visibleToMatchingOrg() {
ApiSpecInfoDto spec = apiSpec(ORG_A, null);
assertTrue(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_A, ROLE_MANAGER));
}
@Test
void checkApiPermissions_orgOnlyRestriction_hiddenFromDifferentOrg() {
// 회귀 케이스 — 과거 OR 버그에서는 displayRoleCode 가 비어 roleMatch=true 가 되어 여기서 true 가 나왔다.
ApiSpecInfoDto spec = apiSpec(ORG_A, null);
assertFalse(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_B, ROLE_MANAGER));
}
@Test
void checkApiPermissions_roleOnlyRestriction_hiddenFromNonMatchingRole() {
ApiSpecInfoDto spec = apiSpec(null, ROLE_MANAGER);
assertFalse(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_A, ROLE_USER));
}
@Test
void checkApiPermissions_roleOnlyRestriction_visibleRegardlessOfOrg() {
ApiSpecInfoDto spec = apiSpec(null, ROLE_MANAGER);
assertTrue(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_B, ROLE_MANAGER));
}
@Test
void checkApiPermissions_bothRestrictions_requireIntersection() {
ApiSpecInfoDto spec = apiSpec(ORG_A, ROLE_MANAGER);
assertTrue(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_A, ROLE_MANAGER));
// org 는 맞지만 role 이 안 맞음 → 여전히 숨겨져야 한다(AND)
assertFalse(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_A, ROLE_USER));
// role 은 맞지만 org 가 안 맞음
assertFalse(ApiPermissionFilter.checkApiPermissions(spec, true, ORG_B, ROLE_MANAGER));
}
// ── checkServicePermissions (ApiServiceDTO, 현재 호출부 없음 — 동일 규칙 유지 확인) ──
@Test
void checkServicePermissions_orgOnlyRestriction_hiddenFromDifferentOrg() {
ApiServiceDTO spec = new ApiServiceDTO();
spec.setDisplayOrg(ORG_A);
assertFalse(ApiPermissionFilter.checkServicePermissions(spec, true, ORG_B, ROLE_MANAGER));
assertTrue(ApiPermissionFilter.checkServicePermissions(spec, true, ORG_A, ROLE_MANAGER));
}
// ── filterApisByPermissions (SecurityUtil 정적 메서드 경유 통합 경로) ──
@Test
void filterApisByPermissions_orgOnlyRestriction_excludesAuthenticatedDifferentOrg() {
ApiSpecInfoDto visibleToOrgA = apiSpec(ORG_A, null);
visibleToOrgA.setApiId("API-ORG-A");
ApiSpecInfoDto publicApi = apiSpec(null, null);
publicApi.setApiId("API-PUBLIC");
List<ApiSpecInfoDto> apis = Arrays.asList(visibleToOrgA, publicApi);
PortalAuthenticatedUser user = org.mockito.Mockito.mock(PortalAuthenticatedUser.class);
PortalOrgDTO userOrg = new PortalOrgDTO();
userOrg.setId(ORG_B);
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
securityUtil.when(SecurityUtil::isAuthenticated).thenReturn(true);
securityUtil.when(SecurityUtil::getPortalAuthenticatedUser).thenReturn(user);
securityUtil.when(SecurityUtil::getUserOrg).thenReturn(userOrg);
when(user.getRoleCode()).thenReturn(com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode.ROLE_CORP_MANAGER);
List<ApiSpecInfoDto> filtered = ApiPermissionFilter.filterApisByPermissions(apis);
assertEquals(1, filtered.size());
assertEquals("API-PUBLIC", filtered.get(0).getApiId());
}
}
@Test
void filterApisByPermissions_anonymous_onlyPublicApisVisible() {
ApiSpecInfoDto restricted = apiSpec(ORG_A, null);
restricted.setApiId("API-ORG-A");
ApiSpecInfoDto publicApi = apiSpec(null, null);
publicApi.setApiId("API-PUBLIC");
try (MockedStatic<SecurityUtil> securityUtil = mockStatic(SecurityUtil.class)) {
securityUtil.when(SecurityUtil::isAuthenticated).thenReturn(false);
List<ApiSpecInfoDto> filtered =
ApiPermissionFilter.filterApisByPermissions(Arrays.asList(restricted, publicApi));
assertEquals(1, filtered.size());
assertEquals("API-PUBLIC", filtered.get(0).getApiId());
}
}
private ApiSpecInfoDto apiSpec(String displayOrg, String displayRoleCode) {
ApiSpecInfoDto dto = new ApiSpecInfoDto();
dto.setDisplayOrg(displayOrg);
dto.setDisplayRoleCode(displayRoleCode);
return dto;
}
}
@@ -0,0 +1,65 @@
package com.eactive.apim.portal.apps.auth.service;
import com.eactive.apim.portal.portaluser.entity.TwoFactorAuth;
import com.eactive.apim.portal.portaluser.service.AuthNumberException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AuthNumberServiceImplTest {
private static final Pattern RETRY_SECONDS = Pattern.compile(
"인증번호 재발송 제한이 적용 중입니다\\. (\\d+)초 후 다시 시도해 주세요\\.");
@Mock
private AuthNumberStorage storage;
@Mock
private AuthNumberGenerator generator;
@Mock
private MessageSender messageSender;
private AuthNumberServiceImpl service;
@BeforeEach
void setUp() {
service = new AuthNumberServiceImpl(storage, generator, messageSender);
ReflectionTestUtils.setField(service, "authNumberExpirationTime", 300);
ReflectionTestUtils.setField(service, "resendLimitSeconds", 30);
}
@Test
void resendLimitMessageIncludesRemainingSeconds() {
String recipient = "01099121100";
TwoFactorAuth existing = new TwoFactorAuth(
recipient, "123456", LocalDateTime.now().plusSeconds(300));
when(storage.getAuthNumber(recipient)).thenReturn(Optional.of(existing));
AuthNumberException exception = assertThrows(
AuthNumberException.class,
() -> service.sendRequestAuthNumber(recipient, "SMS")
);
Matcher matcher = RETRY_SECONDS.matcher(exception.getMessage());
assertTrue(matcher.matches(), "남은 재시도 초가 안내 메시지에 포함되어야 함");
long remainingSeconds = Long.parseLong(matcher.group(1));
assertTrue(remainingSeconds >= 1 && remainingSeconds <= 30,
"남은 초는 1~30 범위여야 함: " + remainingSeconds);
assertNull(exception.getReason());
verifyNoInteractions(generator, messageSender);
}
}
@@ -0,0 +1,71 @@
package com.eactive.apim.portal.apps.user;
import com.eactive.apim.portal.apps.user.service.PasswordService;
import com.eactive.apim.portal.common.dto.PasswordValidationDTO;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.UserPasswordHistory;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PasswordServiceTest {
@Mock
private PortalUserRepository portalUserRepository;
@Mock
private UserPasswordHistoryRepository passwordHistoryRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private LocalValidatorFactoryBean validator;
@InjectMocks
private PasswordService passwordService;
@Test
void rejectsPasswordStoredAgainstPortalUserId() {
PortalUser user = new PortalUser();
user.setId("user-uuid");
user.setLoginId("user@example.com");
user.setMobileNumber("010-1234-5678");
user.setPasswordHash("temporary-password-hash");
UserPasswordHistory previousPassword = new UserPasswordHistory();
previousPassword.setUserId("user-uuid");
previousPassword.setPasswordHash("original-password-hash");
when(portalUserRepository.findByLoginId("user@example.com")).thenReturn(Optional.of(user));
when(passwordEncoder.matches("Original!123", "temporary-password-hash")).thenReturn(false);
when(validator.validate(any(PasswordValidationDTO.class))).thenReturn(Collections.emptySet());
when(passwordHistoryRepository.findRecentPasswordsByUserId("user-uuid"))
.thenReturn(Collections.singletonList(previousPassword));
when(passwordEncoder.matches("Original!123", "original-password-hash")).thenReturn(true);
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> passwordService.updatePassword(
"user@example.com", "Original!123", "Original!123")
);
assertEquals("최근 5회 이내에 사용한 비밀번호는 사용할 수 없습니다.", exception.getMessage());
verify(passwordHistoryRepository).findRecentPasswordsByUserId("user-uuid");
verify(passwordHistoryRepository, never()).findRecentPasswordsByUserId("user@example.com");
verify(portalUserRepository, never()).save(user);
}
}
@@ -0,0 +1,83 @@
package com.eactive.apim.portal.apps.user;
import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade;
import com.eactive.apim.portal.apps.user.facade.MessageRequestFacade;
import com.eactive.apim.portal.apps.user.facade.UserFacadeImpl;
import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper;
import com.eactive.apim.portal.apps.user.service.PasswordService;
import com.eactive.apim.portal.apps.user.service.PortalOrgService;
import com.eactive.apim.portal.apps.user.service.PortalUserService;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UserFacadeImplTest {
@Mock
private PortalUserService portalUserService;
@Mock
private PortalOrgService portalOrgService;
@Mock
private PasswordService passwordService;
@Mock
private PortalUserMapper portalUserMapper;
@Mock
private MessageHandlerService messageHandlerService;
@Mock
private AgreementsFacade agreementsFacade;
@Mock
private MessageRequestFacade messageRequestFacade;
@InjectMocks
private UserFacadeImpl userFacade;
@Test
void corporateManagerCannotWithdraw() {
PortalUser manager = new PortalUser();
manager.setId("manager-1");
manager.setRoleCode(RoleCode.ROLE_CORP_MANAGER);
when(portalUserService.findById("manager-1")).thenReturn(manager);
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> userFacade.withdrawUser("manager-1", "withdrawal reason")
);
assertEquals(
"법인 관리자는 회원 탈퇴를 할 수 없습니다. "
+ "관리자 권한을 다른 사용자에게 이관하거나 담당자에게 연락해 주세요.",
exception.getMessage()
);
verifyNoInteractions(agreementsFacade, messageRequestFacade);
verify(portalUserService, never()).deleteUser(manager, "withdrawal reason");
}
@Test
void corporateUserCanWithdraw() {
PortalUser user = new PortalUser();
user.setId("user-1");
user.setLoginId("corp-user@example.com");
user.setUserName("법인 사용자");
user.setRoleCode(RoleCode.ROLE_CORP_USER);
when(portalUserService.findById("user-1")).thenReturn(user);
userFacade.withdrawUser("user-1", "withdrawal reason");
verify(agreementsFacade).deleteUserAgreements("user-1");
verify(messageRequestFacade).deleteUserMessage("법인 사용자", "corp-user@example.com");
verify(portalUserService).deleteUser(user, "withdrawal reason");
}
}
@@ -0,0 +1,73 @@
package com.eactive.apim.portal.common.validator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* [WEB-SER-006] 키보드 배열 연속 문자 차단 규칙 검증.
*
* <p>클라이언트 {@code static/js/password-policy.js} 의 hasKeyboardSequence 와 동일 규칙이어야 한다.</p>
*/
class PasswordRuleValidatorTest {
private final PasswordRuleValidator validator = new PasswordRuleValidator();
@Test
@DisplayName("키보드 배열 인접 3자 이상이면 연속으로 판정한다")
void detectsKeyboardSequence() {
String[] values = {
"qwe", "asd", "zxc", "qwert", "asdfg",
"QWE", "AsDfG", // 대소문자 무시
"ewq", "dsa", "trewq", // 역방향
"abqwe12", "12asd!@" // 문자열 중간에 포함
};
for (String value : values) {
assertTrue(PasswordRuleValidator.hasKeyboardSequence(value), value + " 는 키보드 연속으로 판정돼야 한다");
}
}
@Test
@DisplayName("키보드 배열 인접이 아니면 연속으로 보지 않는다")
void ignoresNonSequence() {
String[] values = { "qw", "qa", "qaz", "qwa", "Djb#7k2Qm4", "Jeju@2026x", "aq", "" };
for (String value : values) {
assertFalse(PasswordRuleValidator.hasKeyboardSequence(value), value + " 는 키보드 연속이 아니어야 한다");
}
}
@Test
@DisplayName("null 은 연속으로 보지 않는다")
void nullIsNotSequence() {
assertFalse(PasswordRuleValidator.hasKeyboardSequence(null));
}
@Test
@DisplayName("점검 지적 사례 qwert12! 는 정책 적용 시 거부된다")
void rejectsReportedCase() {
assertFalse(validator.isValid("qwert12!", null, null, true), "qwert12! 는 거부돼야 한다");
}
@Test
@DisplayName("정책을 끄면 qwert12! 가 다른 규칙만으로 판정된다")
void allowsWhenPolicyDisabled() {
assertTrue(validator.isValid("qwert12!", null, null, false), "정책 off 면 통과해야 한다");
}
@Test
@DisplayName("정상 비밀번호는 정책을 켜도 통과한다")
void acceptsStrongPassword() {
assertTrue(validator.isValid("Djb#7k2Qm4", null, null, true));
}
@Test
@DisplayName("기존 규칙(연속 문자·반복·공백)은 그대로 동작한다")
void keepsExistingRules() {
assertFalse(validator.isValid("abc12345!", null, null, true), "알파벳 3연속은 거부");
assertFalse(validator.isValid("Djb#7kkk2", null, null, true), "동일 문자 3회 반복은 거부");
assertFalse(validator.isValid("Djb# 7k2Q", null, null, true), "공백 포함은 거부");
assertFalse(validator.isValid("Djb#7k2", null, null, true), "8자 미만은 거부");
}
}
@@ -0,0 +1,69 @@
package com.eactive.apim.portal.djb.apistatus.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.repository.ApiStatusIncidentQueryRepository;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* API 개별 현재상태(P11, 정상/점검/지연/장애) 판정이 진행 중 장애·점검·최근 이슈를
* 모두 현재 사용자의 조회가능 API 집합으로 필터링해서 조회하는지 검증한다.
* {@code apiId} 를 직접 지정해 호출하는 {@code /apistatus/current.json} 도 이 경로를 탄다 -
* 조회 권한 없는 apiId 를 직접 넣어도 실제 상태(장애 여부)가 새어나가지 않아야 한다.
*/
@ExtendWith(MockitoExtension.class)
class ApiCurrentStatusServiceTest {
@Mock private ApiStatusIncidentQueryRepository incidentQueryRepository;
@Mock private DjbApistatusIncidentApiRepository incidentApiRepository;
@Mock private ApiSpecInfoRepository apiSpecInfoRepository;
@Mock private ApiStatusUptimeService uptimeService;
@Mock private ApiStatusCatalogService catalogService;
private ApiCurrentStatusService service;
private Set<String> visibleApiIds;
@BeforeEach
void setUp() {
service = new ApiCurrentStatusService(
incidentQueryRepository, incidentApiRepository, apiSpecInfoRepository, uptimeService, catalogService);
visibleApiIds = Collections.singleton("API-PUBLIC");
when(catalogService.getVisibleApiIdsForQuery()).thenReturn(visibleApiIds);
when(catalogService.getWindowDays()).thenReturn(90);
}
@Test
void resolveStatuses_passesVisibleApiIdsToOpenMaintenanceAndOverlapQueries() {
when(incidentQueryRepository.findVisibleOpenIncidents(
eq(IncidentKind.DEGRADING), any(), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
when(incidentQueryRepository.findVisibleOngoingMaintenance(
eq(IncidentKind.MAINTENANCE), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
when(incidentQueryRepository.findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.resolveStatuses(Collections.singleton("API1"));
verify(incidentQueryRepository).findVisibleOpenIncidents(
eq(IncidentKind.DEGRADING), any(), eq(visibleApiIds));
verify(incidentQueryRepository).findVisibleOngoingMaintenance(
eq(IncidentKind.MAINTENANCE), any(LocalDateTime.class), eq(visibleApiIds));
verify(incidentQueryRepository).findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds));
}
}
@@ -0,0 +1,78 @@
package com.eactive.apim.portal.djb.apistatus.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import com.eactive.apim.gateway.data.statistics.repository.GwApiStatusRepository;
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* getVisibleApiIdsForQuery() 는 ApiStatusIncidentQueryRepository 의 {@code :visibleApiIds}
* 바인딩 소스다. API 그룹 공개범위(ApiSearchFacade → ApiPermissionFilter 경로)를 그대로
* 재사용하는지, 조회 가능한 API 가 하나도 없을 때 Oracle {@code IN ()} 구문 오류를 피하는
* 더미값으로 대체하는지를 검증한다.
*/
@ExtendWith(MockitoExtension.class)
class ApiStatusCatalogServiceTest {
@Mock private ApiSearchFacade apiSearchFacade;
@Mock private PortalPropertyService portalPropertyService;
@Mock private GwApiStatusRepository gwApiStatusRepository;
private ApiStatusCatalogService service;
@BeforeEach
void setUp() {
service = new ApiStatusCatalogService(apiSearchFacade, portalPropertyService, gwApiStatusRepository);
}
@Test
void getVisibleApiIdsForQuery_returnsApiIdsVisibleToCurrentUser() {
stubSearchApis(apiSpec("API1", "결제 API"), apiSpec("API2", "조회 API"));
Collection<String> visibleApiIds = service.getVisibleApiIdsForQuery();
assertEquals(2, visibleApiIds.size());
assertTrue(visibleApiIds.contains("API1"));
assertTrue(visibleApiIds.contains("API2"));
}
@Test
void getVisibleApiIdsForQuery_whenNothingVisible_returnsDummyThatMatchesNoRealApiId() {
stubSearchApis();
Collection<String> visibleApiIds = service.getVisibleApiIdsForQuery();
assertEquals(1, visibleApiIds.size());
assertFalse(visibleApiIds.contains("API1"));
assertFalse(visibleApiIds.isEmpty());
}
private void stubSearchApis(ApiSpecInfoDto... apis) {
Map<String, Object> result = new HashMap<>();
result.put("apis", Arrays.asList(apis));
when(apiSearchFacade.searchApis(any(ApiGroupSearch.class))).thenReturn(result);
}
private ApiSpecInfoDto apiSpec(String apiId, String apiName) {
ApiSpecInfoDto dto = new ApiSpecInfoDto();
dto.setApiId(apiId);
dto.setApiName(apiName);
return dto;
}
}
@@ -0,0 +1,93 @@
package com.eactive.apim.portal.djb.apistatus.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.djb.apistatus.repository.ApiStatusIncidentQueryRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageRequest;
/**
* 전체 장애 이력 페이지(P8) 의 일자 인덱스(P9)·목록(P10) 조회가 현재 사용자의
* 조회가능 API 집합을 {@link ApiStatusIncidentQueryRepository} 에 그대로 전달하는지 검증한다.
* 이 테스트가 다루는 대상이 원 분석에서 지적된 "그룹 전용 API 장애가 전체 사용자에게
* 노출되던" 경로다.
*
* <p>mock 이라 JPQL 자체의 필터링 정확성은 검증 범위 밖이다 - 실제 쿼리 실행 검증은
* 별도 통합 테스트(@DataJpaTest 등)가 필요하다.</p>
*/
@ExtendWith(MockitoExtension.class)
class ApiStatusIssueHistoryServiceTest {
@Mock private ApiStatusIncidentQueryRepository incidentQueryRepository;
@Mock private ApiStatusAssembler assembler;
@Mock private ApiStatusCatalogService catalogService;
private ApiStatusIssueHistoryService service;
private Set<String> visibleApiIds;
@BeforeEach
void setUp() {
service = new ApiStatusIssueHistoryService(incidentQueryRepository, assembler, catalogService);
visibleApiIds = Collections.singleton("API-PUBLIC");
when(catalogService.getVisibleApiIdsForQuery()).thenReturn(visibleApiIds);
}
@Test
void getIssueDates_withoutApiFilter_passesVisibleApiIdsToOverlapQuery() {
when(incidentQueryRepository.findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getIssueDates(30, null, null);
verify(incidentQueryRepository).findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds));
}
@Test
void getIssueDates_withApiFilter_apiIdItselfMustBeVisibleTooOrRowsAreExcluded() {
when(incidentQueryRepository.findVisibleOverlappingByApi(
any(LocalDateTime.class), any(LocalDateTime.class), eq("API-RESTRICTED"), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getIssueDates(30, "API-RESTRICTED", null);
verify(incidentQueryRepository).findVisibleOverlappingByApi(
any(LocalDateTime.class), any(LocalDateTime.class), eq("API-RESTRICTED"), eq(visibleApiIds));
}
@Test
void getIssues_withoutApiFilter_passesVisibleApiIdsToOverlapQuery() {
when(incidentQueryRepository.findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getIssues(null, null, null, PageRequest.of(0, 20));
verify(incidentQueryRepository).findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds));
}
@Test
void getIssues_withApiFilter_passesVisibleApiIdsToPerApiQuery() {
when(incidentQueryRepository.findVisibleOverlappingByApi(
any(LocalDateTime.class), any(LocalDateTime.class), eq("API-RESTRICTED"), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getIssues(LocalDate.now(), "API-RESTRICTED", null, PageRequest.of(0, 20));
verify(incidentQueryRepository).findVisibleOverlappingByApi(
any(LocalDateTime.class), any(LocalDateTime.class), eq("API-RESTRICTED"), eq(visibleApiIds));
}
}
@@ -0,0 +1,102 @@
package com.eactive.apim.portal.djb.apistatus.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
import com.eactive.apim.portal.djb.apistatus.repository.ApiStatusIncidentQueryRepository;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
/**
* API Status 메인 화면(P3 진행중 장애 / P5 점검 / P6 최근 이슈 / P7 상세) 조회가
* 현재 사용자의 조회가능 API 집합({@link ApiStatusCatalogService#getVisibleApiIdsForQuery()})을
* {@link ApiStatusIncidentQueryRepository} 에 그대로 전달하는지 검증한다.
*
* <p>mock 이라 JPQL 자체의 필터링 정확성(그룹 전용 API 의 인시던트가 실제로 걸러지는지)은
* 검증 범위 밖이다 - 실제 쿼리 실행 검증은 별도 통합 테스트가 필요하다.</p>
*/
@ExtendWith(MockitoExtension.class)
class ApiStatusQueryServiceTest {
@Mock private ApiStatusIncidentQueryRepository incidentQueryRepository;
@Mock private ApiStatusAssembler assembler;
@Mock private ApiStatusCatalogService catalogService;
private ApiStatusQueryService service;
private Set<String> visibleApiIds;
@BeforeEach
void setUp() {
service = new ApiStatusQueryService(incidentQueryRepository, assembler, catalogService);
visibleApiIds = Collections.singleton("API-PUBLIC");
when(catalogService.getVisibleApiIdsForQuery()).thenReturn(visibleApiIds);
}
@Test
void getActiveIncidents_passesVisibleApiIds() {
when(incidentQueryRepository.findVisibleOpenIncidents(
eq(IncidentKind.DEGRADING), eq(ApiStatusSupport.CLOSED_STATES), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
when(assembler.toActiveIncidents(anyList(), any(LocalDateTime.class)))
.thenReturn(Collections.emptyList());
service.getActiveIncidents();
verify(incidentQueryRepository).findVisibleOpenIncidents(
eq(IncidentKind.DEGRADING), eq(ApiStatusSupport.CLOSED_STATES), eq(visibleApiIds));
}
@Test
void getOngoingMaintenance_passesVisibleApiIds() {
when(incidentQueryRepository.findVisibleOngoingMaintenance(
eq(IncidentKind.MAINTENANCE), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
when(assembler.toMaintenanceCards(anyList())).thenReturn(Collections.emptyList());
service.getOngoingMaintenance();
verify(incidentQueryRepository).findVisibleOngoingMaintenance(
eq(IncidentKind.MAINTENANCE), any(LocalDateTime.class), eq(visibleApiIds));
}
@Test
void getRecentClosedIssues_passesVisibleApiIds() {
List<DjbApistatusIncident> empty = Collections.emptyList();
when(incidentQueryRepository.findVisibleClosedIssues(
eq(ApiStatusSupport.CLOSED_STATES), any(LocalDateTime.class), eq(visibleApiIds),
eq(PageRequest.of(0, 5))))
.thenReturn(new PageImpl<>(empty));
when(assembler.toPastIssueCards(anyList())).thenReturn(Collections.emptyList());
service.getRecentClosedIssues(0);
verify(incidentQueryRepository).findVisibleClosedIssues(
eq(ApiStatusSupport.CLOSED_STATES), any(LocalDateTime.class), eq(visibleApiIds),
eq(PageRequest.of(0, 5)));
}
@Test
void getIssueDetail_passesVisibleApiIds() {
when(incidentQueryRepository.findVisibleById(eq(1L), eq(visibleApiIds)))
.thenReturn(Optional.empty());
service.getIssueDetail(1L);
verify(incidentQueryRepository).findVisibleById(eq(1L), eq(visibleApiIds));
}
}
@@ -0,0 +1,64 @@
package com.eactive.apim.portal.djb.apistatus.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.repository.ApiStatusIncidentQueryRepository;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* 90일 가동률 집계(P2) 가 인시던트 조회를 현재 사용자의 조회가능 API 집합으로
* 필터링하는지 검증한다. 필터링 전에는 그룹 전용 API 의 장애가 전체 가동률과
* 일자별 이슈 제목({@code DailyStatDTO.issues})에 그대로 섞여 들어갔다.
*/
@ExtendWith(MockitoExtension.class)
class ApiStatusUptimeServiceTest {
@Mock private ApiStatusIncidentQueryRepository incidentQueryRepository;
@Mock private DjbApistatusIncidentApiRepository incidentApiRepository;
@Mock private ApiStatusCatalogService catalogService;
private ApiStatusUptimeService service;
private Set<String> visibleApiIds;
@BeforeEach
void setUp() {
service = new ApiStatusUptimeService(incidentQueryRepository, incidentApiRepository, catalogService);
visibleApiIds = Collections.singleton("API-PUBLIC");
when(catalogService.getVisibleApiIdsForQuery()).thenReturn(visibleApiIds);
}
@Test
void getDailyStats_passesVisibleApiIds() {
when(incidentQueryRepository.findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getDailyStats(7);
verify(incidentQueryRepository).findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds));
}
@Test
void getApiUptimeRatios_passesVisibleApiIds() {
when(incidentQueryRepository.findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds)))
.thenReturn(Collections.emptyList());
service.getApiUptimeRatios(Collections.singleton("API1"), 7);
verify(incidentQueryRepository).findVisibleOverlapping(
any(LocalDateTime.class), any(LocalDateTime.class), eq(visibleApiIds));
}
}
@@ -0,0 +1,55 @@
package com.eactive.apim.portal.djb.testcleanup.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.test.util.ReflectionTestUtils;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class OrphanCleanupServiceTest {
@Mock private Environment environment;
@Mock private EntityManager entityManager;
@Mock private Query query;
private OrphanCleanupService service;
@BeforeEach
void setUp() {
service = new OrphanCleanupService(environment);
ReflectionTestUtils.setField(service, "entityManager", entityManager);
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(false);
when(entityManager.createNativeQuery(org.mockito.ArgumentMatchers.anyString())).thenReturn(query);
when(query.executeUpdate()).thenReturn(0);
}
@Test
void sweep_runsAllThirteenTablesAndReportsCounts() {
when(query.executeUpdate()).thenReturn(3);
TestCleanupResult result = service.sweep();
assertEquals(13, result.getDeletedCounts().size());
result.getDeletedCounts().values().forEach(count -> assertEquals(3L, count));
}
@Test
void sweep_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.sweep());
}
}
@@ -0,0 +1,321 @@
package com.eactive.apim.portal.djb.testcleanup.service;
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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.eactive.apim.portal.app.repository.CredentialRepository;
import com.eactive.apim.portal.apprequest.entity.AppRequest;
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.approval.entity.Approval;
import com.eactive.apim.portal.approval.statemachine.RequestedState;
import com.eactive.apim.portal.apps.approval.service.ApprovalService;
import com.eactive.apim.portal.apps.community.partnership.repository.PartnershipApplicationRepository;
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.apps.user.service.PortalUserService;
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestApiRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestEventRepository;
import com.eactive.apim.portal.djb.webhook.repository.WebhookRequestRepository;
import com.eactive.apim.portal.djb.webhook.repository.entity.WebhookRequest;
import com.eactive.apim.portal.djb.webhook.service.WebhookService;
import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserPrivacyAgreementRepository;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository;
import com.eactive.apim.portal.user.repository.UserLogRepository;
import java.util.Collections;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.security.crypto.password.PasswordEncoder;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class TestCleanupServiceTest {
@Mock private Environment environment;
@Mock private PortalOrgRepository portalOrgRepository;
@Mock private PortalUserRepository portalUserRepository;
@Mock private InquiryRepository inquiryRepository;
@Mock private InquiryCommentRepository inquiryCommentRepository;
@Mock private PartnershipApplicationRepository partnershipApplicationRepository;
@Mock private UserRoleHistoryRepository userRoleHistoryRepository;
@Mock private PortalUserPrivacyAgreementRepository portalUserPrivacyAgreementRepository;
@Mock private UserPasswordHistoryRepository userPasswordHistoryRepository;
@Mock private UserLogRepository userLogRepository;
@Mock private CredentialRepository credentialRepository;
@Mock private AppRequestRepository appRequestRepository;
@Mock private ApprovalService approvalService;
@Mock private WebhookRequestRepository webhookRequestRepository;
@Mock private WebhookRequestApiRepository webhookRequestApiRepository;
@Mock private WebhookRequestEventRepository webhookRequestEventRepository;
@Mock private WebhookService webhookService;
@Mock private UserInvitationRepository userInvitationRepository;
@Mock private FileService fileService;
@Mock private TestCleanupNativeQueries nativeQueries;
@Mock private PortalUserService portalUserService;
@Mock private PasswordEncoder passwordEncoder;
@Mock private ApiSpecInfoRepository apiSpecInfoRepository;
@Mock private DjbApistatusIncidentRepository djbApistatusIncidentRepository;
@Mock private DjbApistatusIncidentApiRepository djbApistatusIncidentApiRepository;
@Mock private com.eactive.apim.portal.apps.apis.service.ApiSearchFacade apiSearchFacade;
private TestCleanupService service;
private static final String ORG_ID = "ORG1";
private static final String USER_ID = "USER1";
private static final String LOGIN_ID = "user1@test.com";
private static final String COMP_REG_NO = "1234567890";
private static final String EMAIL = "user1@test.com";
@BeforeEach
void setUp() {
service = new TestCleanupService(environment, portalOrgRepository, portalUserRepository,
inquiryRepository, inquiryCommentRepository, partnershipApplicationRepository,
userRoleHistoryRepository, portalUserPrivacyAgreementRepository, userPasswordHistoryRepository,
userLogRepository, credentialRepository, appRequestRepository, approvalService,
webhookRequestRepository, webhookRequestApiRepository,
webhookRequestEventRepository, webhookService, userInvitationRepository, fileService,
nativeQueries, portalUserService, passwordEncoder,
apiSpecInfoRepository, djbApistatusIncidentRepository, djbApistatusIncidentApiRepository,
apiSearchFacade);
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(false);
}
private PortalUser user() {
PortalUser u = new PortalUser();
u.setId(USER_ID);
u.setLoginId(LOGIN_ID);
return u;
}
@Test
void deleteOrgCascade_notFound() {
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.empty());
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
assertFalse(result.isFound());
assertTrue(result.getDeletedCounts().isEmpty());
verifyNoInteractions(webhookService, portalUserRepository);
}
@Test
void deleteOrgCascade_fallsBackToDigitsOnlyBusinessNumber() {
String hyphenatedCompRegNo = "123-45-67890";
String digitsOnlyCompRegNo = "1234567890";
PortalOrg org = org();
when(portalOrgRepository.findByCompRegNo(hyphenatedCompRegNo)).thenReturn(Optional.empty());
when(portalOrgRepository.findByCompRegNo(digitsOnlyCompRegNo)).thenReturn(Optional.of(org));
when(portalUserRepository.findAllByPortalOrg_Id(ORG_ID)).thenReturn(Collections.emptyList());
when(webhookRequestRepository.findByOrgId(ORG_ID)).thenReturn(Optional.empty());
TestCleanupResult result = service.deleteOrgCascade(hyphenatedCompRegNo);
assertTrue(result.isFound());
assertEquals(ORG_ID, result.getTargetId());
verify(portalOrgRepository).findByCompRegNo(digitsOnlyCompRegNo);
verify(portalOrgRepository).delete(org);
}
@Test
void deleteOrgCascade_found_cascadesUsersAndOrg() {
PortalOrg org = org();
PortalUser member = user();
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.of(org));
when(portalUserRepository.findAllByPortalOrg_Id(ORG_ID)).thenReturn(Collections.singletonList(member));
when(webhookRequestRepository.findByOrgId(ORG_ID)).thenReturn(Optional.empty());
when(nativeQueries.deleteCredentialApiByOrgId(ORG_ID)).thenReturn(2);
when(credentialRepository.deleteByOrgid(ORG_ID)).thenReturn(1L);
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
assertTrue(result.isFound());
assertEquals(ORG_ID, result.getTargetId());
assertEquals(1L, result.getDeletedCounts().get("PTL_USER"));
assertEquals(2L, result.getDeletedCounts().get("PTL_CREDENTIAL_API"));
assertEquals(1L, result.getDeletedCounts().get("PTL_CREDENTIAL"));
assertEquals(1L, result.getDeletedCounts().get("PTL_ORG"));
assertEquals(0L, result.getDeletedCounts().get("PTL_WEBHOOK_REQ"));
verify(inquiryCommentRepository).deleteByInquiry_Inquirer_Id(USER_ID);
verify(inquiryRepository).deleteByInquirer_Id(USER_ID);
verify(userRoleHistoryRepository).deleteByUserId(LOGIN_ID);
verify(userPasswordHistoryRepository).deleteByUserId(USER_ID);
verify(portalUserRepository).delete(member);
verify(webhookService, never()).delete(anyLong(), anyString());
verify(portalOrgRepository).delete(org);
}
@Test
void deleteOrgCascade_withWebhook_delegatesToWebhookService() {
PortalOrg org = org();
WebhookRequest webhook = new WebhookRequest();
webhook.setId(99L);
when(portalOrgRepository.findByCompRegNo(COMP_REG_NO)).thenReturn(Optional.of(org));
when(portalUserRepository.findAllByPortalOrg_Id(ORG_ID)).thenReturn(Collections.emptyList());
when(webhookRequestRepository.findByOrgId(ORG_ID)).thenReturn(Optional.of(webhook));
when(webhookRequestApiRepository.findByWebhookReqId(99L)).thenReturn(Collections.emptyList());
when(webhookRequestEventRepository.findByWebhookReqId(99L)).thenReturn(Collections.emptyList());
TestCleanupResult result = service.deleteOrgCascade(COMP_REG_NO);
verify(webhookService, times(1)).delete(99L, ORG_ID);
assertEquals(1L, result.getDeletedCounts().get("PTL_WEBHOOK_REQ"));
}
@Test
void deleteUserCascade_notFound() {
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.empty());
TestCleanupResult result = service.deleteUserCascade(EMAIL);
assertFalse(result.isFound());
assertTrue(result.getDeletedCounts().isEmpty());
}
@Test
void checkUserExists_found_doesNotDeleteData() {
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.of(user()));
TestCleanupResult result = service.checkUserExists(EMAIL);
assertTrue(result.isFound());
assertEquals(USER_ID, result.getTargetId());
assertTrue(result.getDeletedCounts().isEmpty());
verify(portalUserRepository, never()).delete(org.mockito.ArgumentMatchers.any(PortalUser.class));
}
@Test
void checkUserExists_notFound() {
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.empty());
TestCleanupResult result = service.checkUserExists(EMAIL);
assertFalse(result.isFound());
assertTrue(result.getDeletedCounts().isEmpty());
}
@Test
void deleteInvitationsByMobile_normalizesAndDeletesAllMatches() {
when(userInvitationRepository.deleteByInvitationMobile("010-1234-5678")).thenReturn(2L);
TestCleanupResult result = service.deleteInvitationsByMobile("01012345678");
assertTrue(result.isFound());
assertEquals("010-1234-5678", result.getTargetId());
assertEquals(2L, result.getDeletedCounts().get("PTL_USER_INVITATION"));
verify(userInvitationRepository).deleteByInvitationMobile("010-1234-5678");
}
@Test
void detachUsersFromOrgByMobile_keepsUserAndRemovesOnlyMembership() {
PortalUser target = user();
target.setPortalOrg(org());
when(portalUserRepository.findAllByMobileNumber("010-1234-5678"))
.thenReturn(Collections.singletonList(target));
TestCleanupResult result = service.detachUsersFromOrgByMobile("01012345678");
assertTrue(result.isFound());
assertEquals(USER_ID, result.getTargetId());
assertEquals(1L, result.getDeletedCounts().get("PTL_USER_ORG_MEMBERSHIP"));
assertNull(target.getPortalOrg());
assertEquals(com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode.ROLE_USER, target.getRoleCode());
verify(portalUserRepository).save(target);
verify(portalUserRepository, never()).delete(target);
}
@Test
void cancelPendingTestAppRequests_cancelsOnlyExactTestAppRequest() {
PortalUser target = user();
PortalOrg org = org();
target.setPortalOrg(org);
AppRequest request = new AppRequest();
request.setId("APP_REQ_1");
request.setClientName("단위테스트앱-20260824-144907");
Approval approval = new Approval();
approval.setApprovalStatus(new RequestedState());
request.setApproval(approval);
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.of(target));
when(appRequestRepository.findAllByOrgAndClientName(org, request.getClientName()))
.thenReturn(Collections.singletonList(request));
TestCleanupResult result = service.cancelPendingTestAppRequests(EMAIL, request.getClientName());
assertTrue(result.isFound());
assertEquals("APP_REQ_1", result.getTargetId());
assertEquals(1L, result.getDeletedCounts().get("PTL_APP_REQUEST_CANCELLED"));
verify(approvalService).cancelAppApproval(request);
}
@Test
void cancelPendingTestAppRequests_rejectsNonTestName() {
assertThrows(IllegalArgumentException.class,
() -> service.cancelPendingTestAppRequests(EMAIL, "운영앱"));
verifyNoInteractions(appRequestRepository, approvalService);
}
@Test
void deleteUserCascade_found_deletesOrphanDataAndUser() {
PortalUser target = user();
when(portalUserRepository.findPortalUserByEmailAddr(EMAIL)).thenReturn(Optional.of(target));
TestCleanupResult result = service.deleteUserCascade(EMAIL);
assertTrue(result.isFound());
assertEquals(USER_ID, result.getTargetId());
verify(inquiryCommentRepository).deleteByInquiry_Inquirer_Id(USER_ID);
verify(inquiryRepository).deleteByInquirer_Id(USER_ID);
verify(partnershipApplicationRepository).deleteByCreatedBy(USER_ID);
verify(userRoleHistoryRepository).deleteByUserId(LOGIN_ID);
verify(portalUserPrivacyAgreementRepository).deleteByCreatedBy(USER_ID);
verify(userPasswordHistoryRepository).deleteByUserId(USER_ID);
verify(userLogRepository).deleteByLoginId(LOGIN_ID);
verify(portalUserRepository).delete(target);
}
@Test
void deleteOrgCascade_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.deleteOrgCascade(COMP_REG_NO));
}
@Test
void deleteUserCascade_prodProfile_throws() {
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(true);
assertThrows(IllegalStateException.class, () -> service.deleteUserCascade(EMAIL));
}
private PortalOrg org() {
PortalOrg o = new PortalOrg();
o.setId(ORG_ID);
return o;
}
}