|
@@ -0,0 +1,272 @@
|
|
|
|
|
+package com.example.position.controller;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.example.position.common.api.Result;
|
|
|
|
|
+import com.example.position.common.enums.ResultCode;
|
|
|
|
|
+import com.example.position.common.exception.BusinessException;
|
|
|
|
|
+import com.example.position.common.exception.GlobalExceptionHandler;
|
|
|
|
|
+import com.example.position.service.PositionApplicationService;
|
|
|
|
|
+import com.example.position.service.dto.PositionResponseDTO;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Nested;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
|
|
+import org.mockito.InjectMocks;
|
|
|
|
|
+import org.mockito.Mock;
|
|
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
|
|
+
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * PositionController 接口测试
|
|
|
|
|
+ * 使用 standaloneSetup 不加载 Spring 上下文,纯 Mock 测试
|
|
|
|
|
+ */
|
|
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
|
|
+@DisplayName("头寸接口测试")
|
|
|
|
|
+class PositionControllerTest {
|
|
|
|
|
+
|
|
|
|
|
+ private MockMvc mockMvc;
|
|
|
|
|
+
|
|
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private PositionApplicationService positionApplicationService;
|
|
|
|
|
+
|
|
|
|
|
+ @InjectMocks
|
|
|
|
|
+ private PositionController positionController;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() {
|
|
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(positionController)
|
|
|
|
|
+ .setControllerAdvice(new GlobalExceptionHandler())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private PositionResponseDTO buildResponse() {
|
|
|
|
|
+ PositionResponseDTO dto = new PositionResponseDTO();
|
|
|
|
|
+ dto.setId(1L);
|
|
|
|
|
+ dto.setPositionCode("POS001");
|
|
|
|
|
+ dto.setPositionName("测试头寸");
|
|
|
|
|
+ dto.setPositionType("TRADING");
|
|
|
|
|
+ dto.setDirection("LONG");
|
|
|
|
|
+ dto.setQuantity(new BigDecimal("1000"));
|
|
|
|
|
+ dto.setPrice(new BigDecimal("12.50"));
|
|
|
|
|
+ dto.setMarketValue(new BigDecimal("12500"));
|
|
|
|
|
+ dto.setAccountCode("ACC001");
|
|
|
|
|
+ dto.setProductCode("PROD001");
|
|
|
|
|
+ dto.setStatus("ACTIVE");
|
|
|
|
|
+ dto.setVersion(0);
|
|
|
|
|
+ dto.setCreateTime(LocalDateTime.of(2026, 8, 10, 12, 0, 0));
|
|
|
|
|
+ dto.setUpdateTime(LocalDateTime.of(2026, 8, 10, 12, 0, 0));
|
|
|
|
|
+ return dto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 创建 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Nested
|
|
|
|
|
+ @DisplayName("POST /api/positions - 创建头寸")
|
|
|
|
|
+ class Create {
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("成功创建 - 返回200和数据")
|
|
|
|
|
+ void shouldReturn200WhenCreate() throws Exception {
|
|
|
|
|
+ String body = """
|
|
|
|
|
+ {
|
|
|
|
|
+ "positionCode": "POS001",
|
|
|
|
|
+ "positionName": "测试头寸",
|
|
|
|
|
+ "positionType": "TRADING",
|
|
|
|
|
+ "direction": "LONG",
|
|
|
|
|
+ "quantity": 1000,
|
|
|
|
|
+ "price": 12.50
|
|
|
|
|
+ }
|
|
|
|
|
+ """;
|
|
|
|
|
+
|
|
|
|
|
+ when(positionApplicationService.createPosition(any()))
|
|
|
|
|
+ .thenReturn(buildResponse());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/positions")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(body))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(true))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.positionCode").value("POS001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.positionName").value("测试头寸"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("创建失败 - 编号重复时返回业务错误")
|
|
|
|
|
+ void shouldReturnErrorWhenDuplicate() throws Exception {
|
|
|
|
|
+ String body = """
|
|
|
|
|
+ { "positionCode": "POS001", "positionName": "重复" }
|
|
|
|
|
+ """;
|
|
|
|
|
+
|
|
|
|
|
+ when(positionApplicationService.createPosition(any()))
|
|
|
|
|
+ .thenThrow(new BusinessException(ResultCode.DATA_DUPLICATED, "头寸编号已存在: POS001"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/positions")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(body))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(ResultCode.DATA_DUPLICATED.getCode()))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(false));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 更新 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Nested
|
|
|
|
|
+ @DisplayName("PUT /api/positions/{id} - 更新头寸")
|
|
|
|
|
+ class Update {
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("成功更新 - 返回200和更新后数据")
|
|
|
|
|
+ void shouldReturn200WhenUpdate() throws Exception {
|
|
|
|
|
+ String body = """
|
|
|
|
|
+ { "positionName": "已修改", "quantity": 2000 }
|
|
|
|
|
+ """;
|
|
|
|
|
+
|
|
|
|
|
+ PositionResponseDTO updated = buildResponse();
|
|
|
|
|
+ updated.setPositionName("已修改");
|
|
|
|
|
+ updated.setQuantity(new BigDecimal("2000"));
|
|
|
|
|
+ updated.setVersion(1);
|
|
|
|
|
+
|
|
|
|
|
+ when(positionApplicationService.updatePosition(eq(1L), any()))
|
|
|
|
|
+ .thenReturn(updated);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(put("/api/positions/1")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(body))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.positionName").value("已修改"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.quantity").value(2000))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.version").value(1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("更新失败 - 头寸不存在时返回错误")
|
|
|
|
|
+ void shouldReturnErrorWhenNotFound() throws Exception {
|
|
|
|
|
+ when(positionApplicationService.updatePosition(eq(99L), any()))
|
|
|
|
|
+ .thenThrow(new BusinessException(ResultCode.DATA_NOT_FOUND, "头寸不存在: id=99"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(put("/api/positions/99")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content("{}"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(ResultCode.DATA_NOT_FOUND.getCode()))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(false));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 删除 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Nested
|
|
|
|
|
+ @DisplayName("DELETE /api/positions/{id} - 删除头寸")
|
|
|
|
|
+ class Delete {
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("成功删除 - 返回200")
|
|
|
|
|
+ void shouldReturn200WhenDelete() throws Exception {
|
|
|
|
|
+ doNothing().when(positionApplicationService).deletePosition(1L);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(delete("/api/positions/1"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(true))
|
|
|
|
|
+ .andExpect(jsonPath("$.data").doesNotExist());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("删除失败 - 头寸不存在时返回错误")
|
|
|
|
|
+ void shouldReturnErrorWhenDeleteNotFound() throws Exception {
|
|
|
|
|
+ doThrow(new BusinessException(ResultCode.DATA_NOT_FOUND, "头寸不存在: id=99"))
|
|
|
|
|
+ .when(positionApplicationService).deletePosition(99L);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(delete("/api/positions/99"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(ResultCode.DATA_NOT_FOUND.getCode()))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(false));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 查询 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Nested
|
|
|
|
|
+ @DisplayName("GET 接口 - 查询头寸")
|
|
|
|
|
+ class Query {
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("根据ID查询 - 返回200和数据")
|
|
|
|
|
+ void shouldReturn200WhenFindById() throws Exception {
|
|
|
|
|
+ when(positionApplicationService.findById(1L)).thenReturn(buildResponse());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get("/api/positions/1"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.id").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.positionCode").value("POS001"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("根据ID查询 - 不存在时返回错误")
|
|
|
|
|
+ void shouldReturnErrorWhenFindByIdNotFound() throws Exception {
|
|
|
|
|
+ when(positionApplicationService.findById(99L))
|
|
|
|
|
+ .thenThrow(new BusinessException(ResultCode.DATA_NOT_FOUND, "头寸不存在"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get("/api/positions/99"))
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(ResultCode.DATA_NOT_FOUND.getCode()))
|
|
|
|
|
+ .andExpect(jsonPath("$.success").value(false));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("根据编号查询 - 返回200和数据")
|
|
|
|
|
+ void shouldReturn200WhenFindByCode() throws Exception {
|
|
|
|
|
+ when(positionApplicationService.findByCode("POS001")).thenReturn(buildResponse());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get("/api/positions/code/POS001"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.positionCode").value("POS001"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("查询全部 - 返回列表")
|
|
|
|
|
+ void shouldReturn200WhenFindAll() throws Exception {
|
|
|
|
|
+ when(positionApplicationService.findAll()).thenReturn(List.of(buildResponse()));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get("/api/positions"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data[0].positionCode").value("POS001"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("分页查询 - 返回分页数据")
|
|
|
|
|
+ void shouldReturn200WhenPageQuery() throws Exception {
|
|
|
|
|
+ Page<PositionResponseDTO> page = new Page<>(1, 10, 1);
|
|
|
|
|
+ page.setRecords(List.of(buildResponse()));
|
|
|
|
|
+
|
|
|
|
|
+ when(positionApplicationService.pageQuery(any())).thenReturn(page);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get("/api/positions/page")
|
|
|
|
|
+ .param("pageNum", "1")
|
|
|
|
|
+ .param("pageSize", "10"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.total").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.records[0].positionCode").value("POS001"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.current").value(1));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|