package com.starry.admin.api; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.reset; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.common.task.OverdueOrderHandlerTask; import com.starry.admin.modules.order.module.constant.OrderConstant; import com.starry.admin.modules.order.module.entity.PlayOrderContinueInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.service.IPlayOrderContinueInfoService; import com.starry.admin.modules.weichat.service.NotificationSender; import com.starry.admin.modules.weichat.service.WxCustomMpService; import com.starry.common.constant.Constants; import com.starry.common.context.CustomSecurityContextHolder; import java.time.LocalDateTime; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { private static final String MESSAGE_OPERATION_SUCCESS = "操作成功"; private static final String REVIEW_PENDING_STATE = "0"; private static final String EXCLUDE_HISTORY_DISABLED = "0"; private static final String OTHER_CLERK_ID = "clerk-apitest-other"; private static final int SINGLE_QUANTITY = 1; @MockBean private NotificationSender notificationSender; @MockBean private WxCustomMpService wxCustomMpService; @MockBean private OverdueOrderHandlerTask overdueOrderHandlerTask; @Autowired private IPlayOrderContinueInfoService orderContinueInfoService; private final ObjectMapper mapper = new ObjectMapper(); private String customerToken; private String clerkToken; @BeforeEach void setUpTokens() { ensureTenantContext(); customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken); clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID); clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken); // Relax notifications to avoid strict verification noise doNothing().when(notificationSender).sendOrderMessageAsync(Mockito.any()); doNothing().when(notificationSender).sendOrderFinishMessageAsync(Mockito.any()); doNothing().when(wxCustomMpService).sendCreateOrderMessageBatch( anyList(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString()); doNothing().when(overdueOrderHandlerTask).enqueue(Mockito.anyString()); } @AfterEach void tearDown() { ensureTenantContext(); orderContinueInfoService.lambdaUpdate() .eq(PlayOrderContinueInfoEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID) .remove(); reset(notificationSender, wxCustomMpService, overdueOrderHandlerTask); CustomSecurityContextHolder.remove(); } @Test void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception { String marker = "privacy-" + LocalDateTime.now().toString(); String orderId = createRandomOrder(marker); MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") .param("id", orderId) .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andReturn(); JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data"); assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode()); assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode()); } @Test void duplicateContinuationRequestIsRejected() throws Exception { String marker = "continue-" + LocalDateTime.now().toString(); String orderId = createRandomOrder(marker); acceptOrder(orderId); ArrayNode images = mapper.createArrayNode().add("https://example.com/proof.png"); ObjectNode payload = mapper.createObjectNode() .put("orderId", orderId) .put("remark", "加场申请") .set("images", images); mockMvc.perform(post("/wx/order/clerk/continue") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken) .contentType(MediaType.APPLICATION_JSON) .content(payload.toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value("下单成功")) .andExpect(jsonPath("$.message").value(MESSAGE_OPERATION_SUCCESS)); mockMvc.perform(post("/wx/order/clerk/continue") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken) .contentType(MediaType.APPLICATION_JSON) .content(payload.toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(500)) .andExpect(jsonPath("$.message").value("同一场订单只能续单一次")); ensureTenantContext(); PlayOrderContinueInfoEntity continuation = orderContinueInfoService.lambdaQuery() .eq(PlayOrderContinueInfoEntity::getOrderId, orderId) .last("limit 1") .one(); assertThat(continuation).isNotNull(); assertThat(continuation.getReviewedState()).isEqualTo(REVIEW_PENDING_STATE); } @Test void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception { String marker = "privacy-accepted-" + LocalDateTime.now(); String orderId = createRandomOrder(marker); ensureTenantContext(); playOrderInfoService.lambdaUpdate() .eq(PlayOrderInfoEntity::getId, orderId) .set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID) .set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode()) .update(); MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") .param("id", orderId) .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andReturn(); JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data"); assertThat(data.path("acceptBy").asText()).isEqualTo(OTHER_CLERK_ID); assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("customId").asText()).isEmpty(); String nickname = data.path("customNickname").asText(); assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue(); assertThat(data.path("customAvatar").asText()).isEmpty(); } private String createRandomOrder(String remark) throws Exception { ensureTenantContext(); resetCustomerBalance(); ObjectNode payload = mapper.createObjectNode(); payload.put("sex", OrderConstant.Gender.FEMALE.getCode()); payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID); payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID); payload.put("commodityQuantity", SINGLE_QUANTITY); payload.put("weiChatCode", "apitest-customer-wx"); payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED); payload.set("couponIds", mapper.createArrayNode()); payload.put("remark", remark); mockMvc.perform(post("/wx/custom/order/random") .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken) .contentType(MediaType.APPLICATION_JSON) .content(payload.toString())) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); ensureTenantContext(); PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery() .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) .eq(PlayOrderInfoEntity::getRemark, remark) .orderByDesc(PlayOrderInfoEntity::getCreatedTime) .last("limit 1") .one(); assertThat(order).isNotNull(); return order.getId(); } private void acceptOrder(String orderId) throws Exception { mockMvc.perform(get("/wx/clerk/order/accept") .param("id", orderId) .header(USER_HEADER, DEFAULT_USER) .header(TENANT_HEADER, DEFAULT_TENANT) .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } }