테스트 코드 추가:
- API 상태 관련 서비스 테스트 클래스 신규 작성 (총 7개) - 테스트 코드에서 `visibleApiIds` 필터 적용 확인 및 조회 로직 검증 포함 - `TestCleanupService` 확장: 그룹 제한 데이터 관련 신규 Fixture 및 Mock 추가
This commit is contained in:
@@ -64,33 +64,36 @@ public class ApiPermissionFilter {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static boolean checkServicePermissions(ApiServiceDTO spec, boolean isAuthenticated,
|
||||
/**
|
||||
* org 축과 role 축은 각자 독립적으로 선택적 제한이다(비어있으면 그 축은 통과) — 둘을 AND로 합친다.
|
||||
* (과거 OR 합성은 한쪽 축이 비어있으면 그 축이 무조건 true가 되어 다른 쪽 제한을 무력화시키는
|
||||
* 결함이 있었다 — 예: displayOrg 만 설정하고 displayRoleCode 를 비워두면 roleMatch 가 로그인
|
||||
* 여부만으로 true 가 되어 조직 제한이 사실상 적용되지 않았다.)
|
||||
*/
|
||||
static boolean checkServicePermissions(ApiServiceDTO spec, boolean isAuthenticated,
|
||||
String org, String roleCode) {
|
||||
if (!isAuthenticated) {
|
||||
return (spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()) && (spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty());
|
||||
}
|
||||
|
||||
boolean orgMatch = org != null && (spec.getDisplayOrg() == null ||
|
||||
spec.getDisplayOrg().isEmpty() ||
|
||||
spec.getDisplayOrg().contains(org));
|
||||
boolean roleMatch = roleCode != null && (spec.getDisplayRoleCode() == null ||
|
||||
spec.getDisplayRoleCode().isEmpty() ||
|
||||
spec.getDisplayRoleCode().contains(roleCode));
|
||||
return orgMatch || roleMatch;
|
||||
boolean orgOk = spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()
|
||||
|| (org != null && spec.getDisplayOrg().contains(org));
|
||||
boolean roleOk = spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty()
|
||||
|| (roleCode != null && spec.getDisplayRoleCode().contains(roleCode));
|
||||
return orgOk && roleOk;
|
||||
}
|
||||
|
||||
private static boolean checkApiPermissions(ApiSpecInfoDto spec, boolean isAuthenticated,
|
||||
/** {@link #checkServicePermissions} 와 동일 규칙(중복 유지 — 대상 DTO 타입만 다름). */
|
||||
static boolean checkApiPermissions(ApiSpecInfoDto spec, boolean isAuthenticated,
|
||||
String org, String roleCode) {
|
||||
if (!isAuthenticated) {
|
||||
return (spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()) && (spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty());
|
||||
}
|
||||
|
||||
boolean orgMatch = org != null && (spec.getDisplayOrg() == null ||
|
||||
spec.getDisplayOrg().isEmpty() ||
|
||||
spec.getDisplayOrg().contains(org));
|
||||
boolean roleMatch = roleCode != null && (spec.getDisplayRoleCode() == null ||
|
||||
spec.getDisplayRoleCode().isEmpty() ||
|
||||
spec.getDisplayRoleCode().contains(roleCode));
|
||||
return orgMatch || roleMatch;
|
||||
boolean orgOk = spec.getDisplayOrg() == null || spec.getDisplayOrg().isEmpty()
|
||||
|| (org != null && spec.getDisplayOrg().contains(org));
|
||||
boolean roleOk = spec.getDisplayRoleCode() == null || spec.getDisplayRoleCode().isEmpty()
|
||||
|| (roleCode != null && spec.getDisplayRoleCode().contains(roleCode));
|
||||
return orgOk && roleOk;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-6
@@ -27,13 +27,23 @@ import java.util.Optional;
|
||||
*
|
||||
* <p>공지 조건을 EXISTS 로 쓰는 이유: 예전처럼 {@code FROM ... , PortalNotice n} 으로 조인하면
|
||||
* NOTICE_ID 가 없는 지연 이슈가 행 자체에서 사라진다.</p>
|
||||
*
|
||||
* <p>API 그룹으로 특정 법인에게만 공개된 API 의 이슈는, 그 API 를 조회할 수 없는 사용자에게는
|
||||
* 이슈 자체(제목·요약·타임라인 포함)를 노출하지 않는다. 영향 API 가 하나도 없는 이슈(전사 공지성)는
|
||||
* 그대로 노출한다. {@code visibleApiIds} 는 {@code ApiStatusCatalogService#getVisibleApiIdsForQuery()}
|
||||
* 로 구한, 현재 사용자에게 공개된 API ID 집합이다.</p>
|
||||
*/
|
||||
public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatusIncident, Long> {
|
||||
|
||||
String VISIBLE = " i.draftYn = 'N'"
|
||||
+ " AND (i.noticeId IS NULL"
|
||||
+ " OR EXISTS (SELECT 1 FROM PortalNotice n"
|
||||
+ " WHERE n.id = i.noticeId AND n.useYn = 'Y')) ";
|
||||
+ " WHERE n.id = i.noticeId AND n.useYn = 'Y'))"
|
||||
+ " AND (NOT EXISTS (SELECT 1 FROM DjbApistatusIncidentApi ia"
|
||||
+ " WHERE ia.incidentId = i.incidentId)"
|
||||
+ " OR EXISTS (SELECT 1 FROM DjbApistatusIncidentApi ia2"
|
||||
+ " WHERE ia2.incidentId = i.incidentId"
|
||||
+ " AND ia2.apiId IN :visibleApiIds)) ";
|
||||
|
||||
/** 종결 판정이 STATE 로 이뤄지는 종류 (장애·지연). JPQL 리터럴로 써야 해서 FQCN 을 쓴다 */
|
||||
String KIND_INCIDENT = "com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind.INCIDENT";
|
||||
@@ -55,7 +65,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
+ " AND i.state NOT IN :closedStates"
|
||||
+ " ORDER BY i.startedAt DESC")
|
||||
List<DjbApistatusIncident> findVisibleOpenIncidents(@Param("kinds") Collection<IncidentKind> kinds,
|
||||
@Param("closedStates") Collection<IncidentState> closedStates);
|
||||
@Param("closedStates") Collection<IncidentState> closedStates,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds);
|
||||
|
||||
/**
|
||||
* 예정/진행 중 점검 (P5). 종료 시각이 없거나 아직 지나지 않은 점검.
|
||||
@@ -66,7 +77,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
+ " AND (i.endAt IS NULL OR i.endAt >= :now)"
|
||||
+ " ORDER BY i.startedAt ASC")
|
||||
List<DjbApistatusIncident> findVisibleOngoingMaintenance(@Param("kind") IncidentKind kind,
|
||||
@Param("now") LocalDateTime now);
|
||||
@Param("now") LocalDateTime now,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds);
|
||||
|
||||
/**
|
||||
* 종결된 이슈 (P6). 장애·지연은 종결 상태, 점검은 종료 시각 경과.
|
||||
@@ -80,6 +92,7 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
+ " AND " + CLOSED_CONDITION)
|
||||
Page<DjbApistatusIncident> findVisibleClosedIssues(@Param("closedStates") Collection<IncidentState> closedStates,
|
||||
@Param("now") LocalDateTime now,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
@@ -92,7 +105,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
+ " AND (i.endAt IS NULL OR i.endAt >= :from)"
|
||||
+ " ORDER BY i.startedAt DESC")
|
||||
List<DjbApistatusIncident> findVisibleOverlapping(@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to);
|
||||
@Param("to") LocalDateTime to,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds);
|
||||
|
||||
/**
|
||||
* 기간과 겹치고 특정 API 에 영향을 준 이슈 (P9/P10 의 apiId 필터).
|
||||
@@ -106,7 +120,8 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
+ " ORDER BY i.startedAt DESC")
|
||||
List<DjbApistatusIncident> findVisibleOverlappingByApi(@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to,
|
||||
@Param("apiId") String apiId);
|
||||
@Param("apiId") String apiId,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds);
|
||||
|
||||
/**
|
||||
* 공개 상세 (P7)
|
||||
@@ -114,5 +129,6 @@ public interface ApiStatusIncidentQueryRepository extends Repository<DjbApistatu
|
||||
@Query("SELECT i FROM DjbApistatusIncident i"
|
||||
+ " WHERE " + VISIBLE
|
||||
+ " AND i.incidentId = :incidentId")
|
||||
Optional<DjbApistatusIncident> findVisibleById(@Param("incidentId") Long incidentId);
|
||||
Optional<DjbApistatusIncident> findVisibleById(@Param("incidentId") Long incidentId,
|
||||
@Param("visibleApiIds") Collection<String> visibleApiIds);
|
||||
}
|
||||
|
||||
+6
-3
@@ -152,7 +152,8 @@ public class ApiCurrentStatusService {
|
||||
/** 진행 중(미종결) 장애·지연 중 API 별로 가장 심각한 한 건 */
|
||||
private Map<String, DjbApistatusIncident> mapOpenIncidents(Set<String> apiIds) {
|
||||
List<DjbApistatusIncident> openIncidents = incidentQueryRepository
|
||||
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
|
||||
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES,
|
||||
catalogService.getVisibleApiIdsForQuery());
|
||||
if (openIncidents.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
@@ -178,7 +179,8 @@ public class ApiCurrentStatusService {
|
||||
/** 이미 시작된 점검 중 API 별로 가장 먼저 시작된 한 건 (예정 점검은 현재 상태가 아니므로 제외) */
|
||||
private Map<String, DjbApistatusIncident> mapStartedMaintenance(Set<String> apiIds, LocalDateTime now) {
|
||||
List<DjbApistatusIncident> maintenances = incidentQueryRepository
|
||||
.findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, now).stream()
|
||||
.findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, now,
|
||||
catalogService.getVisibleApiIdsForQuery()).stream()
|
||||
.filter(incident -> incident.getStartedAt() != null && !incident.getStartedAt().isAfter(now))
|
||||
.collect(Collectors.toList());
|
||||
if (maintenances.isEmpty()) {
|
||||
@@ -208,7 +210,8 @@ public class ApiCurrentStatusService {
|
||||
private Map<String, LocalDateTime> collectLastIncidentAt(Set<String> apiIds, LocalDateTime now, int windowDays) {
|
||||
LocalDateTime windowStart = now.toLocalDate().minusDays(windowDays - 1L).atStartOfDay();
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay(),
|
||||
catalogService.getVisibleApiIdsForQuery()).stream()
|
||||
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
|
||||
.collect(Collectors.toList());
|
||||
if (incidents.isEmpty()) {
|
||||
|
||||
+14
@@ -16,6 +16,7 @@ import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -99,6 +100,19 @@ public class ApiStatusCatalogService {
|
||||
return names;
|
||||
}
|
||||
|
||||
/** IN 절 바인딩용 API ID 없을 리 없는 더미값. 실제 API ID 는 절대 이 값이 될 수 없다 */
|
||||
private static final List<String> NO_VISIBLE_API = Collections.singletonList("__NO_VISIBLE_API__");
|
||||
|
||||
/**
|
||||
* {@link ApiStatusIncidentQueryRepository} 의 {@code :visibleApiIds} 바인딩용.
|
||||
* Oracle 은 빈 컬렉션으로 {@code IN ()} 을 만들면 구문 오류가 나므로, 조회 가능한 API 가
|
||||
* 하나도 없을 때는 절대 매치되지 않는 더미값으로 대체한다.
|
||||
*/
|
||||
public Collection<String> getVisibleApiIdsForQuery() {
|
||||
Map<String, String> names = getVisibleApiNames();
|
||||
return names.isEmpty() ? NO_VISIBLE_API : names.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* API 상태 모니터링 Job(eapim-admin Quartz)의 마지막 실행 시각.
|
||||
*
|
||||
|
||||
+3
-2
@@ -130,9 +130,10 @@ public class ApiStatusIssueHistoryService {
|
||||
|
||||
private List<DjbApistatusIncident> findOverlapping(LocalDateTime from, LocalDateTime to,
|
||||
String apiId, String kind) {
|
||||
java.util.Collection<String> visibleApiIds = catalogService.getVisibleApiIdsForQuery();
|
||||
List<DjbApistatusIncident> incidents = StringUtils.isBlank(apiId)
|
||||
? incidentQueryRepository.findVisibleOverlapping(from, to)
|
||||
: incidentQueryRepository.findVisibleOverlappingByApi(from, to, apiId);
|
||||
? incidentQueryRepository.findVisibleOverlapping(from, to, visibleApiIds)
|
||||
: incidentQueryRepository.findVisibleOverlappingByApi(from, to, apiId, visibleApiIds);
|
||||
|
||||
java.util.function.Predicate<DjbApistatusIncident> filter = kindFilter(kind);
|
||||
if (filter == null) {
|
||||
|
||||
+7
-4
@@ -25,19 +25,22 @@ public class ApiStatusQueryService {
|
||||
|
||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||
private final ApiStatusAssembler assembler;
|
||||
private final ApiStatusCatalogService catalogService;
|
||||
|
||||
/** P3 - 진행 중 장애·지연 */
|
||||
public List<ActiveIncidentDTO> getActiveIncidents() {
|
||||
LocalDateTime now = ApiStatusSupport.now();
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES);
|
||||
.findVisibleOpenIncidents(IncidentKind.DEGRADING, ApiStatusSupport.CLOSED_STATES,
|
||||
catalogService.getVisibleApiIdsForQuery());
|
||||
return assembler.toActiveIncidents(incidents, now);
|
||||
}
|
||||
|
||||
/** P5 - 예정/진행 중 점검 */
|
||||
public List<MaintenanceCardDTO> getOngoingMaintenance() {
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, ApiStatusSupport.now());
|
||||
.findVisibleOngoingMaintenance(IncidentKind.MAINTENANCE, ApiStatusSupport.now(),
|
||||
catalogService.getVisibleApiIdsForQuery());
|
||||
return assembler.toMaintenanceCards(incidents);
|
||||
}
|
||||
|
||||
@@ -46,14 +49,14 @@ public class ApiStatusQueryService {
|
||||
int limit = size <= 0 ? 5 : Math.min(size, 50);
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleClosedIssues(ApiStatusSupport.CLOSED_STATES, ApiStatusSupport.now(),
|
||||
PageRequest.of(0, limit))
|
||||
catalogService.getVisibleApiIdsForQuery(), PageRequest.of(0, limit))
|
||||
.getContent();
|
||||
return assembler.toPastIssueCards(incidents);
|
||||
}
|
||||
|
||||
/** P7 - 이슈 공개 상세 */
|
||||
public Optional<PastIssueCardDTO> getIssueDetail(Long incidentId) {
|
||||
return incidentQueryRepository.findVisibleById(incidentId)
|
||||
return incidentQueryRepository.findVisibleById(incidentId, catalogService.getVisibleApiIdsForQuery())
|
||||
.map(incident -> assembler.toPastIssueCards(java.util.Collections.singletonList(incident)).get(0));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -34,6 +34,7 @@ public class ApiStatusUptimeService {
|
||||
|
||||
private final ApiStatusIncidentQueryRepository incidentQueryRepository;
|
||||
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||
private final ApiStatusCatalogService catalogService;
|
||||
|
||||
/** P2 - 90일 가동률 */
|
||||
public List<DailyStatDTO> getDailyStats(int days) {
|
||||
@@ -43,7 +44,8 @@ public class ApiStatusUptimeService {
|
||||
LocalDate from = today.minusDays(windowDays - 1L);
|
||||
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOverlapping(from.atStartOfDay(), today.plusDays(1).atStartOfDay());
|
||||
.findVisibleOverlapping(from.atStartOfDay(), today.plusDays(1).atStartOfDay(),
|
||||
catalogService.getVisibleApiIdsForQuery());
|
||||
|
||||
List<DailyStatDTO> result = new ArrayList<>();
|
||||
for (int offset = 0; offset < windowDays; offset++) {
|
||||
@@ -66,7 +68,8 @@ public class ApiStatusUptimeService {
|
||||
|
||||
// 장애와 지연 모두 서비스 저하이므로 가동률에서 차감한다. 점검은 계획된 작업이라 제외.
|
||||
List<DjbApistatusIncident> incidents = incidentQueryRepository
|
||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay()).stream()
|
||||
.findVisibleOverlapping(windowStart, now.toLocalDate().plusDays(1).atStartOfDay(),
|
||||
catalogService.getVisibleApiIdsForQuery()).stream()
|
||||
.filter(incident -> incident.getKind() != null && incident.getKind().isDegrading())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/djb/notitest")
|
||||
@RequestMapping("//notitest")
|
||||
@RequiredArgsConstructor
|
||||
public class NotiTestController {
|
||||
|
||||
|
||||
+100
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.djb.testcleanup.controller;
|
||||
|
||||
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
|
||||
import com.eactive.apim.portal.common.util.IpAddressMatcher;
|
||||
import com.eactive.apim.portal.djb.testcleanup.service.GroupRestrictedIncidentFixture;
|
||||
import com.eactive.apim.portal.djb.testcleanup.service.OrphanCleanupService;
|
||||
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupResult;
|
||||
import com.eactive.apim.portal.djb.testcleanup.service.TestCleanupService;
|
||||
@@ -310,6 +311,105 @@ public class TestCleanupInternalController {
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* API Status 그룹공개(법인전용) 필터 검증용 "다른 법인" 계정을 준비한다. 지정 사업자번호의
|
||||
* 법인이 없으면 새로 만들고(승인 절차 없이 즉시 로그인 가능), 있으면 매니저 계정을 재소속·
|
||||
* 비밀번호 재설정으로 되돌린다(heal). 정리는 기존 {@code /internal/test-cleanup/org} 를
|
||||
* 그대로 재사용한다(같은 compRegNo).
|
||||
*/
|
||||
@PostMapping("/secondary-org")
|
||||
public ResponseEntity<Map<String, Object>> ensureSecondaryOrg(
|
||||
@RequestParam String compRegNo, @RequestParam String orgName, @RequestParam String managerEmail,
|
||||
@RequestParam String password, @RequestParam String mobile, @RequestParam String userName,
|
||||
HttpServletRequest request) {
|
||||
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
|
||||
if (guardFailure != null) {
|
||||
return guardFailure;
|
||||
}
|
||||
if (StringUtils.isBlank(compRegNo) || StringUtils.isBlank(orgName) || StringUtils.isBlank(managerEmail)
|
||||
|| StringUtils.isBlank(password) || StringUtils.isBlank(mobile) || StringUtils.isBlank(userName)) {
|
||||
return badRequest("compRegNo, orgName, managerEmail, password, mobile, userName 은 모두 필수입니다.");
|
||||
}
|
||||
|
||||
TestCleanupResult result =
|
||||
testCleanupService.ensureSecondaryTestOrg(compRegNo, orgName, managerEmail, password, mobile, userName);
|
||||
log.info("테스트 정리(secondary-org) 실행 - compRegNo: {}, managerEmail: {}, from: {}",
|
||||
compRegNo, managerEmail, request.getRemoteAddr());
|
||||
|
||||
Map<String, Object> body = baseBody();
|
||||
body.put("compRegNo", compRegNo);
|
||||
body.put("managerEmail", managerEmail);
|
||||
body.put("orgId", result.getTargetId());
|
||||
body.put("orgCreated", result.getDeletedCounts().getOrDefault("PTL_ORG_CREATED", 0L) > 0);
|
||||
body.put("userCreated", result.getDeletedCounts().getOrDefault("PTL_USER_CREATED", 0L) > 0);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* API Status 그룹공개 필터 검증용 픽스처를 만든다 — {@code managerEmail} 소속 법인으로 현재
|
||||
* 게시된 API 중 하나(동적 선택)를 제한하고, 그 API 에 영향을 준 종결 인시던트 1건을 만든다.
|
||||
*/
|
||||
@PostMapping("/group-restricted-incident")
|
||||
public ResponseEntity<Map<String, Object>> ensureGroupRestrictedIncident(
|
||||
@RequestParam String managerEmail, @RequestParam String subject,
|
||||
@RequestParam(required = false) String summary, HttpServletRequest request) {
|
||||
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
|
||||
if (guardFailure != null) {
|
||||
return guardFailure;
|
||||
}
|
||||
if (StringUtils.isBlank(managerEmail) || StringUtils.isBlank(subject)) {
|
||||
return badRequest("managerEmail 과 subject 는 필수입니다.");
|
||||
}
|
||||
|
||||
GroupRestrictedIncidentFixture fixture;
|
||||
try {
|
||||
fixture = testCleanupService.ensureGroupRestrictedIncident(managerEmail, subject, summary);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return badRequest(e.getMessage());
|
||||
}
|
||||
log.info("테스트 정리(group-restricted-incident) 실행 - managerEmail: {}, apiId: {}, incidentId: {}, from: {}",
|
||||
managerEmail, fixture.getApiId(), fixture.getIncidentId(), request.getRemoteAddr());
|
||||
|
||||
Map<String, Object> body = baseBody();
|
||||
body.put("apiId", fixture.getApiId());
|
||||
body.put("apiName", fixture.getApiName());
|
||||
body.put("orgId", fixture.getOrgId());
|
||||
body.put("incidentId", fixture.getIncidentId());
|
||||
body.put("previousDisplayOrg", fixture.getPreviousDisplayOrg());
|
||||
body.put("previousDisplayRoleCode", fixture.getPreviousDisplayRoleCode());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code /group-restricted-incident} 로 만든 픽스처를 원복한다 — 정확한 apiId/incidentId 를
|
||||
* 요구해 와일드카드 삭제가 없다.
|
||||
*/
|
||||
@PostMapping("/group-restricted-incident/revert")
|
||||
public ResponseEntity<Map<String, Object>> revertGroupRestrictedIncident(
|
||||
@RequestParam String apiId, @RequestParam Long incidentId,
|
||||
@RequestParam(required = false) String previousDisplayOrg,
|
||||
@RequestParam(required = false) String previousDisplayRoleCode, HttpServletRequest request) {
|
||||
ResponseEntity<Map<String, Object>> guardFailure = checkGuards(request);
|
||||
if (guardFailure != null) {
|
||||
return guardFailure;
|
||||
}
|
||||
if (StringUtils.isBlank(apiId) || incidentId == null) {
|
||||
return badRequest("apiId 와 incidentId 는 필수입니다.");
|
||||
}
|
||||
|
||||
TestCleanupResult result = testCleanupService.revertGroupRestrictedIncident(
|
||||
apiId, incidentId, previousDisplayOrg, previousDisplayRoleCode);
|
||||
log.info("테스트 정리(group-restricted-incident revert) 실행 - apiId: {}, incidentId: {}, found: {}, from: {}",
|
||||
apiId, incidentId, result.isFound(), request.getRemoteAddr());
|
||||
|
||||
Map<String, Object> body = baseBody();
|
||||
body.put("apiId", apiId);
|
||||
body.put("incidentId", incidentId);
|
||||
body.put("incidentFound", result.isFound());
|
||||
body.put("deletedCounts", result.getDeletedCounts());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴대폰 번호로 남아 있는 초대 레코드를 삭제한다. 1020 재실행 전 초대중 중복을 정리하는 용도다.
|
||||
*/
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.eactive.apim.portal.djb.testcleanup.service;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* {@link TestCleanupService#ensureGroupRestrictedIncident} 결과. {@link TestCleanupResult}는
|
||||
* "대상 존재 여부 + 테이블별 삭제건수"(Long) 형태라 이 작업의 응답(선택된 API/법인/원래 공개범위)엔
|
||||
* 맞지 않아 전용 타입을 둔다. {@code previousDisplayOrg}/{@code previousDisplayRoleCode}는
|
||||
* 원복({@link TestCleanupService#revertGroupRestrictedIncident})에 그대로 되돌려줘야 한다.
|
||||
*/
|
||||
@Value
|
||||
public class GroupRestrictedIncidentFixture {
|
||||
String apiId;
|
||||
String apiName;
|
||||
String orgId;
|
||||
Long incidentId;
|
||||
String previousDisplayOrg;
|
||||
String previousDisplayRoleCode;
|
||||
}
|
||||
+207
@@ -8,9 +8,20 @@ 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.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.apps.user.dto.PortalUserRegistrationDTO;
|
||||
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserService;
|
||||
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState;
|
||||
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;
|
||||
@@ -22,6 +33,7 @@ import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.partnershipapplication.entity.PartnershipApplication;
|
||||
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
|
||||
@@ -32,11 +44,14 @@ import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.user.repository.UserLogRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
@@ -81,6 +96,22 @@ public class TestCleanupService {
|
||||
private final TestCleanupNativeQueries nativeQueries;
|
||||
private final PortalUserService portalUserService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final ApiSpecInfoRepository apiSpecInfoRepository;
|
||||
private final DjbApistatusIncidentRepository djbApistatusIncidentRepository;
|
||||
private final DjbApistatusIncidentApiRepository djbApistatusIncidentApiRepository;
|
||||
private final ApiSearchFacade apiSearchFacade;
|
||||
|
||||
/** notice-incident.spec.ts 기본 영향 API — 동시 실행 충돌을 피하려 자동선택에서 제외한다 */
|
||||
private static final String RESERVED_DEFAULT_API_ID = "ANSTOSOBP00001S2";
|
||||
|
||||
/**
|
||||
* 이 내부 API 는 Spring Security 인증 없이(IP+토큰 가드만으로) 호출되므로
|
||||
* {@code AuditorAwareImpl}(SecurityUtil.getPortalAuthenticatedUser() 기반)이 항상
|
||||
* empty 를 반환해 {@code @CreatedBy} 자동 채움이 동작하지 않는다. PTL_ORG/PTL_USER 는
|
||||
* created_by 가 nullable 이라 문제없이 넘어가지만, DJB_APISTATUS_INCIDENT(_API) 는
|
||||
* NOT NULL 이라 명시적으로 채워야 한다(ORA-01400).
|
||||
*/
|
||||
private static final String TEST_FIXTURE_CREATED_BY = "PLAYWRIGHT_TEST";
|
||||
|
||||
/**
|
||||
* 사업자등록번호로 법인을 찾아, 소속 계정 전원 + org 소유 CREDENTIAL/WEBHOOK + 법인 자체를 하드 삭제한다.
|
||||
@@ -416,6 +447,182 @@ public class TestCleanupService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Status 그룹공개(법인전용) 필터를 Playwright 로 검증하려면 "다른 법인" 로그인 계정이
|
||||
* 하나 더 필요하다. 지정 사업자번호(compRegNo)의 법인이 없으면 새로 만들고(관리자 승인 절차
|
||||
* 없이 바로 ACTIVE/COMPLETED — {@link PortalUserService#createUserWithOrg} 가 신규 법인가입
|
||||
* 경로에서 쓰는 것과 같은 즉시활성 경로를 재사용), 있으면 매니저 계정을 재소속·비밀번호
|
||||
* 재설정으로 로그인 가능한 상태로 되돌린다(heal). 정리는 새 API 를 만들 필요 없이 기존
|
||||
* {@link #deleteOrgCascade(String)}(compRegNo)를 그대로 재사용한다.
|
||||
*/
|
||||
public TestCleanupResult ensureSecondaryTestOrg(
|
||||
String compRegNo, String orgName, String managerEmail, String password, String mobile, String userName) {
|
||||
assertNonProdProfile();
|
||||
|
||||
Optional<PortalOrg> existingOrg = portalOrgRepository.findByCompRegNo(compRegNo);
|
||||
boolean orgCreated = !existingOrg.isPresent();
|
||||
PortalOrg org;
|
||||
if (existingOrg.isPresent()) {
|
||||
org = existingOrg.get();
|
||||
} else {
|
||||
PortalOrg created = new PortalOrg();
|
||||
created.setId(UUID.randomUUID().toString());
|
||||
created.setCompRegNo(compRegNo);
|
||||
created.setOrgName(orgName);
|
||||
created.setOrgStatus(PortalOrgEnums.OrgStatus.ACTIVE);
|
||||
created.setApprovalStatus(PortalOrgEnums.ApprovalStatus.COMPLETED);
|
||||
org = portalOrgRepository.save(created);
|
||||
}
|
||||
|
||||
Optional<PortalUser> existingUser = portalUserRepository.findPortalUserByEmailAddr(managerEmail);
|
||||
boolean userCreated;
|
||||
PortalUser user;
|
||||
if (existingUser.isPresent()) {
|
||||
user = existingUser.get();
|
||||
user.setPortalOrg(org);
|
||||
user.setRoleCode(RoleCode.ROLE_CORP_MANAGER);
|
||||
user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE);
|
||||
user.setApprovalStatus(PortalUserEnums.ApprovalStatus.COMPLETED);
|
||||
user.setAccountLockYn("N");
|
||||
user.setLoginFailureCount(0);
|
||||
user.setPasswordHash(passwordEncoder.encode(password));
|
||||
user.setPasswordChangeDate(LocalDateTime.now());
|
||||
String normalizedMobile = PhoneNumberUtil.normalize(mobile);
|
||||
if (normalizedMobile != null) {
|
||||
user.setMobileNumber(normalizedMobile);
|
||||
}
|
||||
portalUserRepository.save(user);
|
||||
userCreated = false;
|
||||
} else {
|
||||
PortalUserRegistrationDTO dto = new PortalUserRegistrationDTO();
|
||||
dto.setLoginId(managerEmail);
|
||||
dto.setUserName(userName);
|
||||
dto.setPassword(password);
|
||||
dto.setMobileNumber(mobile);
|
||||
// emailVerified=true — 가입 즉시 ACTIVE/COMPLETED (승인 대기 없음), registrationType="corporate" → ROLE_CORP_MANAGER
|
||||
user = portalUserService.createUserWithOrg(dto, org, "corporate", true);
|
||||
userCreated = true;
|
||||
}
|
||||
|
||||
TestCleanupResult result = TestCleanupResult.found(org.getId());
|
||||
result.put("PTL_ORG_CREATED", orgCreated ? 1L : 0L);
|
||||
result.put("PTL_USER_CREATED", userCreated ? 1L : 0L);
|
||||
log.info("테스트 정리 - 보조 법인 준비 완료: compRegNo={}, orgId={}, orgCreated={}, managerEmail={}, userCreated={}",
|
||||
compRegNo, org.getId(), orgCreated, managerEmail, userCreated);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Status 그룹공개 필터 Playwright 검증용 픽스처를 만든다. {@code managerEmail} 로 제한 대상
|
||||
* 법인을 역산하므로(환경마다 org id 를 몰라도 됨), 이 환경에 실제로 게시된(displayYn=Y) API 중
|
||||
* 하나를 동적으로 골라 그 API 의 {@code displayOrg} 를 해당 법인으로 제한한 뒤, 그 API 에 영향을
|
||||
* 준 것으로 기록된 종결(RESOLVED) 인시던트 1건을 만든다 — API Status 메인(P6)과 전체 이력
|
||||
* (P9/P10) 양쪽에서 동시에 검증 가능하도록 최근 시각으로 둔다.
|
||||
*
|
||||
* <p>특정 api_id 를 하드코딩하지 않는다 — 환경(dev/local/stage)마다 카탈로그가 다르기 때문이다.
|
||||
* {@code notice-incident.spec.ts} 가 기본으로 쓰는 API 는 동시실행 충돌을 피하려 후보에서
|
||||
* 제외한다(있으면).</p>
|
||||
*
|
||||
* <p>후보는 {@code PTL_API_SPEC_INFO.DISPLAY_YN='Y'} 만으로 고르지 않는다 — 포털 카탈로그
|
||||
* 노출은 그것만으로 결정되지 않고 AGWAPP.API_GROUP_API 매핑까지 있어야 한다
|
||||
* ({@link ApiStatusCatalogService#getSelectableApis()} 주석 참고). 그 조건까지 반영된 실제
|
||||
* 카탈로그 조회 경로({@link ApiSearchFacade#searchApis})를 그대로 재사용해야, 뽑은 API 가
|
||||
* {@code ApiStatusCatalogService#getVisibleApiIdsForQuery()}(= 이 검증이 실제로 통제하는
|
||||
* visibleApiIds)에도 반드시 잡힌다. 이 메서드는 비인증 컨텍스트에서 호출되므로
|
||||
* {@code searchApis} 결과는 이미 "공개범위 제한 없음"만 걸러져 있다(비로그인 판정 —
|
||||
* {@code ApiPermissionFilter.checkApiPermissions} 의 !isAuthenticated 분기).</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public GroupRestrictedIncidentFixture ensureGroupRestrictedIncident(
|
||||
String managerEmail, String subject, String summary) {
|
||||
assertNonProdProfile();
|
||||
|
||||
PortalUser manager = portalUserRepository.findPortalUserByEmailAddr(managerEmail)
|
||||
.orElseThrow(() -> new IllegalArgumentException("관리자 계정을 찾을 수 없습니다: " + managerEmail));
|
||||
PortalOrg org = manager.getPortalOrg();
|
||||
if (org == null) {
|
||||
throw new IllegalArgumentException("관리자 계정이 법인에 소속되어 있지 않습니다: " + managerEmail);
|
||||
}
|
||||
|
||||
Object apisObj = apiSearchFacade.searchApis(new ApiGroupSearch()).get("apis");
|
||||
List<ApiSpecInfoDto> catalog = apisObj instanceof List ? (List<ApiSpecInfoDto>) apisObj : Collections.emptyList();
|
||||
if (catalog.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"이 환경에 공개범위 제한 없이 카탈로그에 노출된 API가 하나도 없습니다. "
|
||||
+ "API 그룹에 편성된 API Spec을 하나 이상 게시해야 이 검증을 진행할 수 있습니다.");
|
||||
}
|
||||
String candidateApiId = catalog.stream()
|
||||
.map(ApiSpecInfoDto::getApiId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(apiId -> !RESERVED_DEFAULT_API_ID.equals(apiId))
|
||||
.findFirst()
|
||||
.orElseGet(() -> catalog.get(0).getApiId());
|
||||
|
||||
ApiSpecInfo target = apiSpecInfoRepository.findById(candidateApiId)
|
||||
.orElseThrow(() -> new IllegalStateException("카탈로그 API 스펙을 찾지 못했습니다: " + candidateApiId));
|
||||
|
||||
String previousDisplayOrg = target.getDisplayOrg();
|
||||
String previousDisplayRoleCode = target.getDisplayRoleCode();
|
||||
target.setDisplayOrg(org.getId());
|
||||
apiSpecInfoRepository.save(target);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DjbApistatusIncident incident = new DjbApistatusIncident();
|
||||
incident.setKind(IncidentKind.INCIDENT);
|
||||
incident.setState(IncidentState.RESOLVED);
|
||||
incident.setTitle(subject);
|
||||
incident.setSummary(StringUtils.defaultString(summary));
|
||||
incident.setStartedAt(now.minusHours(1));
|
||||
incident.setEndAt(now);
|
||||
incident.setDraftYn("N");
|
||||
incident.setCreatedBy(TEST_FIXTURE_CREATED_BY);
|
||||
incident = djbApistatusIncidentRepository.save(incident);
|
||||
|
||||
DjbApistatusIncidentApi incidentApi = new DjbApistatusIncidentApi();
|
||||
incidentApi.setIncidentId(incident.getIncidentId());
|
||||
incidentApi.setApiId(target.getApiId());
|
||||
incidentApi.setApiName(target.getApiName());
|
||||
incidentApi.setCreatedBy(TEST_FIXTURE_CREATED_BY);
|
||||
djbApistatusIncidentApiRepository.save(incidentApi);
|
||||
|
||||
log.info("테스트 정리 - 그룹전용 인시던트 준비 완료: apiId={}, orgId={}, incidentId={}, previousDisplayOrg={}",
|
||||
target.getApiId(), org.getId(), incident.getIncidentId(), previousDisplayOrg);
|
||||
return new GroupRestrictedIncidentFixture(target.getApiId(), target.getApiName(), org.getId(),
|
||||
incident.getIncidentId(), previousDisplayOrg, previousDisplayRoleCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #ensureGroupRestrictedIncident} 로 만든 픽스처를 원복한다 — 인시던트/영향API 행 삭제 +
|
||||
* 대상 API 의 공개범위를 호출자가 넘긴 원래값으로 되돌린다. 정확한 {@code apiId}/{@code incidentId}
|
||||
* 를 요구해 와일드카드 삭제가 없다.
|
||||
*/
|
||||
public TestCleanupResult revertGroupRestrictedIncident(
|
||||
String apiId, Long incidentId, String previousDisplayOrg, String previousDisplayRoleCode) {
|
||||
assertNonProdProfile();
|
||||
|
||||
Optional<DjbApistatusIncident> incidentOpt = djbApistatusIncidentRepository.findById(incidentId);
|
||||
if (!incidentOpt.isPresent()) {
|
||||
return TestCleanupResult.notFound();
|
||||
}
|
||||
|
||||
djbApistatusIncidentApiRepository.deleteByIncidentId(incidentId);
|
||||
djbApistatusIncidentRepository.deleteById(incidentId);
|
||||
|
||||
Optional<ApiSpecInfo> specOpt = apiSpecInfoRepository.findById(apiId);
|
||||
if (specOpt.isPresent()) {
|
||||
ApiSpecInfo spec = specOpt.get();
|
||||
spec.setDisplayOrg(previousDisplayOrg);
|
||||
spec.setDisplayRoleCode(previousDisplayRoleCode);
|
||||
apiSpecInfoRepository.save(spec);
|
||||
}
|
||||
|
||||
TestCleanupResult result = TestCleanupResult.found(String.valueOf(incidentId));
|
||||
result.put("DJB_APISTATUS_INCIDENT", 1L);
|
||||
result.put("DJB_APISTATUS_INCIDENT_API", 1L);
|
||||
log.info("테스트 정리 - 그룹전용 인시던트 원복 완료: apiId={}, incidentId={}", apiId, incidentId);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void assertNonProdProfile() {
|
||||
if (environment.acceptsProfiles(Profiles.of("stage", "prod"))) {
|
||||
throw new IllegalStateException("stage/prod 환경에서는 테스트 정리 API를 수행할 수 없습니다.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+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));
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -23,6 +23,9 @@ import com.eactive.apim.portal.apps.community.partnership.repository.Partnership
|
||||
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;
|
||||
@@ -77,6 +80,10 @@ class TestCleanupServiceTest {
|
||||
@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;
|
||||
|
||||
@@ -94,7 +101,9 @@ class TestCleanupServiceTest {
|
||||
userLogRepository, credentialRepository, appRequestRepository, approvalService,
|
||||
webhookRequestRepository, webhookRequestApiRepository,
|
||||
webhookRequestEventRepository, webhookService, userInvitationRepository, fileService,
|
||||
nativeQueries, portalUserService, passwordEncoder);
|
||||
nativeQueries, portalUserService, passwordEncoder,
|
||||
apiSpecInfoRepository, djbApistatusIncidentRepository, djbApistatusIncidentApiRepository,
|
||||
apiSearchFacade);
|
||||
when(environment.acceptsProfiles(Profiles.of("stage", "prod"))).thenReturn(false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user