Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxCouponControllerApiTest.java
irving 985b35cd90
Some checks failed
Build and Push Backend / docker (push) Has been cancelled
test: add wechat integration test suite
- Add llm/wechat-subsystem-test-matrix.md and tests covering Wx controllers/services\n- Make ApiTestDataSeeder personnel group seeding idempotent for full-suite stability
2026-01-12 18:54:14 -05:00

470 lines
22 KiB
Java

package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
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.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class WxCouponControllerApiTest extends AbstractApiTest {
private static final String COUPON_ID_PREFIX = "apitest-cpn-";
private static final BigDecimal DEFAULT_DISCOUNT_AMOUNT = new BigDecimal("5");
private static final BigDecimal MINIMUM_USAGE_AMOUNT = new BigDecimal("60");
private static final int DEFAULT_MAX_PER_USER = 3;
private static final int DEFAULT_STOCK = 100;
private static final String DISCOUNT_BEARER_SHOP = "0";
private static final String GLOBAL_CLERK_SCOPE = "0";
private static final String CHECK_CONDITION_DISABLED = "0";
private static final String NEW_USER_FLAG_DISABLED = "0";
@Autowired
private IPlayCouponInfoService couponInfoService;
@Autowired
private IPlayCouponDetailsService couponDetailsService;
@Autowired
private WxTokenService wxTokenService;
@Autowired
private IPlayCustomUserInfoService customUserInfoService;
private final ObjectMapper mapper = new ObjectMapper();
private final List<String> couponIds = new ArrayList<>();
private final List<String> couponDetailIds = new ArrayList<>();
private String customerToken;
@BeforeEach
void setUp() {
ensureTenantContext();
couponInfoService.lambdaUpdate()
.eq(PlayCouponInfoEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.like(PlayCouponInfoEntity::getId, COUPON_ID_PREFIX)
.remove();
couponDetailsService.lambdaUpdate()
.eq(PlayCouponDetailsEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
.like(PlayCouponDetailsEntity::getCouponId, COUPON_ID_PREFIX)
.remove();
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
}
@AfterEach
void tearDown() {
ensureTenantContext();
if (!couponDetailIds.isEmpty()) {
couponDetailsService.removeByIds(couponDetailIds);
couponDetailIds.clear();
}
if (!couponIds.isEmpty()) {
couponInfoService.removeByIds(couponIds);
couponIds.clear();
}
CustomSecurityContextHolder.remove();
}
@Test
void obtainCouponReturnsNonEligibilityReasonWhenNonWhitelisted__covers_CP_002() throws Exception {
ensureTenantContext();
String couponId = newCouponId("whitelist");
PlayCouponInfoEntity coupon = createBaseCoupon(couponId);
coupon.setClaimConditionType(CouponClaimConditionType.WHITELIST.code());
coupon.setCustomWhitelist(List.of("other-customer"));
couponInfoService.save(coupon);
couponIds.add(coupon.getId());
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
.param("id", coupon.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.success").value(false))
.andExpect(jsonPath("$.data.msg").value("非指定用户"));
}
@Test
void queryAllSkipsOfflineCouponsAndMarksObtainedOnes__covers_CP_004() throws Exception {
ensureTenantContext();
PlayCouponInfoEntity onlineCoupon = createBaseCoupon(newCouponId("online"));
onlineCoupon.setCustomWhitelist(List.of(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID));
couponInfoService.save(onlineCoupon);
couponIds.add(onlineCoupon.getId());
PlayCouponDetailsEntity obtained = createCouponDetail(onlineCoupon.getId(), CouponUseState.UNUSED);
couponDetailsService.save(obtained);
couponDetailIds.add(obtained.getId());
PlayCouponInfoEntity offlineCoupon = createBaseCoupon(newCouponId("offline"));
offlineCoupon.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode());
couponInfoService.save(offlineCoupon);
couponIds.add(offlineCoupon.getId());
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryAll")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
assertThat(data).isNotNull();
assertThat(data.isArray()).isTrue();
assertThat(data).hasSizeGreaterThanOrEqualTo(1);
boolean containsOnline = false;
for (JsonNode node : data) {
assertThat(node.path("id").asText()).isNotEqualTo(offlineCoupon.getId());
if (onlineCoupon.getId().equals(node.path("id").asText())) {
containsOnline = true;
assertThat(node.path("obtained").asText()).isEqualTo("1");
}
}
assertThat(containsOnline).isTrue();
}
@Test
void queryByOrderFlagsOnlyEligibleCouponsAsAvailable__covers_CP_006() throws Exception {
ensureTenantContext();
PlayCouponInfoEntity eligible = createBaseCoupon(newCouponId("eligible"));
eligible.setUseMinAmount(MINIMUM_USAGE_AMOUNT);
couponInfoService.save(eligible);
couponIds.add(eligible.getId());
PlayCouponInfoEntity ineligible = createBaseCoupon(newCouponId("ineligible"));
ineligible.setPlaceType(List.of(OrderConstant.PlaceType.SPECIFIED.getCode()));
couponInfoService.save(ineligible);
couponIds.add(ineligible.getId());
PlayCouponDetailsEntity eligibleDetail = createCouponDetail(eligible.getId(), CouponUseState.UNUSED);
PlayCouponDetailsEntity ineligibleDetail = createCouponDetail(ineligible.getId(), CouponUseState.UNUSED);
couponDetailsService.save(eligibleDetail);
couponDetailsService.save(ineligibleDetail);
couponDetailIds.add(eligibleDetail.getId());
couponDetailIds.add(ineligibleDetail.getId());
ObjectNode payload = mapper.createObjectNode();
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
payload.put("clerkId", "");
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
payload.put("commodityQuantity", 1);
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryByOrder")
.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))
.andReturn();
ArrayNode data = (ArrayNode) mapper.readTree(result.getResponse().getContentAsString(java.nio.charset.StandardCharsets.UTF_8))
.path("data");
assertThat(data).isNotNull();
assertThat(data.size()).isGreaterThanOrEqualTo(2);
boolean foundEligibleDetail = false;
boolean foundIneligibleDetail = false;
for (JsonNode node : data) {
if (eligibleDetail.getId().equals(node.path("id").asText())) {
foundEligibleDetail = true;
assertThat(node.path("available").asText()).isEqualTo("1");
assertThat(node.path("reasonForUnavailableUse").asText("")).isEmpty();
}
if (ineligibleDetail.getId().equals(node.path("id").asText())) {
foundIneligibleDetail = true;
assertThat(node.path("available").asText()).isEqualTo("0");
assertThat(node.path("reasonForUnavailableUse").asText()).isEqualTo("订单类型不符合");
}
}
assertThat(foundEligibleDetail).isTrue();
assertThat(foundIneligibleDetail).isTrue();
}
@Test
void obtainCouponSucceedsWhenEligible__covers_CP_003() throws Exception {
ensureTenantContext();
PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("success"));
couponInfoService.save(coupon);
couponIds.add(coupon.getId());
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
.param("id", coupon.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.success").value(true))
.andExpect(jsonPath("$.data.msg").value(anyOf(is(""), nullValue())));
ensureTenantContext();
PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
.eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
.eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.orderByDesc(PlayCouponDetailsEntity::getObtainingTime)
.last("limit 1")
.one();
assertThat(detail).isNotNull();
couponDetailIds.add(detail.getId());
}
@Test
void obtainCouponHonorsPerUserLimit__covers_CP_002() throws Exception {
ensureTenantContext();
PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("limit"));
coupon.setClerkObtainedMaxQuantity(1);
couponInfoService.save(coupon);
couponIds.add(coupon.getId());
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
.param("id", coupon.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.success").value(true));
ensureTenantContext();
PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
.eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
.eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.one();
assertThat(detail).isNotNull();
couponDetailIds.add(detail.getId());
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
.param("id", coupon.getId())
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.success").value(false))
.andExpect(jsonPath("$.data.msg").value("优惠券已达到领取上限"));
}
@Test
void obtainCouponRejectsEmptyId__covers_CP_001() throws Exception {
ensureTenantContext();
mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
.param("id", "")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500))
.andExpect(jsonPath("$.message").value("请求参数异常,优惠券ID不能为"));
}
@Test
void queryAllHidesWhitelistCouponFromNonWhitelistCustomer__covers_CP_004() throws Exception {
ensureTenantContext();
String couponId = newCouponId("qall-wl");
PlayCouponInfoEntity coupon = createBaseCoupon(couponId);
coupon.setClaimConditionType(CouponClaimConditionType.WHITELIST.code());
coupon.setCustomWhitelist(List.of("other-customer"));
couponInfoService.save(coupon);
couponIds.add(coupon.getId());
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryAll")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode data = mapper.readTree(result.getResponse().getContentAsString(java.nio.charset.StandardCharsets.UTF_8))
.path("data");
assertThat(data).isNotNull();
assertThat(data.isArray()).isTrue();
for (JsonNode node : data) {
assertThat(node.path("id").asText()).isNotEqualTo(coupon.getId());
}
}
@Test
void queryByOrderRejectsWhenClerkIdAndLevelIdBothEmpty__covers_CP_005() throws Exception {
ensureTenantContext();
ObjectNode payload = mapper.createObjectNode();
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
payload.put("levelId", "");
payload.put("clerkId", "");
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
payload.put("commodityQuantity", 1);
mockMvc.perform(post("/wx/coupon/custom/queryByOrder")
.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(500))
.andExpect(jsonPath("$.message").value("请求参数异常,店员ID不能为空,等级ID不能为空"));
}
@Test
void queryByOrderSwallowsBrokenCouponAndReturnsRemaining__covers_CP_007() throws Exception {
ensureTenantContext();
PlayCouponInfoEntity eligible = createBaseCoupon(newCouponId("swv"));
eligible.setUseMinAmount(BigDecimal.ZERO);
couponInfoService.save(eligible);
couponIds.add(eligible.getId());
PlayCouponDetailsEntity eligibleDetail = createCouponDetail(eligible.getId(), CouponUseState.UNUSED);
couponDetailsService.save(eligibleDetail);
couponDetailIds.add(eligibleDetail.getId());
PlayCouponDetailsEntity brokenDetail = createCouponDetail(newCouponId("missing"), CouponUseState.UNUSED);
couponDetailsService.save(brokenDetail);
couponDetailIds.add(brokenDetail.getId());
ObjectNode payload = mapper.createObjectNode();
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
payload.put("clerkId", "");
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
payload.put("commodityQuantity", 1);
MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryByOrder")
.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))
.andReturn();
JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
assertThat(data.isArray()).isTrue();
boolean foundEligibleDetail = false;
boolean foundBrokenDetail = false;
for (JsonNode node : data) {
String returnedId = node.path("id").asText();
if (eligibleDetail.getId().equals(returnedId)) {
foundEligibleDetail = true;
}
if (brokenDetail.getId().equals(returnedId)) {
foundBrokenDetail = true;
}
}
assertThat(foundEligibleDetail).isTrue();
assertThat(foundBrokenDetail).isFalse();
}
private PlayCouponInfoEntity createBaseCoupon(String id) {
PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
coupon.setId(id);
coupon.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
coupon.setCouponName("测试券-" + id);
coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
coupon.setUseMinAmount(BigDecimal.ZERO);
coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
coupon.setDiscountContent("测试优惠");
coupon.setDiscountAmount(DEFAULT_DISCOUNT_AMOUNT);
coupon.setAttributionDiscounts(DISCOUNT_BEARER_SHOP);
coupon.setPlaceType(List.of(
OrderConstant.PlaceType.RANDOM.getCode(),
OrderConstant.PlaceType.SPECIFIED.getCode()));
coupon.setClerkType(GLOBAL_CLERK_SCOPE);
coupon.setCouponQuantity(DEFAULT_STOCK);
coupon.setIssuedQuantity(0);
coupon.setRemainingQuantity(DEFAULT_STOCK);
coupon.setClerkObtainedMaxQuantity(DEFAULT_MAX_PER_USER);
coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
coupon.setCustomWhitelist(new ArrayList<>());
coupon.setCustomLevelCheckType(CHECK_CONDITION_DISABLED);
coupon.setCustomSexCheckType(CHECK_CONDITION_DISABLED);
coupon.setNewUser(NEW_USER_FLAG_DISABLED);
coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
LocalDateTime now = LocalDateTime.now();
coupon.setProductiveTime(now.minusDays(1));
coupon.setExpirationTime(now.plusDays(30));
coupon.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
coupon.setCreatedTime(toDate(now));
coupon.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
coupon.setUpdatedTime(toDate(now));
return coupon;
}
private PlayCouponDetailsEntity createCouponDetail(String couponId, CouponUseState status) {
PlayCouponDetailsEntity entity = new PlayCouponDetailsEntity();
entity.setId("detail-" + IdUtils.getUuid());
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
entity.setCouponId(couponId);
entity.setCustomNickname("APITester");
entity.setCustomLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
entity.setObtainingChannels(CouponObtainChannel.SELF_SERVICE.getCode());
entity.setUseState(status.getCode());
LocalDateTime now = LocalDateTime.now();
entity.setObtainingTime(now);
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setCreatedTime(toDate(now));
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
entity.setUpdatedTime(toDate(now));
return entity;
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
private String newCouponId(String suffix) {
return COUPON_ID_PREFIX + suffix + "-" + IdUtils.getUuid();
}
private java.util.Date toDate(LocalDateTime value) {
return java.util.Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
}
}