package com.ruoyi.app.mendian; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.system.domain.PosFood; import com.ruoyi.system.service.IPosFoodService; import com.ruoyi.system.service.MerchantStoreAccessService; import com.ruoyi.system.utils.Auth; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.bind.annotation.PutMapping; import java.lang.reflect.Method; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** 平台审核独立于商家 token/门店归属,只更新审核状态,不能误走商品保存或规格处理。 */ class PosFoodControllerAuditTest { private IPosFoodService foods; private MerchantStoreAccessService merchantAccess; private MockMvc mvc; private MockedStatic messages; @BeforeEach void setup() { messages = mockStatic(MessageUtils.class); messages.when(() -> MessageUtils.message(anyString())).thenAnswer(call -> call.getArgument(0)); PosFoodController controller = new PosFoodController(); foods = mock(IPosFoodService.class); merchantAccess = mock(MerchantStoreAccessService.class); ReflectionTestUtils.setField(controller, "posFoodService", foods); ReflectionTestUtils.setField(controller, "merchantStoreAccessService", merchantAccess); mvc = MockMvcBuilders.standaloneSetup(controller).build(); when(foods.updatePosFood(any(PosFood.class))).thenReturn(1); } @AfterEach void cleanup() { messages.close(); } @Test void platformAuditUsesExistingEditPermissionAndNeverMerchantAuth() { Method audit = Arrays.stream(PosFoodController.class.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(PutMapping.class) && Arrays.asList(method.getAnnotation(PutMapping.class).value()).contains("/audit")) .findFirst().orElse(null); assertNotNull(audit, "platform audit endpoint must exist"); assertNotNull(audit.getAnnotation(PreAuthorize.class)); assertEquals("@ss.hasPermi('chanting:food:edit')", audit.getAnnotation(PreAuthorize.class).value()); assertNull(audit.getAnnotation(Anonymous.class)); assertNull(audit.getAnnotation(Auth.class)); } @Test void approveAndRevokeWorkWithoutMerchantTokenHeader() throws Exception { for (String state : new String[] {"1", "0"}) { mvc.perform(put("/chanting/food/audit").header("Authorization", "Bearer platform-test") .contentType(MediaType.APPLICATION_JSON).content("{\"id\":9137,\"toExamine\":\"" + state + "\"}")) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); } ArgumentCaptor update = ArgumentCaptor.forClass(PosFood.class); verify(foods, times(2)).updatePosFood(update.capture()); assertEquals("1", update.getAllValues().get(0).getToExamine()); assertEquals("0", update.getAllValues().get(1).getToExamine()); verifyNoInteractions(merchantAccess); verify(foods, never()).saveOrUpdate(any(PosFood.class)); } @Test void auditIgnoresInjectedContentAndNeverChangesStoreOrSpecs() throws Exception { mvc.perform(put("/chanting/food/audit").contentType(MediaType.APPLICATION_JSON) .content("{\"id\":9137,\"toExamine\":\"1\",\"mdid\":999,\"price\":1,\"name\":\"injected\",\"stackingUp\":\"0\",\"foodSpecs\":[]}")) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); ArgumentCaptor update = ArgumentCaptor.forClass(PosFood.class); verify(foods).updatePosFood(update.capture()); PosFood changed = update.getValue(); assertEquals(9137L, changed.getId()); assertEquals("1", changed.getToExamine()); assertNull(changed.getMdid()); assertNull(changed.getPrice()); assertNull(changed.getName()); assertNull(changed.getStackingUp()); assertNull(changed.getFoodSpecs()); assertNull(changed.getRecommend()); } @Test void invalidIdOrAuditStateIsRejectedBeforeWrite() throws Exception { for (String payload : new String[] {"{}", "{\"id\":0,\"toExamine\":\"1\"}", "{\"id\":-1,\"toExamine\":\"1\"}", "{\"id\":9137}", "{\"id\":9137,\"toExamine\":\"2\"}", "{\"id\":9137,\"toExamine\":\"\"}"}) { mvc.perform(put("/chanting/food/audit").contentType(MediaType.APPLICATION_JSON).content(payload)) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(500)) .andExpect(jsonPath("$.msg").value("food.audit.invalid")); } verifyNoInteractions(foods); } @Test void missingFoodDoesNotCreateProductOrReportSuccess() throws Exception { when(foods.updatePosFood(any(PosFood.class))).thenReturn(0); mvc.perform(put("/chanting/food/audit").contentType(MediaType.APPLICATION_JSON) .content("{\"id\":9137,\"toExamine\":\"1\"}")) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(500)); verify(foods, never()).saveOrUpdate(any(PosFood.class)); } @Test void originalMerchantEndpointStillRequiresMerchantToken() throws Exception { mvc.perform(put("/chanting/food/changerecommend").header("Authorization", "Bearer platform-test") .contentType(MediaType.APPLICATION_JSON).content("{\"id\":9137,\"toExamine\":\"1\"}")) .andExpect(status().isBadRequest()); verifyNoInteractions(foods, merchantAccess); } }