fix: mask random order details for non-owner clerks

This commit is contained in:
irving
2025-12-28 19:31:56 -05:00
parent c29f76c2fc
commit f39b560a05
2 changed files with 166 additions and 25 deletions

View File

@@ -15,7 +15,9 @@ 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.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
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;
@@ -24,9 +26,13 @@ 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 java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -34,11 +40,21 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
private enum RandomOrderOwnership {
UNACCEPTED,
OWNED_BY_CURRENT,
OWNED_BY_OTHER
}
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 String CUSTOMER_AVATAR_URL = "https://example.com/avatar.png";
private static final String ANON_NICKNAME = "匿名用户";
private static final String CUSTOMER_WECHAT_CODE = "apitest-customer-wx";
private static final String PRIVACY_MARKER_PREFIX = "privacy-";
private static final int SINGLE_QUANTITY = 1;
@MockBean
@@ -90,7 +106,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test
void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception {
String marker = "privacy-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
@@ -153,7 +169,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test
void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception {
String marker = "privacy-accepted-" + LocalDateTime.now();
String marker = PRIVACY_MARKER_PREFIX + "accepted-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
ensureTenantContext();
@@ -177,13 +193,88 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
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(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@ParameterizedTest
@MethodSource("randomOrderMaskingCases")
void selectRandomOrderByIdAppliesPrivacyRules(OrderStatus status,
RandomOrderOwnership ownership,
boolean expectCustomerMasked,
boolean expectStatusMasked) throws Exception {
String marker = PRIVACY_MARKER_PREFIX + "matrix-" + status.name() + "-" + ownership.name() + "-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
ensureTenantContext();
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL)
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.update();
updateRandomOrderStatusAndOwner(orderId, status, ownership);
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("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
if (expectCustomerMasked) {
assertMaskedCustomerInfo(data);
} else {
assertVisibleCustomerInfo(data);
}
if (expectStatusMasked) {
assertThat(data.path("orderStatus").asText()).isEmpty();
} else {
assertThat(data.path("orderStatus").asText()).isEqualTo(status.getCode());
}
}
@Test
void selectRandomOrderByIdForCancelledRandomOrderMasksCustomerAvatarWhenNotAccepted() throws Exception {
String marker = PRIVACY_MARKER_PREFIX + "cancelled-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
ensureTenantContext();
customUserInfoService.lambdaUpdate()
.set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL)
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.update();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.CANCELLED.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("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@Test
void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-leak-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + "leak-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Access via the generic Clerk Order Detail endpoint (which the notification likely links to)
@@ -203,7 +294,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@@ -236,7 +327,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
// 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(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
assertThat(data.path("weiChatCode").asText())
.withFailMessage("Owner clerk should see customer wechat for accepted random order")
.isNotEmpty();
@@ -271,7 +362,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
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(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@@ -304,12 +395,12 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
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("匿名用户", "匿名用户");
assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
}
@Test
void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-list-" + LocalDateTime.now().toString();
String marker = PRIVACY_MARKER_PREFIX + "list-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Ensure the created pending random order appears in the clerk's own order list
@@ -348,7 +439,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
if (orderId.equals(node.path("id").asText())) {
found = true;
String nickname = node.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(node.path("customAvatar").asText()).isEmpty();
assertThat(node.path("customId").asText()).isEmpty();
}
@@ -366,7 +457,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
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("weiChatCode", CUSTOMER_WECHAT_CODE);
payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED);
payload.set("couponIds", mapper.createArrayNode());
payload.put("remark", remark);
@@ -391,6 +482,54 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
return order.getId();
}
private void updateRandomOrderStatusAndOwner(String orderId, OrderStatus status, RandomOrderOwnership ownership) {
ensureTenantContext();
var updater = playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getOrderStatus, status.getCode());
if (ownership == RandomOrderOwnership.OWNED_BY_CURRENT) {
updater.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID);
} else if (ownership == RandomOrderOwnership.OWNED_BY_OTHER) {
updater.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID);
}
updater.update();
}
private void assertMaskedCustomerInfo(JsonNode data) {
assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
private void assertVisibleCustomerInfo(JsonNode data) {
assertThat(data.path("weiChatCode").asText()).isNotEmpty();
assertThat(data.path("customId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotEmpty();
assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
assertThat(data.path("customAvatar").asText()).isEqualTo(CUSTOMER_AVATAR_URL);
}
private static Stream<Arguments> randomOrderMaskingCases() {
return Stream.of(
Arguments.of(OrderStatus.PENDING, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.UNACCEPTED, true, false),
Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false),
Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_OTHER, true, true),
Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_OTHER, true, true)
);
}
private void acceptOrder(String orderId) throws Exception {
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)