package com.xuekairui.gateway; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * 体验配额配置接口测试 * 覆盖:用户端获取活跃配置、运营端CRUD管理、权限校验 */ @DisplayName("体验配额配置接口测试") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class TrialQuotaConfigTest extends GatewayBaseTest { private static final ObjectMapper mapper = new ObjectMapper(); /** 动态捕获的配置 ID,避免依赖 auto_increment 顺序 */ private static Long createdConfigId; private static Long activityConfigId; private static Long deleteConfigId; private String adminToken() { return generateAccessToken(100L, "13900000000", "ADMIN"); } // ==================== 用户端:获取活跃配置 ==================== @Test @Order(1) @DisplayName("GET /api/trial-quota/active - 获取活跃体验配额配置(公开接口)") void getActiveConfig_shouldReturnConfig() throws Exception { mockMvc.perform(get("/api/trial-quota/active")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").isNumber()); } // ==================== 运营端:查询配置列表 ==================== @Test @Order(10) @DisplayName("GET /api/admin/trial-quota/configs - 查询配置列表") void listConfigs_shouldReturnList() throws Exception { mockMvc.perform(get("/api/admin/trial-quota/configs") .header("Authorization", "Bearer " + adminToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isArray()); } @Test @Order(11) @DisplayName("GET /api/admin/trial-quota/configs - 无Token应返回401") void listConfigs_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(get("/api/admin/trial-quota/configs")) .andExpect(status().isUnauthorized()); } // ==================== 运营端:创建配置 ==================== @Test @Order(20) @DisplayName("POST /api/admin/trial-quota/config - 创建体验配额配置") void createConfig_shouldSucceed() throws Exception { String body = """ { "configName": "测试体验配置", "trialDays": 15, "dailyQueryLimit": 30, "monthlyWatchlistLimit": 200, "enabled": true, "configType": "DEFAULT", "applicableUserType": "NEW_USER", "priority": 1, "description": "自动化测试创建的配置" } """; MvcResult result = mockMvc.perform(post("/api/admin/trial-quota/config") .header("Authorization", "Bearer " + adminToken()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.id").isNumber()) .andExpect(jsonPath("$.data.configName").value("测试体验配置")) .andExpect(jsonPath("$.data.trialDays").value(15)) .andExpect(jsonPath("$.data.dailyQueryLimit").value(30)) .andReturn(); createdConfigId = mapper.readTree(result.getResponse().getContentAsString()) .get("data").get("id").asLong(); } @Test @Order(21) @DisplayName("POST /api/admin/trial-quota/config - 创建活动类型配置") void createConfig_activityType_shouldSucceed() throws Exception { String body = """ { "configName": "618活动体验配置", "trialDays": 30, "dailyQueryLimit": 50, "monthlyWatchlistLimit": 500, "enabled": true, "configType": "ACTIVITY", "applicableUserType": "ALL", "priority": 10, "maxUsers": 1000, "description": "618活动专属体验配置" } """; MvcResult result = mockMvc.perform(post("/api/admin/trial-quota/config") .header("Authorization", "Bearer " + adminToken()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.configName").value("618活动体验配置")) .andExpect(jsonPath("$.data.configType").value("ACTIVITY")) .andExpect(jsonPath("$.data.maxUsers").value(1000)) .andReturn(); activityConfigId = mapper.readTree(result.getResponse().getContentAsString()) .get("data").get("id").asLong(); } @Test @Order(22) @DisplayName("POST /api/admin/trial-quota/config - 无Token应返回401") void createConfig_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(post("/api/admin/trial-quota/config") .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isUnauthorized()); } // ==================== 运营端:更新配置 ==================== @Test @Order(30) @DisplayName("PUT /api/admin/trial-quota/config/{id} - 更新配置") void updateConfig_shouldSucceed() throws Exception { String body = """ { "configName": "测试体验配置(已更新)", "trialDays": 20, "dailyQueryLimit": 40, "monthlyWatchlistLimit": 300, "enabled": true, "configType": "DEFAULT", "applicableUserType": "NEW_USER", "priority": 2, "description": "更新后的配置" } """; mockMvc.perform(put("/api/admin/trial-quota/config/" + createdConfigId) .header("Authorization", "Bearer " + adminToken()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.configName").value("测试体验配置(已更新)")) .andExpect(jsonPath("$.data.trialDays").value(20)); } @Test @Order(31) @DisplayName("PUT /api/admin/trial-quota/config/{id} - 更新不存在的配置应返回404") void updateConfig_nonExistent_shouldReturn404() throws Exception { String body = """ { "configName": "不存在的配置", "trialDays": 15, "dailyQueryLimit": 20, "monthlyWatchlistLimit": 100, "enabled": true, "configType": "DEFAULT", "applicableUserType": "NEW_USER" } """; mockMvc.perform(put("/api/admin/trial-quota/config/9999") .header("Authorization", "Bearer " + adminToken()) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(404)); } @Test @Order(32) @DisplayName("PUT /api/admin/trial-quota/config/{id} - 无Token应返回401") void updateConfig_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(put("/api/admin/trial-quota/config/" + createdConfigId) .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isUnauthorized()); } // ==================== 运营端:删除配置 ==================== @Test @Order(40) @DisplayName("DELETE /api/admin/trial-quota/config/{id} - 删除配置") void deleteConfig_shouldSucceed() throws Exception { // 先创建一个新配置用来删除 String createBody = """ { "configName": "待删除配置", "trialDays": 7, "dailyQueryLimit": 10, "monthlyWatchlistLimit": 50, "enabled": true, "configType": "COUPON", "applicableUserType": "NEW_USER", "priority": 99, "description": "此配置将被删除" } """; MvcResult createResult = mockMvc.perform(post("/api/admin/trial-quota/config") .header("Authorization", "Bearer " + adminToken()) .contentType(MediaType.APPLICATION_JSON) .content(createBody)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").isNumber()) .andReturn(); deleteConfigId = mapper.readTree(createResult.getResponse().getContentAsString()) .get("data").get("id").asLong(); // 删除该配置 mockMvc.perform(delete("/api/admin/trial-quota/config/" + deleteConfigId) .header("Authorization", "Bearer " + adminToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test @Order(41) @DisplayName("DELETE /api/admin/trial-quota/config/{id} - 删除不存在的配置应返回404") void deleteConfig_nonExistent_shouldReturn404() throws Exception { mockMvc.perform(delete("/api/admin/trial-quota/config/9999") .header("Authorization", "Bearer " + adminToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(404)); } @Test @Order(42) @DisplayName("DELETE /api/admin/trial-quota/config/{id} - 无Token应返回401") void deleteConfig_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(delete("/api/admin/trial-quota/config/" + createdConfigId)) .andExpect(status().isUnauthorized()); } // ==================== 边界场景 ==================== @Test @Order(50) @DisplayName("GET /api/trial-quota/active - 无Token也可访问(公开接口)") void getActiveConfig_noAuth_shouldBeAccessible() throws Exception { mockMvc.perform(get("/api/trial-quota/active")) .andExpect(status().isOk()); } }