Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java

330 lines
17 KiB
Java

package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
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.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.oss.service.IOssFileService;
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaOwnerType;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.admin.modules.weichat.entity.PlayClerkUserLoginResponseVo;
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.enums.ClerkReviewState;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MvcResult;
/**
* 覆盖店员端媒资上传与相册审核的关键业务路径(图片/视频 + 删除后不复活)。
*/
class WxClerkMediaControllerApiTest extends AbstractApiTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private WxTokenService wxTokenService;
@Autowired
private IPlayClerkUserInfoService clerkUserInfoService;
@Autowired
private IPlayMediaService mediaService;
@Autowired
private IPlayClerkDataReviewInfoService dataReviewInfoService;
@Autowired
private IPlayClerkMediaAssetService mediaAssetService;
@MockBean
private IOssFileService ossFileService;
@Test
void clerkCanUploadImageMediaAndPersistUrl() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
String ossUrl = "https://oss.mock/apitest/avatar.png";
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(ossUrl);
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
MockMultipartFile file = new MockMultipartFile(
"file",
"avatar.png",
"image/png",
baos.toByteArray());
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.url").value(ossUrl))
.andExpect(jsonPath("$.data.kind").value("image"))
.andExpect(jsonPath("$.data.usage").value("profile"))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
JsonNode data = root.path("data");
String mediaId = data.path("mediaId").asText(null);
assertThat(mediaId).isNotBlank();
ensureTenantContext();
PlayMediaEntity persisted = mediaService.getById(mediaId);
assertThat(persisted).isNotNull();
assertThat(persisted.getOwnerType()).isEqualTo(MediaOwnerType.CLERK);
assertThat(persisted.getOwnerId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
assertThat(persisted.getUrl()).isEqualTo(ossUrl);
}
@Test
void clerkCanUploadVideoMediaAndPersistUrl() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
String videoUrl = "https://oss.mock/apitest/video.mp4";
String coverUrl = "https://oss.mock/apitest/video-cover.jpg";
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(videoUrl, coverUrl);
byte[] videoBytes = Files.readAllBytes(Paths.get("/Volumes/main/code/yunpei/sample_data/sample_video.mp4"));
MockMultipartFile file = new MockMultipartFile(
"file",
"sample_video.mp4",
"video/mp4",
videoBytes);
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.kind").value("video"))
.andExpect(jsonPath("$.data.usage").value("profile"))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
JsonNode data = root.path("data");
String mediaId = data.path("mediaId").asText(null);
assertThat(mediaId).isNotBlank();
ensureTenantContext();
PlayMediaEntity persisted = mediaService.getById(mediaId);
assertThat(persisted).isNotNull();
assertThat(persisted.getKind()).isEqualTo("video");
assertThat(persisted.getUrl()).isEqualTo(videoUrl);
assertThat(persisted.getCoverUrl()).isEqualTo(coverUrl);
}
@Test
void aggressiveAlbumLifecycleWithFourMediaAndDeletionReflectedForClerkAndCustomer() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(
"https://oss.mock/apitest/album-a.png",
"https://oss.mock/apitest/album-b.png",
"https://oss.mock/apitest/album-c.png",
"https://oss.mock/apitest/album-d.png");
String mediaIdA = extractMediaIdFromUpload(buildTinyPng("album-a.png"), clerkToken);
String mediaIdB = extractMediaIdFromUpload(buildTinyPng("album-b.png"), clerkToken);
String mediaIdC = extractMediaIdFromUpload(buildTinyPng("album-c.png"), clerkToken);
String mediaIdD = extractMediaIdFromUpload(buildTinyPng("album-d.png"), clerkToken);
List<String> allMediaIds = List.of(mediaIdA, mediaIdB, mediaIdC, mediaIdD);
submitAlbumUpdate(allMediaIds, clerkToken);
ensureTenantContext();
approveLatestAlbumReview();
List<PlayClerkMediaAssetEntity> assetsAfterFirstApprove = mediaAssetService
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
assertThat(assetsAfterFirstApprove)
.extracting(PlayClerkMediaAssetEntity::getMediaId)
.containsAll(allMediaIds);
List<String> reviewStatesForNewMedia = assetsAfterFirstApprove.stream()
.filter(asset -> allMediaIds.contains(asset.getMediaId()))
.map(PlayClerkMediaAssetEntity::getReviewState)
.collect(Collectors.toList());
assertThat(reviewStatesForNewMedia).containsOnly(ClerkMediaReviewState.APPROVED.getCode());
List<String> clerkVisibleMediaIdsAfterFirst = assetsAfterFirstApprove.stream()
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
.map(PlayClerkMediaAssetEntity::getMediaId)
.collect(Collectors.toList());
assertThat(clerkVisibleMediaIdsAfterFirst).containsAll(allMediaIds);
PlayClerkUserInfoResultVo customerDetailAfterFirst =
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
List<String> customerMediaIdsAfterFirst = customerDetailAfterFirst.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(customerMediaIdsAfterFirst).containsAll(allMediaIds);
List<String> keptMedia = List.of(mediaIdA, mediaIdC);
// 第二次提交:只删除与重新排序,不再生成新的资料审核记录,应直接生效
long reviewCountBeforeSecond = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
ApiTestDataSeeder.DEFAULT_CLERK_ID)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
submitAlbumUpdate(keptMedia, clerkToken);
ensureTenantContext();
long reviewCountAfterSecond = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
ApiTestDataSeeder.DEFAULT_CLERK_ID)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
assertThat(reviewCountAfterSecond)
.as("deleting/reordering album should not create another review record")
.isEqualTo(reviewCountBeforeSecond);
List<PlayClerkMediaAssetEntity> assetsAfterSecondApprove = mediaAssetService
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
PlayClerkMediaAssetEntity assetA = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdA.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity assetC = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdC.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
assertThat(assetA.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
assertThat(assetA.getOrderIndex()).isEqualTo(0);
assertThat(assetC.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
assertThat(assetC.getOrderIndex()).isEqualTo(1);
PlayClerkUserInfoResultVo customerDetailAfterSecond =
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
List<String> customerMediaIdsAfterSecond = customerDetailAfterSecond.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(customerMediaIdsAfterSecond)
.contains(mediaIdA, mediaIdC)
.doesNotContain(mediaIdB, mediaIdD);
List<String> clerkVisibleMediaIdsAfterSecond = assetsAfterSecondApprove.stream()
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
.map(PlayClerkMediaAssetEntity::getMediaId)
.collect(Collectors.toList());
assertThat(clerkVisibleMediaIdsAfterSecond)
.contains(mediaIdA, mediaIdC)
.doesNotContain(mediaIdB, mediaIdD);
}
private MockMultipartFile buildTinyPng(String filename) throws Exception {
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return new MockMultipartFile("file", filename, "image/png", baos.toByteArray());
}
private String extractMediaIdFromUpload(MockMultipartFile file, String clerkToken) throws Exception {
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
return root.path("data").path("mediaId").asText();
}
private void submitAlbumUpdate(List<String> mediaIds, String clerkToken) throws Exception {
ObjectNode payload = objectMapper.createObjectNode();
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
mediaIds.forEach(albumArray::add);
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/wx/clerk/user/updateAlbum")
.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));
}
private void approveLatestAlbumReview() {
List<com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity> reviews = dataReviewInfoService
.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
ApiTestDataSeeder.DEFAULT_CLERK_ID)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.orderByDesc(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getAddTime)
.list();
assertThat(reviews).isNotEmpty();
com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity latest = reviews.get(0);
com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo vo =
new com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo();
vo.setId(latest.getId());
vo.setReviewState(ClerkReviewState.APPROVED);
vo.setReviewCon("ok");
vo.setReviewTime(LocalDateTime.now());
dataReviewInfoService.updateDataReviewState(vo);
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
}