404 lines
20 KiB
Java
404 lines
20 KiB
Java
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();
|
|
}
|
|
|
|
@Test
|
|
void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception {
|
|
String marker = "privacy-leak-" + LocalDateTime.now().toString();
|
|
String orderId = createRandomOrder(marker);
|
|
|
|
// Access via the generic Clerk Order Detail endpoint (which the notification likely links to)
|
|
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
|
|
.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());
|
|
|
|
String nickname = data.path("customNickname").asText();
|
|
assertThat(nickname.equals("匿名用户") || "å¿åç¨æ·".equals(nickname)).isTrue();
|
|
assertThat(data.path("customAvatar").asText()).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
void queryByIdForAcceptedRandomOrderOwnedByCurrentClerkRetainsCustomerInfoAndStatus() throws Exception {
|
|
String marker = "random-owned-" + LocalDateTime.now();
|
|
String orderId = createRandomOrder(marker);
|
|
|
|
// Mark the random order as accepted by the current test clerk
|
|
ensureTenantContext();
|
|
playOrderInfoService.lambdaUpdate()
|
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
|
.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
|
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
|
|
.update();
|
|
|
|
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
|
|
.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("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
|
assertThat(data.path("acceptBy").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
|
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode());
|
|
// For the owning clerk, customer info should not be forced to anonymous
|
|
String nickname = data.path("customNickname").asText();
|
|
assertThat(nickname).isNotEmpty();
|
|
assertThat(nickname).isNotIn("匿名用户", "å¿åç¨æ·");
|
|
assertThat(data.path("weiChatCode").asText())
|
|
.withFailMessage("Owner clerk should see customer wechat for accepted random order")
|
|
.isNotEmpty();
|
|
}
|
|
|
|
@Test
|
|
void queryByIdForRandomOrderAcceptedByAnotherClerkHidesCustomerInfoAndOrderStatus() throws Exception {
|
|
String marker = "random-other-clerk-" + 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/clerk/order/queryById")
|
|
.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("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
|
assertThat(data.path("acceptBy").asText()).isEqualTo(OTHER_CLERK_ID);
|
|
// Order status must be masked for random orders owned by another clerk
|
|
assertThat(data.path("orderStatus").asText()).isEmpty();
|
|
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();
|
|
}
|
|
|
|
@Test
|
|
void queryByIdForNonRandomOrderAcceptedByAnotherClerkDoesNotMaskStatus() throws Exception {
|
|
String marker = "specified-other-clerk-" + LocalDateTime.now();
|
|
String orderId = createRandomOrder(marker);
|
|
|
|
// Re-tag the order as a specified order to ensure non-random logic
|
|
ensureTenantContext();
|
|
playOrderInfoService.lambdaUpdate()
|
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
|
.set(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
|
|
.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID)
|
|
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
|
|
.update();
|
|
|
|
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
|
|
.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("placeType").asText()).isEqualTo(OrderConstant.PlaceType.SPECIFIED.getCode());
|
|
// Status should remain the real value for non-random orders
|
|
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode());
|
|
// We still expect customer info to not be forcibly anonymized by the random-order privacy rules
|
|
String nickname = data.path("customNickname").asText();
|
|
assertThat(nickname).isNotIn("匿名用户", "å¿åç¨æ·");
|
|
}
|
|
|
|
@Test
|
|
void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception {
|
|
String marker = "privacy-list-" + LocalDateTime.now().toString();
|
|
String orderId = createRandomOrder(marker);
|
|
|
|
// Ensure the created pending random order appears in the clerk's own order list
|
|
ensureTenantContext();
|
|
playOrderInfoService.lambdaUpdate()
|
|
.eq(PlayOrderInfoEntity::getId, orderId)
|
|
.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
|
.update();
|
|
|
|
ObjectNode payload = mapper.createObjectNode();
|
|
payload.put("pageNum", 1);
|
|
payload.put("pageSize", 10);
|
|
payload.put("orderStatus", OrderConstant.OrderStatus.PENDING.getCode());
|
|
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
|
|
|
|
MvcResult result = mockMvc.perform(post("/wx/clerk/order/queryByPage")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.header(USER_HEADER, DEFAULT_USER)
|
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
|
.content(payload.toString()))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.code").value(200))
|
|
.andReturn();
|
|
|
|
JsonNode root = mapper.readTree(result.getResponse().getContentAsString());
|
|
JsonNode data = root.path("data");
|
|
JsonNode records = data.isArray() ? data : data.path("records");
|
|
assertThat(records.isArray()).isTrue();
|
|
assertThat(records.size()).isGreaterThan(0);
|
|
|
|
boolean found = false;
|
|
for (JsonNode node : records) {
|
|
assertThat(node.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
|
assertThat(node.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
|
|
if (orderId.equals(node.path("id").asText())) {
|
|
found = true;
|
|
String nickname = node.path("customNickname").asText();
|
|
assertThat(nickname.equals("匿名用户") || "å¿åç¨æ·".equals(nickname)).isTrue();
|
|
assertThat(node.path("customAvatar").asText()).isEmpty();
|
|
assertThat(node.path("customId").asText()).isEmpty();
|
|
}
|
|
}
|
|
|
|
assertThat(found).as("Pending random order should appear in clerk list response").isTrue();
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|