fix(p1r): 退役 Account 合同兜底路由
This commit is contained in:
parent
a37dc80609
commit
c4a8f4ea5d
@ -1,52 +0,0 @@
|
||||
package cn.iocoder.muse.module.member.controller.admin;
|
||||
|
||||
import cn.iocoder.muse.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.muse.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
/**
|
||||
* 管理后台 - Muse Account 合同入口。
|
||||
*
|
||||
* <p>该入口只负责 P1 合同路由兜底、写命令基础校验和统一响应封装;领域事实由后续 Application 服务逐步替换。</p>
|
||||
*/
|
||||
@Tag(name = "管理后台 - Muse Account 合同入口")
|
||||
@RestController
|
||||
@RequestMapping("/muse")
|
||||
@Validated
|
||||
public class AdminMuseAccountContractController {
|
||||
|
||||
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
|
||||
private static final Set<String> DOMAINS = Set.of("account");
|
||||
|
||||
@Resource
|
||||
private MuseContractPersistenceService contractPersistenceService;
|
||||
|
||||
/**
|
||||
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
|
||||
*
|
||||
* @param request Servlet 请求,用于取得真实方法、路径和命令头
|
||||
* @param queryParams 查询参数
|
||||
* @param body 可选 JSON 请求体
|
||||
* @return 合同占位响应
|
||||
*/
|
||||
@RequestMapping(value = {"/account/**"}, method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE})
|
||||
@Operation(summary = "处理 Muse Admin account 合同请求")
|
||||
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
|
||||
@RequestParam Map<String, String> queryParams,
|
||||
@RequestBody(required = false) Map<String, Object> body) {
|
||||
return success(contractPersistenceService.handle(DOMAINS, "admin", request.getMethod(), request.getRequestURI(),
|
||||
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package cn.iocoder.muse.module.member.controller.app;
|
||||
|
||||
import cn.iocoder.muse.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.muse.framework.mybatis.core.muse.MuseContractPersistenceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.muse.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
/**
|
||||
* 用户 APP - Muse Account 合同入口。
|
||||
*
|
||||
* <p>该入口只负责 P1 合同路由兜底、写命令基础校验和统一响应封装;领域事实由后续 Application 服务逐步替换。</p>
|
||||
*/
|
||||
@Tag(name = "用户 APP - Muse Account 合同入口")
|
||||
@RestController
|
||||
@RequestMapping("/muse")
|
||||
@Validated
|
||||
public class AppMuseAccountContractController {
|
||||
|
||||
/** 当前 Controller 拥有的 OpenAPI 合同域。 */
|
||||
private static final Set<String> DOMAINS = Set.of("account");
|
||||
|
||||
@Resource
|
||||
private MuseContractPersistenceService contractPersistenceService;
|
||||
|
||||
/**
|
||||
* 处理当前领域下尚未落成专用 Controller 的 OpenAPI 合同请求。
|
||||
*
|
||||
* @param request Servlet 请求,用于取得真实方法、路径和命令头
|
||||
* @param queryParams 查询参数
|
||||
* @param body 可选 JSON 请求体
|
||||
* @return 合同占位响应
|
||||
*/
|
||||
@RequestMapping(value = {"/me", "/profile", "/account/**"}, method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH, RequestMethod.DELETE})
|
||||
@Operation(summary = "处理 Muse APP account 合同请求")
|
||||
public CommonResult<Map<String, Object>> handle(HttpServletRequest request,
|
||||
@RequestParam Map<String, String> queryParams,
|
||||
@RequestBody(required = false) Map<String, Object> body) {
|
||||
return success(contractPersistenceService.handle(DOMAINS, "app", request.getMethod(), request.getRequestURI(),
|
||||
request.getHeader("X-Command-Id"), queryParams, body, getLoginUserId()));
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,12 +8,12 @@ import cn.iocoder.muse.module.member.application.account.AccountEntitlementServi
|
||||
import cn.iocoder.muse.module.member.application.account.AccountProfileService;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountQuotaService;
|
||||
import cn.iocoder.muse.module.member.controller.account.vo.AccountPageResult;
|
||||
import cn.iocoder.muse.module.member.controller.admin.AdminMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.admin.account.vo.AdminAccountUserSummaryRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.admin.account.vo.AdminBalanceSnapshotEntryRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.admin.account.vo.AdminUserEntitlementDetailRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.admin.account.vo.QuotaAdjustmentLedgerEntryRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.admin.account.vo.QuotaAdjustmentResultRespVO;
|
||||
import cn.iocoder.muse.module.member.domain.account.AccountUserIdResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
@ -31,6 +31,7 @@ import java.util.List;
|
||||
|
||||
import static cn.iocoder.muse.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
|
||||
import static cn.iocoder.muse.module.member.enums.ErrorCodeConstants.ACCOUNT_API_VERSION_UNSUPPORTED;
|
||||
import static cn.iocoder.muse.module.member.enums.ErrorCodeConstants.ACCOUNT_USER_NOT_EXISTS;
|
||||
import static org.hamcrest.Matchers.hasKey;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@ -232,49 +233,66 @@ class AdminAccountUserControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectNonNumericUserIdAsUnifiedBadRequest_when_getUserEntitlements() throws Exception {
|
||||
void should_returnUserNotExists_when_getUserEntitlementsUserIdIsNonNumeric() throws Exception {
|
||||
when(entitlementService.getAdminUserEntitlements(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER))
|
||||
.thenThrow(new ServiceException(ACCOUNT_USER_NOT_EXISTS));
|
||||
|
||||
mockMvc.perform(get("/muse/account/users/external-user/entitlements")
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$.code").value(ACCOUNT_USER_NOT_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$").value(hasKey("data")));
|
||||
|
||||
verifyNoInteractions(entitlementService);
|
||||
verify(entitlementService).getAdminUserEntitlements(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectNonNumericUserIdAsUnifiedBadRequest_when_createQuotaAdjustment() throws Exception {
|
||||
void should_returnUserNotExists_when_createQuotaAdjustmentUserIdIsNonNumeric() throws Exception {
|
||||
when(quotaService.adminCreateQuotaAdjustment(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER), any(), any()))
|
||||
.thenThrow(new ServiceException(ACCOUNT_USER_NOT_EXISTS));
|
||||
|
||||
mockMvc.perform(post("/muse/account/users/external-user/quota-adjustments")
|
||||
.header("X-API-Version", "1")
|
||||
.contentType("application/json")
|
||||
.content(quotaAdjustmentBody()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$.code").value(ACCOUNT_USER_NOT_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$").value(hasKey("data")));
|
||||
|
||||
verifyNoInteractions(quotaService);
|
||||
verify(quotaService).adminCreateQuotaAdjustment(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER),
|
||||
any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectNonNumericUserIdAsUnifiedBadRequest_when_listQuotaAdjustments() throws Exception {
|
||||
void should_returnEmptyPage_when_listQuotaAdjustmentsUserIdIsNonNumeric() throws Exception {
|
||||
when(quotaService.adminListQuotaAdjustments(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER), any(), isNull()))
|
||||
.thenReturn(new AccountPageResult<>(0L, 1, 20, List.of()));
|
||||
|
||||
mockMvc.perform(get("/muse/account/users/external-user/quota-adjustments")
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$").value(hasKey("data")));
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(0))
|
||||
.andExpect(jsonPath("$.data.list").isEmpty());
|
||||
|
||||
verifyNoInteractions(quotaService);
|
||||
verify(quotaService).adminListQuotaAdjustments(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER),
|
||||
any(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectNonNumericUserIdAsUnifiedBadRequest_when_getBalanceSnapshots() throws Exception {
|
||||
void should_returnEmptyPage_when_getBalanceSnapshotsUserIdIsNonNumeric() throws Exception {
|
||||
when(entitlementService.getAdminBalanceSnapshots(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER), any()))
|
||||
.thenReturn(new AccountPageResult<>(0L, 1, 20, List.of()));
|
||||
|
||||
mockMvc.perform(get("/muse/account/users/external-user/balance-snapshots")
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$").value(hasKey("data")));
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(0))
|
||||
.andExpect(jsonPath("$.data.list").isEmpty());
|
||||
|
||||
verifyNoInteractions(entitlementService);
|
||||
verify(entitlementService).getAdminBalanceSnapshots(eq(AccountUserIdResolver.NON_NUMERIC_USER_ID_MARKER),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -389,8 +407,8 @@ class AdminAccountUserControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keepDedicatedUserRouteAheadOfCatchAll_when_contractControllerExists() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AdminMuseAccountContractController())
|
||||
void should_keepDedicatedUserRouteOwnedByAccountController() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
AdminAccountUserSummaryRespVO summary = new AdminAccountUserSummaryRespVO();
|
||||
|
||||
@ -9,7 +9,6 @@ import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountEntitlementService;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountUsageService;
|
||||
import cn.iocoder.muse.module.member.controller.account.vo.AccountPageResult;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppBalanceSnapshotEntryRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppEntitlementRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppUsageSummaryRespVO;
|
||||
@ -190,8 +189,8 @@ class AppAccountEntitlementControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_entitlementRouteExists() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedEntitlementRoute() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import cn.iocoder.muse.framework.security.core.LoginUser;
|
||||
import cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountExportService;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AccountExportTaskCreateReqVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AccountExportTaskDetailRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AccountExportTaskResultRespVO;
|
||||
@ -66,8 +65,8 @@ class AppAccountExportControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_exportRoutesExist() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedExportRoutes() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountMarketRecordService;
|
||||
import cn.iocoder.muse.module.member.controller.account.vo.AccountPageResult;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppLicenseRecordRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppPublishRecordRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppPurchaseRecordRespVO;
|
||||
@ -67,8 +66,8 @@ class AppAccountMarketRecordControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_marketRoutesExist() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedMarketRoutes() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountNewApiBindingService;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountQuotaService;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountUsageService;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppIntegrationCallDetailRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppNewApiBindingRecheckRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppNewApiBindingSummaryRespVO;
|
||||
@ -69,8 +68,8 @@ class AppAccountNewApiControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_bindingRouteExists() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedBindingRoute() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import cn.iocoder.muse.framework.security.core.LoginUser;
|
||||
import cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountProfileService;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppProfileRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.AppProfileUpdateReqVO;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@ -70,8 +69,8 @@ class AppAccountProfileControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keepDedicatedProfileRouteAheadOfCatchAll_when_missingVersion() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedProfileRoute() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
@ -82,8 +81,8 @@ class AppAccountProfileControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keepDedicatedProfileRouteAheadOfCatchAll_when_versionPresent() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_keepDedicatedProfileRouteOwnedByAccountController() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
AppProfileRespVO profile = new AppProfileRespVO();
|
||||
|
||||
@ -8,7 +8,6 @@ import cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.member.application.account.AccountSecurityService;
|
||||
import cn.iocoder.muse.module.member.controller.account.vo.AccountPageResult;
|
||||
import cn.iocoder.muse.module.member.controller.app.AppMuseAccountContractController;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.SecurityEventAcknowledgeResultRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.SecurityEventDetailRespVO;
|
||||
import cn.iocoder.muse.module.member.controller.app.account.vo.SecurityEventSummaryRespVO;
|
||||
@ -65,8 +64,8 @@ class AppAccountSecurityControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_securityRoutesExist() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedSecurityRoutes() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
@ -78,8 +77,8 @@ class AppAccountSecurityControllerTest extends BaseMockitoUnitTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingApiVersion_beforeCatchAll_when_securityDetailAndAckRoutesExist() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller, new AppMuseAccountContractController())
|
||||
void should_rejectMissingApiVersion_onDedicatedSecurityDetailAndAckRoutes() throws Exception {
|
||||
MockMvc routeMockMvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new TestExceptionAdvice())
|
||||
.build();
|
||||
|
||||
|
||||
@ -0,0 +1,259 @@
|
||||
package cn.iocoder.muse.server.framework.api;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* P1R-3 Account 路由归属静态门禁。
|
||||
*
|
||||
* <p>本测试只证明 33 个 Account 合同路由已经由专用 Controller 持有,且源码不再把这些路由交给
|
||||
* {@code MuseContractPersistenceService} 兜底;它不证明 Account 业务语义已经 completed。</p>
|
||||
*/
|
||||
class P1rAccountRouteOwnershipTest {
|
||||
|
||||
/** 旧管理端合同兜底 Controller,Task 11 优先删除。 */
|
||||
private static final String ADMIN_CONTRACT_CONTROLLER = "muse-cloud/muse-module-member/muse-module-member-server/"
|
||||
+ "src/main/java/cn/iocoder/muse/module/member/controller/admin/AdminMuseAccountContractController.java";
|
||||
|
||||
/** 旧用户端合同兜底 Controller,Task 11 优先删除。 */
|
||||
private static final String APP_CONTRACT_CONTROLLER = "muse-cloud/muse-module-member/muse-module-member-server/"
|
||||
+ "src/main/java/cn/iocoder/muse/module/member/controller/app/AppMuseAccountContractController.java";
|
||||
|
||||
/** Account Controller 源码根目录,用于扫描专用 Controller 的 Spring mapping。 */
|
||||
private static final String ACCOUNT_CONTROLLER_ROOT = "muse-cloud/muse-module-member/muse-module-member-server/"
|
||||
+ "src/main/java/cn/iocoder/muse/module/member/controller";
|
||||
|
||||
/** P1R-3 固定的 33 个 Account method + path,来自阶段计划 Task 11。 */
|
||||
private static final Map<String, String> P1R3_ACCOUNT_ROUTES = Map.ofEntries(
|
||||
entry("GET /admin-api/muse/account/users", "adminListAccountUsers"),
|
||||
entry("GET /admin-api/muse/account/users/{userId}/entitlements", "adminGetUserEntitlements"),
|
||||
entry("POST /admin-api/muse/account/users/{userId}/quota-adjustments", "adminCreateQuotaAdjustment"),
|
||||
entry("GET /admin-api/muse/account/users/{userId}/quota-adjustments", "adminListQuotaAdjustments"),
|
||||
entry("GET /admin-api/muse/account/new-api-bindings", "adminListNewApiBindings"),
|
||||
entry("POST /admin-api/muse/account/users/{userId}/new-api-binding", "adminCreateNewApiBinding"),
|
||||
entry("POST /admin-api/muse/account/users/{userId}/quota-requests", "adminCreateQuotaRequest"),
|
||||
entry("GET /admin-api/muse/account/users/{userId}/balance-snapshots", "adminGetBalanceSnapshots"),
|
||||
entry("POST /admin-api/muse/account/call-attribution-jobs", "adminCreateCallAttributionJob"),
|
||||
entry("GET /admin-api/muse/account/call-attribution-jobs/{jobId}", "adminGetCallAttributionJob"),
|
||||
entry("GET /admin-api/muse/account/integration-calls/by-correlation/{correlationId}",
|
||||
"adminGetIntegrationCallByCorrelation"),
|
||||
entry("GET /admin-api/muse/account/usage-records", "adminListUsageRecords"),
|
||||
entry("GET /admin-api/muse/account/purchase-records", "adminListPurchaseRecords"),
|
||||
entry("GET /app-api/muse/me", "appGetCurrentUser"),
|
||||
entry("GET /app-api/muse/profile", "appGetProfile"),
|
||||
entry("PATCH /app-api/muse/profile", "appUpdateProfile"),
|
||||
entry("GET /app-api/muse/account/entitlements", "appGetEntitlements"),
|
||||
entry("GET /app-api/muse/account/usage", "appGetUsage"),
|
||||
entry("GET /app-api/muse/account/new-api-binding", "appGetNewApiBinding"),
|
||||
entry("POST /app-api/muse/account/new-api-binding/recheck", "appRecheckNewApiBinding"),
|
||||
entry("GET /app-api/muse/account/balance-snapshots", "appGetBalanceSnapshots"),
|
||||
entry("POST /app-api/muse/account/quota-requests", "appCreateQuotaRequest"),
|
||||
entry("GET /app-api/muse/account/quota-requests/{requestId}", "appGetQuotaRequest"),
|
||||
entry("GET /app-api/muse/account/integration-calls/by-correlation/{correlationId}",
|
||||
"appGetIntegrationCallByCorrelation"),
|
||||
entry("GET /app-api/muse/account/purchases", "appListPurchases"),
|
||||
entry("GET /app-api/muse/account/licenses", "appListLicenses"),
|
||||
entry("GET /app-api/muse/account/publish-records", "appListPublishRecords"),
|
||||
entry("GET /app-api/muse/account/security-events", "appListSecurityEvents"),
|
||||
entry("GET /app-api/muse/account/security-events/{eventId}", "appGetSecurityEvent"),
|
||||
entry("POST /app-api/muse/account/security-events/{eventId}/acknowledge", "appAcknowledgeSecurityEvent"),
|
||||
entry("POST /app-api/muse/account/export-tasks", "appCreateExportTask"),
|
||||
entry("GET /app-api/muse/account/export-tasks/{taskId}", "appGetExportTask"),
|
||||
entry("GET /app-api/muse/account/downloads/{credentialId}", "appDownloadExport")
|
||||
);
|
||||
|
||||
/** 用源码扫描覆盖常见 Spring mapping 注解,避免启动完整 Spring 上下文。 */
|
||||
private static final Pattern METHOD_MAPPING_PATTERN = Pattern.compile(
|
||||
"@(GetMapping|PostMapping|PatchMapping|PutMapping|DeleteMapping|RequestMapping)\\s*\\((.*?)\\)",
|
||||
Pattern.DOTALL);
|
||||
|
||||
/** 解析 RequestMapping(method = RequestMethod.GET) 形式时使用。 */
|
||||
private static final Pattern REQUEST_METHOD_PATTERN = Pattern.compile("RequestMethod\\.([A-Z]+)");
|
||||
|
||||
@Test
|
||||
void should_retire_old_account_contract_fallback_controllers() throws IOException {
|
||||
assertRetiredOrNarrowed(findRepositoryRoot().resolve(ADMIN_CONTRACT_CONTROLLER));
|
||||
assertRetiredOrNarrowed(findRepositoryRoot().resolve(APP_CONTRACT_CONTROLLER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_all_p1r3_account_routes_owned_by_dedicated_controllers() throws IOException {
|
||||
Map<String, Path> actualRoutes = collectAccountRoutes();
|
||||
|
||||
assertEquals(33, P1R3_ACCOUNT_ROUTES.size(), "P1R-3 Account 路由清单必须保持 33 个");
|
||||
for (String route : P1R3_ACCOUNT_ROUTES.keySet()) {
|
||||
assertTrue(actualRoutes.containsKey(route),
|
||||
P1R3_ACCOUNT_ROUTES.get(route) + " 必须由 Account 专用 Controller 持有: " + route);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_allow_p1r3_account_routes_to_use_contract_persistence() throws IOException {
|
||||
Map<String, Path> actualRoutes = collectAccountRoutes();
|
||||
|
||||
for (String route : P1R3_ACCOUNT_ROUTES.keySet()) {
|
||||
Path owner = actualRoutes.get(route);
|
||||
assertTrue(owner != null, "P1R-3 Account 路由缺少专用 Controller: " + route);
|
||||
assertNoContractPersistence(owner, route);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_keep_account_wildcard_or_app_profile_contract_fallback() throws IOException {
|
||||
Path controllerRoot = findRepositoryRoot().resolve(ACCOUNT_CONTROLLER_ROOT);
|
||||
try (Stream<Path> stream = Files.walk(controllerRoot)) {
|
||||
for (Path sourceFile : stream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().contains("Account"))
|
||||
.filter(path -> path.getFileName().toString().endsWith("Controller.java"))
|
||||
.toList()) {
|
||||
assertNoRetiredFallbackTokens(sourceFile, Files.readString(sourceFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertRetiredOrNarrowed(Path controllerPath) throws IOException {
|
||||
if (!Files.exists(controllerPath)) {
|
||||
return;
|
||||
}
|
||||
String source = Files.readString(controllerPath);
|
||||
assertNoContractPersistence(controllerPath, "旧 Account 合同兜底 Controller");
|
||||
assertNoRetiredFallbackTokens(controllerPath, source);
|
||||
}
|
||||
|
||||
private static Map<String, Path> collectAccountRoutes() throws IOException {
|
||||
Map<String, Path> routes = new TreeMap<>();
|
||||
Path controllerRoot = findRepositoryRoot().resolve(ACCOUNT_CONTROLLER_ROOT);
|
||||
try (Stream<Path> stream = Files.walk(controllerRoot)) {
|
||||
for (Path sourceFile : stream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().contains("Account"))
|
||||
.filter(path -> path.getFileName().toString().endsWith("Controller.java"))
|
||||
.toList()) {
|
||||
collectAccountRoutesFromFile(sourceFile, routes);
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static void collectAccountRoutesFromFile(Path sourceFile, Map<String, Path> routes) throws IOException {
|
||||
String source = Files.readString(sourceFile);
|
||||
String sidePrefix = sourceFile.toString().contains("/controller/admin/") ? "/admin-api" : "/app-api";
|
||||
String classBasePath = extractClassBasePath(source);
|
||||
|
||||
Matcher matcher = METHOD_MAPPING_PATTERN.matcher(source);
|
||||
while (matcher.find()) {
|
||||
String annotation = matcher.group(1);
|
||||
String mappingBody = matcher.group(2);
|
||||
for (String method : extractHttpMethods(annotation, mappingBody)) {
|
||||
for (String methodPath : extractMappingPaths(mappingBody)) {
|
||||
String fullPath = normalizePath(sidePrefix, classBasePath, methodPath);
|
||||
if (isP1r3AccountPath(fullPath)) {
|
||||
routes.put(method + " " + fullPath, sourceFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractClassBasePath(String source) {
|
||||
int classIndex = source.indexOf("public class ");
|
||||
String classAnnotations = classIndex >= 0 ? source.substring(0, classIndex) : source;
|
||||
Matcher matcher = Pattern.compile("@RequestMapping\\s*\\((.*?)\\)", Pattern.DOTALL).matcher(classAnnotations);
|
||||
if (!matcher.find()) {
|
||||
return "";
|
||||
}
|
||||
return extractMappingPaths(matcher.group(1)).stream().findFirst().orElse("");
|
||||
}
|
||||
|
||||
private static Set<String> extractHttpMethods(String annotation, String mappingBody) {
|
||||
return switch (annotation) {
|
||||
case "GetMapping" -> Set.of("GET");
|
||||
case "PostMapping" -> Set.of("POST");
|
||||
case "PatchMapping" -> Set.of("PATCH");
|
||||
case "PutMapping" -> Set.of("PUT");
|
||||
case "DeleteMapping" -> Set.of("DELETE");
|
||||
default -> {
|
||||
Matcher matcher = REQUEST_METHOD_PATTERN.matcher(mappingBody);
|
||||
Map<String, Boolean> methods = new LinkedHashMap<>();
|
||||
while (matcher.find()) {
|
||||
methods.put(matcher.group(1), true);
|
||||
}
|
||||
yield methods.isEmpty() ? Set.of() : methods.keySet();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Set<String> extractMappingPaths(String mappingBody) {
|
||||
Matcher matcher = Pattern.compile("\"([^\"]+)\"").matcher(mappingBody);
|
||||
Map<String, Boolean> paths = new LinkedHashMap<>();
|
||||
while (matcher.find()) {
|
||||
String value = matcher.group(1);
|
||||
if (value.startsWith("/")) {
|
||||
paths.put(value, true);
|
||||
}
|
||||
}
|
||||
return paths.keySet();
|
||||
}
|
||||
|
||||
private static String normalizePath(String sidePrefix, String classBasePath, String methodPath) {
|
||||
return Stream.of(sidePrefix, classBasePath, methodPath)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(part -> !part.isBlank())
|
||||
.map(part -> part.startsWith("/") ? part.substring(1) : part)
|
||||
.reduce("", (left, right) -> left + "/" + right)
|
||||
.replaceAll("/{2,}", "/");
|
||||
}
|
||||
|
||||
private static boolean isP1r3AccountPath(String fullPath) {
|
||||
return fullPath.startsWith("/admin-api/muse/account/")
|
||||
|| fullPath.equals("/app-api/muse/me")
|
||||
|| fullPath.equals("/app-api/muse/profile")
|
||||
|| fullPath.startsWith("/app-api/muse/account/");
|
||||
}
|
||||
|
||||
private static void assertNoContractPersistence(Path sourceFile, String context) throws IOException {
|
||||
String source = Files.readString(sourceFile);
|
||||
assertFalse(source.contains("MuseContractPersistenceService"),
|
||||
context + " 不能依赖 MuseContractPersistenceService: " + sourceFile);
|
||||
assertFalse(source.contains("contractPersistenceService.handle"),
|
||||
context + " 不能调用 contractPersistenceService.handle: " + sourceFile);
|
||||
}
|
||||
|
||||
private static void assertNoRetiredFallbackTokens(Path sourceFile, String source) {
|
||||
assertFalse(source.contains("\"/account/**\""),
|
||||
"Account Controller 不能保留 /account/** 兜底: " + sourceFile);
|
||||
assertFalse(source.contains("\"/me\", \"/profile\", \"/account/**\""),
|
||||
"App Account Controller 不能保留 /me、/profile、/account/** 组合兜底: " + sourceFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前 Maven 执行目录逐级向上查找仓库根目录,避免 surefire 在不同模块目录执行时路径失效。
|
||||
*/
|
||||
private static Path findRepositoryRoot() {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
for (Path candidate = current; candidate != null; candidate = candidate.getParent()) {
|
||||
if (Files.exists(candidate.resolve("muse-cloud/muse-server/pom.xml"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user