feat: 추천 작업 5종 일괄 진행 (4 agents 병렬)
[A] 등록 모달 7페이지 (frontend) - CompanyList / ProductList / Commission / Payout / Override / Chargeback / ExceptionCode - ModalForm 580px, Switch Y/N 변환, 날짜 dayjs, PermissionButton - api/company/product/rule.ts 에 SaveReq + create/update/remove 추가 [B] BatchRun + RoleList 보강 (frontend) - BatchRun: 정산월/보험사 선택 → 실행, antd Steps 8단계 + 5초 폴링 (refetchInterval 자동 정지), KPI 4 카드, 최근 이력 - RoleList: 좌(역할 목록) 우(메뉴×6권한 매트릭스 체크박스), DIRECTORY 들여쓰기, 전체 토글, Modal.confirm 후 저장 [C] 이체파일 + 은행 어댑터 (api) - BankTransferFileGenerator 인터페이스 + GenericCsvTransferGenerator (UTF-8 BOM, 표준 CSV) + KbBankTransferGenerator (088, 고정폭 + 합계) - BankTransferFactory (Spring Map 빈) - TransferFileService: PENDING 조회 → 파일 생성 → file_storage 적재 → pay_file_ref 갱신 → status=SENT - POST /api/payments/transfer-file (DataChangeLog + EXPORT 권한) - account_no EncryptTypeHandler 자동 복호화 후 파일에 사용 - FileService.saveBytes(byte[]) 오버로드 추가 - PaymentList: 이체파일 생성 ModalForm + 다운로드 컬럼 [D] MockMvc Controller 테스트 (api) - AbstractControllerTest (SpringBootTest + MockMvc, JwtFilter doAnswer 로 chain 통과 처리), TestSecurityConfig (filter 빈 override) - 5 클래스 36 테스트: Agent/Contract/Ledger/Settle/User Controller - happy path + validation 400 + 권한 403 + 인증 401 검증: - ./gradlew clean build 성공 - 백엔드 단위테스트 94건 (common 33 + batch 19 + api 36 + 기타 6) 통과 - 프론트 tsc -b / vite build 통과 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.ga.api.controller.settle;
|
||||
|
||||
import com.ga.api.service.transfer.TransferFileService;
|
||||
import com.ga.common.annotation.DataChangeLog;
|
||||
import com.ga.common.annotation.RequirePermission;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
@@ -7,6 +8,8 @@ import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.settle.PaymentMapper;
|
||||
import com.ga.core.vo.settle.PaymentResp;
|
||||
import com.ga.core.vo.settle.SettleSearchParam;
|
||||
import com.ga.core.vo.settle.TransferFileResp;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -20,6 +23,7 @@ import java.util.Map;
|
||||
public class PaymentController {
|
||||
|
||||
private final PaymentMapper mapper;
|
||||
private final TransferFileService transferFileService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "PAYMENT", perm = "READ")
|
||||
@@ -35,4 +39,14 @@ public class PaymentController {
|
||||
mapper.updateStatus(paymentId, body.get("status"), body.get("failReason"));
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "이체파일 생성", description = "정산월+은행코드 기준 PENDING 지급건을 이체파일로 생성하고 SENT 상태로 갱신")
|
||||
@PostMapping("/transfer-file")
|
||||
@RequirePermission(menu = "PAYMENT", perm = "EXPORT")
|
||||
@DataChangeLog(menu = "PAYMENT", table = "payment")
|
||||
public ApiResponse<TransferFileResp> generateTransferFile(
|
||||
@RequestParam String settleMonth,
|
||||
@RequestParam String bankCode) {
|
||||
return ApiResponse.ok(transferFileService.generate(settleMonth, bankCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.service.transfer;
|
||||
|
||||
import com.ga.api.transfer.BankTransferFactory;
|
||||
import com.ga.api.transfer.BankTransferFileGenerator;
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.file.FileService;
|
||||
import com.ga.common.util.EncryptUtil;
|
||||
import com.ga.core.mapper.settle.PaymentMapper;
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
import com.ga.core.vo.settle.TransferFileResp;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TransferFileService {
|
||||
|
||||
private final PaymentMapper paymentMapper;
|
||||
private final BankTransferFactory factory;
|
||||
private final FileService fileService;
|
||||
private final EncryptUtil encryptUtil;
|
||||
|
||||
@Transactional
|
||||
public TransferFileResp generate(String settleMonth, String bankCode) {
|
||||
// 1) 해당 정산월 + 은행의 PENDING payment 조회 (accountNo는 EncryptTypeHandler로 자동복호화됨)
|
||||
List<PaymentVO> payments = paymentMapper.selectPendingByMonthAndBank(settleMonth, bankCode);
|
||||
|
||||
if (payments.isEmpty()) {
|
||||
throw new BizException(ErrorCode.NOT_FOUND, "이체 대상 지급 건이 없습니다 (settleMonth=" + settleMonth + ", bankCode=" + bankCode + ")");
|
||||
}
|
||||
|
||||
// 2) 은행별 생성기로 byte[] 생성 (accountNo는 이미 복호화된 상태)
|
||||
BankTransferFileGenerator generator = factory.get(bankCode);
|
||||
byte[] fileContent = generator.generate(payments, settleMonth);
|
||||
|
||||
// 3) FileService로 file_storage 적재 → fileId 반환
|
||||
String fileName = "transfer_" + settleMonth + "_" + bankCode + generator.getFileExtension();
|
||||
Long fileId = fileService.saveBytes(fileContent, fileName, "TRANSFER_FILE", "application/octet-stream");
|
||||
|
||||
// 4) payment들의 transfer_file_id 갱신 + status=SENT
|
||||
List<Long> paymentIds = payments.stream()
|
||||
.map(PaymentVO::getPaymentId)
|
||||
.collect(Collectors.toList());
|
||||
paymentMapper.updateTransferFile(paymentIds, fileId);
|
||||
|
||||
// 5) 합계 금액 계산
|
||||
BigDecimal totalAmount = payments.stream()
|
||||
.map(p -> p.getPayAmount() != null ? p.getPayAmount() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
return new TransferFileResp(fileId, fileName, payments.size(), totalAmount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ga.api.transfer;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 은행코드 → BankTransferFileGenerator 조회 팩토리.
|
||||
* 등록되지 않은 은행코드는 GENERIC_CSV 기본 생성기로 폴백.
|
||||
*/
|
||||
@Component
|
||||
public class BankTransferFactory {
|
||||
|
||||
private final Map<String, BankTransferFileGenerator> generators;
|
||||
|
||||
public BankTransferFactory(List<BankTransferFileGenerator> list) {
|
||||
this.generators = list.stream()
|
||||
.collect(Collectors.toMap(BankTransferFileGenerator::getBankCode, Function.identity()));
|
||||
}
|
||||
|
||||
public BankTransferFileGenerator get(String bankCode) {
|
||||
return generators.getOrDefault(bankCode, generators.get("GENERIC_CSV"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ga.api.transfer;
|
||||
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 은행별 이체파일 생성 인터페이스.
|
||||
* 새 은행 포맷 추가 시 이 인터페이스 구현체만 추가하면 됨.
|
||||
*/
|
||||
public interface BankTransferFileGenerator {
|
||||
|
||||
/** 은행 코드 ("088", "020" 등) 또는 기본 "GENERIC_CSV" */
|
||||
String getBankCode();
|
||||
|
||||
/** 파일 확장자 ".csv", ".dat" 등 */
|
||||
String getFileExtension();
|
||||
|
||||
/**
|
||||
* 이체파일 바이트 생성.
|
||||
* accountNo 는 PaymentVO 에서 이미 복호화된 상태로 전달됨.
|
||||
*/
|
||||
byte[] generate(List<PaymentVO> payments, String settleMonth);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ga.api.transfer;
|
||||
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 기본 CSV 이체파일 생성기. 은행별 전용 포맷이 없을 때 사용.
|
||||
* UTF-8 BOM + 헤더 + 데이터 행.
|
||||
*/
|
||||
@Component
|
||||
public class GenericCsvTransferGenerator implements BankTransferFileGenerator {
|
||||
|
||||
private static final byte[] UTF8_BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
|
||||
|
||||
@Override
|
||||
public String getBankCode() {
|
||||
return "GENERIC_CSV";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileExtension() {
|
||||
return ".csv";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] generate(List<PaymentVO> payments, String settleMonth) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("정산월,은행코드,계좌번호,수취인,금액,적요\r\n");
|
||||
|
||||
for (PaymentVO p : payments) {
|
||||
sb.append(csv(settleMonth)).append(",");
|
||||
sb.append(csv(p.getBankCode())).append(",");
|
||||
sb.append(csv(p.getAccountNo())).append(","); // 이미 복호화된 값
|
||||
sb.append(csv(agentLabel(p))).append(",");
|
||||
sb.append(p.getPayAmount() != null ? p.getPayAmount().toPlainString() : "0").append(",");
|
||||
sb.append(csv("수수료 정산 " + formatMonth(settleMonth))).append("\r\n");
|
||||
}
|
||||
|
||||
byte[] body = sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] result = new byte[UTF8_BOM.length + body.length];
|
||||
System.arraycopy(UTF8_BOM, 0, result, 0, UTF8_BOM.length);
|
||||
System.arraycopy(body, 0, result, UTF8_BOM.length, body.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private String csv(String val) {
|
||||
if (val == null) return "";
|
||||
if (val.contains(",") || val.contains("\"") || val.contains("\n")) {
|
||||
return "\"" + val.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private String agentLabel(PaymentVO p) {
|
||||
return p.getAgentId() != null ? "설계사#" + p.getAgentId() : "";
|
||||
}
|
||||
|
||||
/** "202501" → "2025-01" */
|
||||
private String formatMonth(String settleMonth) {
|
||||
if (settleMonth != null && settleMonth.length() == 6) {
|
||||
return settleMonth.substring(0, 4) + "-" + settleMonth.substring(4, 6);
|
||||
}
|
||||
return settleMonth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ga.api.transfer;
|
||||
|
||||
import com.ga.core.vo.settle.PaymentVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* KB국민은행(088) 고정폭 텍스트 이체파일 생성기.
|
||||
* 레코드 구조 (총 80바이트):
|
||||
* 은행코드(3) + 계좌번호(20) + 수취인명(20) + 금액(15, 우측정렬) + 적요(20) + CRLF(2)
|
||||
*/
|
||||
@Component
|
||||
public class KbBankTransferGenerator implements BankTransferFileGenerator {
|
||||
|
||||
private static final String BANK_CODE = "088";
|
||||
|
||||
@Override
|
||||
public String getBankCode() {
|
||||
return BANK_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileExtension() {
|
||||
return ".dat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] generate(List<PaymentVO> payments, String settleMonth) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
|
||||
for (PaymentVO p : payments) {
|
||||
BigDecimal amt = p.getPayAmount() != null ? p.getPayAmount() : BigDecimal.ZERO;
|
||||
totalAmount = totalAmount.add(amt);
|
||||
|
||||
sb.append(rpad(BANK_CODE, 3));
|
||||
sb.append(rpad(nvl(p.getAccountNo()), 20)); // 복호화된 계좌번호
|
||||
sb.append(rpad("설계사#" + p.getAgentId(), 20));
|
||||
sb.append(lpad(amt.longValue(), 15));
|
||||
sb.append(rpad("수수료 정산 " + formatMonth(settleMonth), 20));
|
||||
sb.append("\r\n");
|
||||
}
|
||||
|
||||
// 합계 레코드
|
||||
sb.append(rpad("999", 3));
|
||||
sb.append(rpad("", 20));
|
||||
sb.append(rpad("합계", 20));
|
||||
sb.append(lpad(totalAmount.longValue(), 15));
|
||||
sb.append(rpad("TOTAL:" + payments.size() + "건", 20));
|
||||
sb.append("\r\n");
|
||||
|
||||
return sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String rpad(String val, int width) {
|
||||
if (val == null) val = "";
|
||||
if (val.length() >= width) return val.substring(0, width);
|
||||
return val + " ".repeat(width - val.length());
|
||||
}
|
||||
|
||||
private String lpad(long val, int width) {
|
||||
String s = String.valueOf(val);
|
||||
if (s.length() >= width) return s.substring(s.length() - width);
|
||||
return " ".repeat(width - s.length()) + s;
|
||||
}
|
||||
|
||||
private String nvl(String val) {
|
||||
return val != null ? val : "";
|
||||
}
|
||||
|
||||
private String formatMonth(String settleMonth) {
|
||||
if (settleMonth != null && settleMonth.length() == 6) {
|
||||
return settleMonth.substring(0, 4) + "-" + settleMonth.substring(4, 6);
|
||||
}
|
||||
return settleMonth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.ga.api.controller.ledger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.ledger.LedgerService;
|
||||
import com.ga.api.support.AbstractControllerTest;
|
||||
import com.ga.api.support.MockLoginUser;
|
||||
import com.ga.common.excel.ExcelService;
|
||||
import com.ga.common.menu.PermissionChecker;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.ledger.ExceptionLedgerResp;
|
||||
import com.ga.core.vo.ledger.ExceptionLedgerSaveReq;
|
||||
import com.ga.core.vo.ledger.LedgerResp;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class LedgerControllerTest extends AbstractControllerTest {
|
||||
|
||||
@Autowired ObjectMapper objectMapper;
|
||||
|
||||
@MockBean LedgerService service;
|
||||
@MockBean ExcelService excelService;
|
||||
@MockBean PermissionChecker permissionChecker;
|
||||
|
||||
@BeforeEach
|
||||
void permitAll() {
|
||||
given(permissionChecker.hasPermission(anyLong(), anyString(), anyString())).willReturn(true);
|
||||
}
|
||||
|
||||
// ---- 픽스처 ----
|
||||
|
||||
private PageResponse<LedgerResp> singlePage() {
|
||||
LedgerResp r = new LedgerResp();
|
||||
r.setLedgerId(100L);
|
||||
r.setAgentName("홍길동");
|
||||
r.setPolicyNo("POL-001");
|
||||
return PageResponse.<LedgerResp>builder()
|
||||
.list(List.of(r))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ExceptionLedgerSaveReq validExceptionReq() {
|
||||
ExceptionLedgerSaveReq req = new ExceptionLedgerSaveReq();
|
||||
req.setAgentId(1L);
|
||||
req.setExceptionCode("EXTRA_PAY");
|
||||
req.setSettleMonth("202401");
|
||||
req.setAmount(new BigDecimal("50000"));
|
||||
return req;
|
||||
}
|
||||
|
||||
// ---- 테스트 ----
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/ledger/recruit - 200 모집 원장 목록")
|
||||
void listRecruit_ok() throws Exception {
|
||||
given(service.listRecruit(any())).willReturn(singlePage());
|
||||
|
||||
mockMvc.perform(get("/api/ledger/recruit").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.list[0].policyNo").value("POL-001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/ledger/maintain - 200 유지 원장 목록")
|
||||
void listMaintain_ok() throws Exception {
|
||||
given(service.listMaintain(any())).willReturn(singlePage());
|
||||
|
||||
mockMvc.perform(get("/api/ledger/maintain").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/ledger/exception - 200 예외 원장 목록")
|
||||
void listException_ok() throws Exception {
|
||||
ExceptionLedgerResp er = new ExceptionLedgerResp();
|
||||
er.setLedgerId(200L);
|
||||
er.setExceptionCode("EXTRA_PAY");
|
||||
PageResponse<ExceptionLedgerResp> page = PageResponse.<ExceptionLedgerResp>builder()
|
||||
.list(List.of(er))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
given(service.listException(any())).willReturn(page);
|
||||
|
||||
mockMvc.perform(get("/api/ledger/exception").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.list[0].exceptionCode").value("EXTRA_PAY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/ledger/exception - 200 예외 원장 등록")
|
||||
void createException_ok() throws Exception {
|
||||
given(service.createException(any())).willReturn(200L);
|
||||
|
||||
mockMvc.perform(post("/api/ledger/exception")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validExceptionReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").value(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/ledger/exception - 400 필수값 누락 (agentId null)")
|
||||
void createException_validationFail() throws Exception {
|
||||
ExceptionLedgerSaveReq req = new ExceptionLedgerSaveReq();
|
||||
req.setExceptionCode("EXTRA_PAY");
|
||||
req.setSettleMonth("202401");
|
||||
req.setAmount(new BigDecimal("50000"));
|
||||
// agentId 없음
|
||||
|
||||
mockMvc.perform(post("/api/ledger/exception")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/ledger/exception/{id}/approve - 200 승인 처리")
|
||||
void approveException_ok() throws Exception {
|
||||
mockMvc.perform(put("/api/ledger/exception/200/approve")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(Map.of("status", "APPROVED"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/ledger/recruit - 401 인증 없는 요청")
|
||||
void listRecruit_unauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/ledger/recruit"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.ga.api.controller.org;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.org.AgentService;
|
||||
import com.ga.api.support.AbstractControllerTest;
|
||||
import com.ga.api.support.MockLoginUser;
|
||||
import com.ga.common.excel.ExcelService;
|
||||
import com.ga.common.menu.PermissionChecker;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.org.AgentResp;
|
||||
import com.ga.core.vo.org.AgentSaveReq;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willDoNothing;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class AgentControllerTest extends AbstractControllerTest {
|
||||
|
||||
@Autowired ObjectMapper objectMapper;
|
||||
|
||||
@MockBean AgentService agentService;
|
||||
@MockBean ExcelService excelService;
|
||||
@MockBean PermissionChecker permissionChecker;
|
||||
|
||||
@BeforeEach
|
||||
void permitAll() {
|
||||
given(permissionChecker.hasPermission(anyLong(), anyString(), anyString())).willReturn(true);
|
||||
}
|
||||
|
||||
// ---- 픽스처 ----
|
||||
|
||||
private AgentResp sampleAgent() {
|
||||
AgentResp r = new AgentResp();
|
||||
r.setAgentId(1L);
|
||||
r.setAgentName("홍길동");
|
||||
r.setStatus("ACTIVE");
|
||||
return r;
|
||||
}
|
||||
|
||||
private AgentSaveReq validReq() {
|
||||
AgentSaveReq req = new AgentSaveReq();
|
||||
req.setAgentName("홍길동");
|
||||
req.setOrgId(1L);
|
||||
req.setGradeId(1L);
|
||||
return req;
|
||||
}
|
||||
|
||||
// ---- 테스트 ----
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/agents - 200 목록 반환")
|
||||
void list_ok() throws Exception {
|
||||
PageResponse<AgentResp> page = PageResponse.<AgentResp>builder()
|
||||
.list(List.of(sampleAgent()))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
given(agentService.list(any())).willReturn(page);
|
||||
|
||||
mockMvc.perform(get("/api/agents").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].agentName").value("홍길동"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/agents/{id} - 200 상세 반환")
|
||||
void detail_ok() throws Exception {
|
||||
given(agentService.detail(1L)).willReturn(sampleAgent());
|
||||
|
||||
mockMvc.perform(get("/api/agents/1").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.agentId").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/agents - 200 등록 성공")
|
||||
void create_ok() throws Exception {
|
||||
given(agentService.create(any())).willReturn(1L);
|
||||
|
||||
mockMvc.perform(post("/api/agents")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/agents - 400 필수값 누락 (agentName null)")
|
||||
void create_validationFail() throws Exception {
|
||||
AgentSaveReq req = new AgentSaveReq();
|
||||
req.setOrgId(1L);
|
||||
req.setGradeId(1L);
|
||||
// agentName 없음
|
||||
|
||||
mockMvc.perform(post("/api/agents")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/agents/{id} - 200 수정 성공")
|
||||
void update_ok() throws Exception {
|
||||
willDoNothing().given(agentService).update(eq(1L), any());
|
||||
|
||||
mockMvc.perform(put("/api/agents/1")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/agents - 401 인증 없는 요청")
|
||||
void list_unauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/agents"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/agents - 403 권한 없는 요청")
|
||||
void list_forbidden() throws Exception {
|
||||
given(permissionChecker.hasPermission(anyLong(), eq("ORG_AGENT"), eq("READ"))).willReturn(false);
|
||||
|
||||
mockMvc.perform(get("/api/agents").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.ga.api.controller.product;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.product.ContractService;
|
||||
import com.ga.api.support.AbstractControllerTest;
|
||||
import com.ga.api.support.MockLoginUser;
|
||||
import com.ga.common.menu.PermissionChecker;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.product.ContractResp;
|
||||
import com.ga.core.vo.product.ContractSaveReq;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willDoNothing;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class ContractControllerTest extends AbstractControllerTest {
|
||||
|
||||
@Autowired ObjectMapper objectMapper;
|
||||
|
||||
@MockBean ContractService service;
|
||||
@MockBean PermissionChecker permissionChecker;
|
||||
|
||||
@BeforeEach
|
||||
void permitAll() {
|
||||
given(permissionChecker.hasPermission(anyLong(), anyString(), anyString())).willReturn(true);
|
||||
}
|
||||
|
||||
// ---- 픽스처 ----
|
||||
|
||||
private ContractResp sampleContract() {
|
||||
ContractResp r = new ContractResp();
|
||||
r.setContractId(10L);
|
||||
r.setPolicyNo("POL-2024-001");
|
||||
r.setContractorName("김계약");
|
||||
r.setStatus("ACTIVE");
|
||||
return r;
|
||||
}
|
||||
|
||||
private ContractSaveReq validReq() {
|
||||
ContractSaveReq req = new ContractSaveReq();
|
||||
req.setAgentId(1L);
|
||||
req.setProductId(2L);
|
||||
req.setPolicyNo("POL-2024-002");
|
||||
req.setPremium(new BigDecimal("100000"));
|
||||
req.setContractDate(LocalDate.of(2024, 1, 1));
|
||||
return req;
|
||||
}
|
||||
|
||||
// ---- 테스트 ----
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/contracts - 200 목록 반환")
|
||||
void list_ok() throws Exception {
|
||||
PageResponse<ContractResp> page = PageResponse.<ContractResp>builder()
|
||||
.list(List.of(sampleContract()))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
given(service.list(any())).willReturn(page);
|
||||
|
||||
mockMvc.perform(get("/api/contracts").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].policyNo").value("POL-2024-001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/contracts/{id} - 200 상세 반환")
|
||||
void detail_ok() throws Exception {
|
||||
given(service.detail(10L)).willReturn(sampleContract());
|
||||
|
||||
mockMvc.perform(get("/api/contracts/10").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.contractId").value(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/contracts - 200 등록 성공")
|
||||
void create_ok() throws Exception {
|
||||
given(service.create(any())).willReturn(10L);
|
||||
|
||||
mockMvc.perform(post("/api/contracts")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").value(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/contracts - 400 policyNo 누락")
|
||||
void create_missingPolicyNo() throws Exception {
|
||||
ContractSaveReq req = new ContractSaveReq();
|
||||
req.setAgentId(1L);
|
||||
req.setProductId(2L);
|
||||
req.setPremium(new BigDecimal("100000"));
|
||||
req.setContractDate(LocalDate.of(2024, 1, 1));
|
||||
// policyNo 없음
|
||||
|
||||
mockMvc.perform(post("/api/contracts")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/contracts/{id} - 200 수정 성공")
|
||||
void update_ok() throws Exception {
|
||||
willDoNothing().given(service).update(eq(10L), any());
|
||||
|
||||
mockMvc.perform(put("/api/contracts/10")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/contracts - 401 인증 없는 요청")
|
||||
void list_unauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/contracts"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/contracts - 403 권한 없는 요청")
|
||||
void list_forbidden() throws Exception {
|
||||
given(permissionChecker.hasPermission(anyLong(), eq("CONTRACT_LIST"), eq("READ"))).willReturn(false);
|
||||
|
||||
mockMvc.perform(get("/api/contracts").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.ga.api.controller.settle;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.settle.SettleService;
|
||||
import com.ga.api.support.AbstractControllerTest;
|
||||
import com.ga.api.support.MockLoginUser;
|
||||
import com.ga.common.menu.PermissionChecker;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.settle.SettleMasterResp;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willDoNothing;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class SettleControllerTest extends AbstractControllerTest {
|
||||
|
||||
@Autowired ObjectMapper objectMapper;
|
||||
|
||||
@MockBean SettleService service;
|
||||
@MockBean PermissionChecker permissionChecker;
|
||||
|
||||
@BeforeEach
|
||||
void permitAll() {
|
||||
given(permissionChecker.hasPermission(anyLong(), anyString(), anyString())).willReturn(true);
|
||||
}
|
||||
|
||||
// ---- 픽스처 ----
|
||||
|
||||
private SettleMasterResp sampleSettle() {
|
||||
SettleMasterResp r = new SettleMasterResp();
|
||||
r.setSettleId(50L);
|
||||
r.setAgentId(1L);
|
||||
r.setAgentName("홍길동");
|
||||
r.setSettleMonth("202401");
|
||||
r.setStatus("CALCULATED");
|
||||
r.setNetAmount(new BigDecimal("300000"));
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- 테스트 ----
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/settle - 200 정산 목록 반환")
|
||||
void list_ok() throws Exception {
|
||||
PageResponse<SettleMasterResp> page = PageResponse.<SettleMasterResp>builder()
|
||||
.list(List.of(sampleSettle()))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
given(service.list(any())).willReturn(page);
|
||||
|
||||
mockMvc.perform(get("/api/settle").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.list[0].settleMonth").value("202401"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/settle/summary - 200 월별 요약")
|
||||
void summary_ok() throws Exception {
|
||||
given(service.summary("202401")).willReturn(
|
||||
Map.of("totalAmount", 1000000, "agentCount", 5));
|
||||
|
||||
mockMvc.perform(get("/api/settle/summary")
|
||||
.param("settleMonth", "202401")
|
||||
.with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.agentCount").value(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/settle/{agentId}/months/{settleMonth} - 200 상세")
|
||||
void detail_ok() throws Exception {
|
||||
given(service.detail(1L, "202401")).willReturn(sampleSettle());
|
||||
|
||||
mockMvc.perform(get("/api/settle/1/months/202401").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.settleId").value(50));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/settle/{id}/confirm - 200 확정 처리")
|
||||
void confirm_ok() throws Exception {
|
||||
willDoNothing().given(service).confirm(50L);
|
||||
|
||||
mockMvc.perform(put("/api/settle/50/confirm").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/settle/{id}/hold - 200 보류 처리")
|
||||
void hold_ok() throws Exception {
|
||||
willDoNothing().given(service).hold(eq(50L), anyString());
|
||||
|
||||
mockMvc.perform(put("/api/settle/50/hold")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(Map.of("reason", "검토 필요"))))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/settle/{id}/confirm - 403 APPROVE 권한 없음")
|
||||
void confirm_forbidden() throws Exception {
|
||||
given(permissionChecker.hasPermission(anyLong(), eq("SETTLE_LIST"), eq("APPROVE"))).willReturn(false);
|
||||
|
||||
mockMvc.perform(put("/api/settle/50/confirm").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/settle - 401 인증 없는 요청")
|
||||
void list_unauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/settle"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.ga.api.controller.system;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.api.service.system.UserService;
|
||||
import com.ga.api.support.AbstractControllerTest;
|
||||
import com.ga.api.support.MockLoginUser;
|
||||
import com.ga.common.menu.PermissionChecker;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.vo.user.UserResp;
|
||||
import com.ga.core.vo.user.UserSaveReq;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willDoNothing;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class UserControllerTest extends AbstractControllerTest {
|
||||
|
||||
@Autowired ObjectMapper objectMapper;
|
||||
|
||||
@MockBean UserService service;
|
||||
@MockBean PermissionChecker permissionChecker;
|
||||
|
||||
@BeforeEach
|
||||
void permitAll() {
|
||||
given(permissionChecker.hasPermission(anyLong(), anyString(), anyString())).willReturn(true);
|
||||
}
|
||||
|
||||
// ---- 픽스처 ----
|
||||
|
||||
private UserResp sampleUser() {
|
||||
UserResp r = new UserResp();
|
||||
r.setUserId(99L);
|
||||
r.setLoginId("admin");
|
||||
r.setUserName("관리자");
|
||||
r.setStatus("ACTIVE");
|
||||
return r;
|
||||
}
|
||||
|
||||
private UserSaveReq validReq() {
|
||||
UserSaveReq req = new UserSaveReq();
|
||||
req.setLoginId("newuser");
|
||||
req.setUserName("신규사용자");
|
||||
req.setEmail("new@example.com");
|
||||
return req;
|
||||
}
|
||||
|
||||
// ---- 테스트 ----
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/system/users - 200 사용자 목록")
|
||||
void list_ok() throws Exception {
|
||||
PageResponse<UserResp> page = PageResponse.<UserResp>builder()
|
||||
.list(List.of(sampleUser()))
|
||||
.pageNum(1).pageSize(10).total(1).pages(1)
|
||||
.build();
|
||||
given(service.list(any())).willReturn(page);
|
||||
|
||||
mockMvc.perform(get("/api/system/users").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.list[0].loginId").value("admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/system/users/{id} - 200 사용자 상세")
|
||||
void detail_ok() throws Exception {
|
||||
given(service.detail(99L)).willReturn(sampleUser());
|
||||
|
||||
mockMvc.perform(get("/api/system/users/99").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.userId").value(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/system/users - 200 사용자 등록 성공")
|
||||
void create_ok() throws Exception {
|
||||
given(service.create(any())).willReturn(99L);
|
||||
|
||||
mockMvc.perform(post("/api/system/users")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").value(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/system/users - 400 loginId 누락")
|
||||
void create_missingLoginId() throws Exception {
|
||||
UserSaveReq req = new UserSaveReq();
|
||||
req.setUserName("신규사용자");
|
||||
// loginId 없음
|
||||
|
||||
mockMvc.perform(post("/api/system/users")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/system/users/{id}/reset-password - 200 비밀번호 초기화")
|
||||
void resetPassword_ok() throws Exception {
|
||||
willDoNothing().given(service).resetPassword(99L);
|
||||
|
||||
mockMvc.perform(put("/api/system/users/99/reset-password").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/system/users/{id}/unlock - 200 계정 잠금 해제")
|
||||
void unlock_ok() throws Exception {
|
||||
willDoNothing().given(service).unlock(99L);
|
||||
|
||||
mockMvc.perform(put("/api/system/users/99/unlock").with(MockLoginUser.admin()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/system/users - 401 인증 없는 요청")
|
||||
void list_unauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/system/users"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/system/users - 403 권한 없는 요청")
|
||||
void create_forbidden() throws Exception {
|
||||
given(permissionChecker.hasPermission(anyLong(), eq("SYSTEM_USER"), eq("CREATE"))).willReturn(false);
|
||||
|
||||
mockMvc.perform(post("/api/system/users")
|
||||
.with(MockLoginUser.admin())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(validReq())))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.ga.api.support;
|
||||
|
||||
import com.ga.common.auth.JwtAuthFilter;
|
||||
import com.ga.common.auth.JwtTokenProvider;
|
||||
import com.ga.common.code.CommonCodeMapper;
|
||||
import com.ga.common.file.FileMapper;
|
||||
import com.ga.common.menu.MenuMapper;
|
||||
import com.ga.common.notification.NotificationMapper;
|
||||
import com.ga.common.system.ApiAccessLogService;
|
||||
import com.ga.common.system.DataChangeLogService;
|
||||
import com.ga.common.system.SystemConfigMapper;
|
||||
import com.ga.common.system.SystemLogMapper;
|
||||
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.org.AgentMapper;
|
||||
import com.ga.core.mapper.org.GradeMapper;
|
||||
import com.ga.core.mapper.org.OrganizationMapper;
|
||||
import com.ga.core.mapper.product.ContractMapper;
|
||||
import com.ga.core.mapper.product.InsuranceCompanyMapper;
|
||||
import com.ga.core.mapper.product.ProductMapper;
|
||||
import com.ga.core.mapper.receive.ReceiveMapper;
|
||||
import com.ga.core.mapper.rule.RuleMapper;
|
||||
import com.ga.core.mapper.settle.BatchJobLogMapper;
|
||||
import com.ga.core.mapper.settle.ChargebackMapper;
|
||||
import com.ga.core.mapper.settle.OverrideSettleMapper;
|
||||
import com.ga.core.mapper.settle.PaymentMapper;
|
||||
import com.ga.core.mapper.settle.ReconciliationMapper;
|
||||
import com.ga.core.mapper.settle.SettleMasterMapper;
|
||||
import com.ga.core.mapper.upload.UploadColumnMapper;
|
||||
import com.ga.core.mapper.upload.UploadHistoryMapper;
|
||||
import com.ga.core.mapper.upload.UploadTemplateMapper;
|
||||
import com.ga.core.mapper.user.RoleMapper;
|
||||
import com.ga.core.mapper.user.UserMapper;
|
||||
import com.ga.api.service.upload.LookupMapper;
|
||||
import com.ga.api.service.upload.DynamicInsertMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
|
||||
/**
|
||||
* Controller 테스트 베이스.
|
||||
*
|
||||
* @SpringBootTest + @AutoConfigureMockMvc 로 전체 Spring MVC 컨텍스트 사용.
|
||||
* application-test.yml 로 DB/Redis/Cache 자동설정 제외.
|
||||
* 모든 MyBatis Mapper 를 @MockBean 으로 등록하여 SqlSessionFactory 없이 동작.
|
||||
*
|
||||
* 각 테스트는:
|
||||
* - 사용할 Service 를 @MockBean 으로 추가
|
||||
* - PermissionChecker 를 @MockBean 으로 추가 후 hasPermission() stub
|
||||
* - MockLoginUser.admin() 으로 인증된 요청 실행
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Import(TestSecurityConfig.class)
|
||||
public abstract class AbstractControllerTest {
|
||||
|
||||
@Autowired protected MockMvc mockMvc;
|
||||
|
||||
// ---- 인증 ----
|
||||
@MockBean protected JwtAuthFilter jwtAuthFilter;
|
||||
@MockBean protected JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
/**
|
||||
* JwtAuthFilter 는 OncePerRequestFilter 를 상속한다.
|
||||
* @MockBean 으로 교체하면 doFilter() 가 chain.doFilter() 를 호출하지 않아
|
||||
* 요청이 DispatcherServlet 에 도달하지 못한다.
|
||||
* 이를 해결하기 위해 public doFilter() 를 pass-through 로 stub 한다.
|
||||
*/
|
||||
@BeforeEach
|
||||
void stubJwtFilter() throws Exception {
|
||||
doAnswer(inv -> {
|
||||
jakarta.servlet.ServletRequest req = inv.getArgument(0);
|
||||
jakarta.servlet.ServletResponse res = inv.getArgument(1);
|
||||
FilterChain chain = inv.getArgument(2);
|
||||
chain.doFilter(req, res);
|
||||
return null;
|
||||
}).when(jwtAuthFilter).doFilter(
|
||||
any(jakarta.servlet.ServletRequest.class),
|
||||
any(jakarta.servlet.ServletResponse.class),
|
||||
any(FilterChain.class));
|
||||
}
|
||||
|
||||
// ---- 로그 서비스 ----
|
||||
@MockBean protected DataChangeLogService dataChangeLogService;
|
||||
@MockBean protected ApiAccessLogService apiAccessLogService;
|
||||
|
||||
// ---- 공통 Mapper ----
|
||||
@MockBean protected CommonCodeMapper commonCodeMapper;
|
||||
@MockBean protected FileMapper fileMapper;
|
||||
@MockBean protected MenuMapper menuMapper;
|
||||
@MockBean protected NotificationMapper notificationMapper;
|
||||
@MockBean protected SystemConfigMapper systemConfigMapper;
|
||||
@MockBean protected SystemLogMapper systemLogMapper;
|
||||
|
||||
// ---- 도메인 Mapper ----
|
||||
@MockBean protected AgentMapper agentMapper;
|
||||
@MockBean protected GradeMapper gradeMapper;
|
||||
@MockBean protected OrganizationMapper organizationMapper;
|
||||
@MockBean protected ContractMapper contractMapper;
|
||||
@MockBean protected InsuranceCompanyMapper insuranceCompanyMapper;
|
||||
@MockBean protected ProductMapper productMapper;
|
||||
@MockBean protected RecruitLedgerMapper recruitLedgerMapper;
|
||||
@MockBean protected MaintainLedgerMapper maintainLedgerMapper;
|
||||
@MockBean protected ExceptionLedgerMapper exceptionLedgerMapper;
|
||||
@MockBean protected ReceiveMapper receiveMapper;
|
||||
@MockBean protected RuleMapper ruleMapper;
|
||||
@MockBean protected SettleMasterMapper settleMasterMapper;
|
||||
@MockBean protected PaymentMapper paymentMapper;
|
||||
@MockBean protected ChargebackMapper chargebackMapper;
|
||||
@MockBean protected OverrideSettleMapper overrideSettleMapper;
|
||||
@MockBean protected ReconciliationMapper reconciliationMapper;
|
||||
@MockBean protected BatchJobLogMapper batchJobLogMapper;
|
||||
@MockBean protected UploadHistoryMapper uploadHistoryMapper;
|
||||
@MockBean protected UploadTemplateMapper uploadTemplateMapper;
|
||||
@MockBean protected UploadColumnMapper uploadColumnMapper;
|
||||
@MockBean protected UserMapper userMapper;
|
||||
@MockBean protected RoleMapper roleMapper;
|
||||
|
||||
// ---- ga-api 내부 Mapper ----
|
||||
@MockBean protected LookupMapper lookupMapper;
|
||||
@MockBean protected DynamicInsertMapper dynamicInsertMapper;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ga.api.support;
|
||||
|
||||
import com.ga.common.auth.LoginUser;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 테스트에서 LoginUser 를 SecurityContext 에 직접 주입하는 헬퍼.
|
||||
*
|
||||
* 사용 예:
|
||||
* mockMvc.perform(get("/api/agents").with(MockLoginUser.admin()))
|
||||
*/
|
||||
public final class MockLoginUser {
|
||||
|
||||
private MockLoginUser() {}
|
||||
|
||||
/** userId=1, roleCode=SUPER_ADMIN 인 관리자 사용자 */
|
||||
public static RequestPostProcessor admin() {
|
||||
return asUser(1L, "admin", List.of("SUPER_ADMIN"));
|
||||
}
|
||||
|
||||
public static RequestPostProcessor asUser(Long userId, String loginId, List<String> roles) {
|
||||
LoginUser loginUser = LoginUser.builder()
|
||||
.userId(userId)
|
||||
.loginId(loginId)
|
||||
.userName("테스트사용자")
|
||||
.roleCodes(roles)
|
||||
.build();
|
||||
Authentication auth = new UsernamePasswordAuthenticationToken(
|
||||
loginUser, null, loginUser.getAuthorities());
|
||||
return SecurityMockMvcRequestPostProcessors.authentication(auth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ga.api.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* 테스트용 보안 설정.
|
||||
* - JWT 필터 없이 인증 여부만 체크
|
||||
* - 미인증 요청 → 401
|
||||
* - WebMvcConfig 를 포함하지 않으므로 Jackson 기본 설정 사용
|
||||
*/
|
||||
@TestConfiguration
|
||||
public class TestSecurityConfig {
|
||||
|
||||
/**
|
||||
* JavaTimeModule 포함한 ObjectMapper. WebMvcConfig 대신 사용.
|
||||
* @WebMvcTest 에서 LocalDateTime 직렬화 실패를 방지한다.
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper testObjectMapper() {
|
||||
return new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* SecurityConfig.filter() 를 오버라이드한다.
|
||||
* spring.main.allow-bean-definition-overriding=true 가 필요하다.
|
||||
*/
|
||||
@Bean("filter")
|
||||
public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(AbstractHttpConfigurer::disable)
|
||||
.exceptionHandling(eh -> eh
|
||||
.authenticationEntryPoint((req, res, ex) ->
|
||||
res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")))
|
||||
.authorizeHttpRequests(a -> a.anyRequest().authenticated());
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
spring:
|
||||
application:
|
||||
name: ga-api-test
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
|
||||
- org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration
|
||||
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
|
||||
|
||||
ga:
|
||||
jwt:
|
||||
secret: test-secret-key-for-unit-tests-only-256bits-long-placeholder
|
||||
access-token-expire: 7200000
|
||||
refresh-token-expire: 604800000
|
||||
encrypt:
|
||||
key: 0123456789abcdef0123456789abcdef
|
||||
|
||||
mybatis:
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.ga: WARN
|
||||
org.springframework.security: WARN
|
||||
org.springframework.web: WARN
|
||||
Reference in New Issue
Block a user