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:
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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자 미만은 거부");
|
||||
}
|
||||
}
|
||||
+69
@@ -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));
|
||||
}
|
||||
}
|
||||
+78
@@ -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;
|
||||
}
|
||||
}
|
||||
+93
@@ -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));
|
||||
}
|
||||
}
|
||||
+102
@@ -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));
|
||||
}
|
||||
}
|
||||
+64
@@ -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));
|
||||
}
|
||||
}
|
||||
+55
@@ -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());
|
||||
}
|
||||
}
|
||||
+321
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user