|
|
@@ -0,0 +1,445 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.admin.AdminSupplierController;
|
|
|
+import com.etotem.cfc.controller.admin.EcomSupplierController;
|
|
|
+import com.etotem.cfc.controller.admin.SupplyManageController;
|
|
|
+import com.etotem.cfc.controller.admin.SupplierProductController;
|
|
|
+import com.etotem.cfc.dto.ProductCreateRequest;
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
+import com.etotem.cfc.entity.SupplySystem;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.ProductService;
|
|
|
+import com.etotem.cfc.service.RealNameBackfillService;
|
|
|
+import com.etotem.cfc.service.SupplySystemService;
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
+
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:管理端供应商管理(能力键 {@code admin.supplier.manage},TDD-RED 阶段)。
|
|
|
+ *
|
|
|
+ * <p><b>需求/设计溯源</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>场景:{@code tests/user-scenarios/应用场景总览.md} SC-A-06(供应商管理)、
|
|
|
+ * SC-S-01(入驻申请)、SC-S-02(商品管理上架/编辑);</li>
|
|
|
+ * <li>用例:{@code tests/test-cases/vendor.md} TC-S-01~TC-S-02-E;</li>
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 + §4.60.6 + §4.62.1 + §4.62.2;</li>
|
|
|
+ * <li>任务登记:sc-key-map.md「SC-A-06 / SC-S-01 / SC-S-02 → admin.supplier.manage → S-cfc-200」。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>被校验的契约行为</b></p>
|
|
|
+ * <ol>
|
|
|
+ * <li><b>正常流(结构锚点)</b>:
|
|
|
+ * {@code AdminSupplierController} /api/admin/supplier(list/save/update/delete)、
|
|
|
+ * {@code EcomSupplierController} /api/admin/ecom-supplier(list/all/save/update/delete)、
|
|
|
+ * {@code SupplyManageController} /api/admin/supply-manage(list/detail/create/update/delete)
|
|
|
+ * 均应暴露约定 POST 端点。</li>
|
|
|
+ * <li><b>异常流 / 边界流(TC-S-01-E / SC-A-06 / SC-S-02-E)</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>AdminSupplierController / EcomSupplierController 当前均为 <b>410 废弃壳</b>
|
|
|
+ * (「该接口已废弃,请使用 /api/admin/supply-system/*」),
|
|
|
+ * 契约要求此路径下的 CRUD 提供真实功能而非 410;</li>
|
|
|
+ * <li>SupplyManageController.delete 缺乏「有在售商品则拒绝删除/停用」门禁(SC-A-06 边界:
|
|
|
+ * 有在售商品的供应商停用需先下架);</li>
|
|
|
+ * <li>SupplyManageController.create 对无完整资质的申请仍自动设为
|
|
|
+ * {@code vendorStatus=approved},违反 SC-S-01 的「提交资质 → 平台审核 → 入驻成功」流转
|
|
|
+ * (资质不全应驳回,不得自动通过);</li>
|
|
|
+ * <li>SupplierProductController.create 未拦截负价格/负库存(TC-S-02-E),产品经
|
|
|
+ * {@code ProductService.adminCreate} 直接以 {@code on_shelf} 状态落库。</li>
|
|
|
+ * </ul></li>
|
|
|
+ * <li><b>权限流</b>:三个控制器方法均未带 {@code @RequestAttribute("role")} 守卫(对标
|
|
|
+ * {@code AdminAuditController}/{@code AdminArticleController} 模式),非 admin 无法 403 拒绝
|
|
|
+ * (TC-S-01-P / SC-A-06-P)。</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:结构锚点用例通过(绿,基线);异常/边界/权限用例断言失败(红),
|
|
|
+ * 断言原文清晰标注契约所需的「非 410/CURD 落地/业务门禁/role 守卫」缺失。
|
|
|
+ * GREEN 阶段实现后本测试自动转绿。
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务。</p>
|
|
|
+ *
|
|
|
+ * @see <a href="https://git.iwintrue.com/cfc/superpowers/-/wikis/tdd/contract-tests">TDD 契约测试规范</a>
|
|
|
+ */
|
|
|
+@SuppressWarnings("unchecked")
|
|
|
+class SupplierManageAdminContractTest extends EnergyContractTestBase {
|
|
|
+
|
|
|
+ // ==================== 被校验对象 ====================
|
|
|
+
|
|
|
+ private SupplyManageController supplyManageController;
|
|
|
+ @Mock
|
|
|
+ private UserMapper userMapper;
|
|
|
+ @Mock
|
|
|
+ private UserService userService;
|
|
|
+ @Mock
|
|
|
+ private RealNameBackfillService realNameBackfillService;
|
|
|
+
|
|
|
+ private AdminSupplierController adminSupplierController;
|
|
|
+ private EcomSupplierController ecomSupplierController;
|
|
|
+
|
|
|
+ private SupplierProductController supplierProductController;
|
|
|
+ @Mock
|
|
|
+ private SupplySystemService supplySystemService;
|
|
|
+ @Mock
|
|
|
+ private ProductService productService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ // MyBatis-Plus TableInfo 预注册:使 LambdaQueryWrapper 在无 Spring 上下文中可解析列名
|
|
|
+ initTable(User.class);
|
|
|
+
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+
|
|
|
+ // --- SupplyManageController ---
|
|
|
+ supplyManageController = new SupplyManageController();
|
|
|
+ inject(supplyManageController, "userMapper", userMapper);
|
|
|
+ inject(supplyManageController, "userService", userService);
|
|
|
+ inject(supplyManageController, "realNameBackfillService", realNameBackfillService);
|
|
|
+
|
|
|
+ // --- AdminSupplierController / EcomSupplierController ---
|
|
|
+ adminSupplierController = new AdminSupplierController();
|
|
|
+ ecomSupplierController = new EcomSupplierController();
|
|
|
+
|
|
|
+ // --- SupplierProductController ---
|
|
|
+ supplierProductController = new SupplierProductController();
|
|
|
+ inject(supplierProductController, "supplySystemService", supplySystemService);
|
|
|
+ inject(supplierProductController, "productService", productService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 正常流(结构锚点,SC-A-06 + SC-S-01) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 正常流(结构锚点):SupplyManageController 暴露 list/detail/create/update/delete")
|
|
|
+ void supplyManage_exposesRequiredEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(SupplyManageController.class, "/list"),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/list");
|
|
|
+ assertTrue(hasEndpoint(SupplyManageController.class, "/detail"),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/detail");
|
|
|
+ assertTrue(hasEndpoint(SupplyManageController.class, "/create"),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/create");
|
|
|
+ assertTrue(hasEndpoint(SupplyManageController.class, "/update"),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/update");
|
|
|
+ assertTrue(hasEndpoint(SupplyManageController.class, "/delete"),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/delete");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 正常流(结构锚点):AdminSupplierController 暴露 list/save/update/delete")
|
|
|
+ void adminSupplier_exposesRequiredEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(AdminSupplierController.class, "/list"),
|
|
|
+ "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/list");
|
|
|
+ assertTrue(hasEndpoint(AdminSupplierController.class, "/save"),
|
|
|
+ "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/save");
|
|
|
+ assertTrue(hasEndpoint(AdminSupplierController.class, "/update"),
|
|
|
+ "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/update");
|
|
|
+ assertTrue(hasEndpoint(AdminSupplierController.class, "/delete"),
|
|
|
+ "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/delete");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 正常流(结构锚点):EcomSupplierController 暴露 list/all/save/update/delete")
|
|
|
+ void ecomSupplier_exposesRequiredEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(EcomSupplierController.class, "/list"),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/list");
|
|
|
+ assertTrue(hasEndpoint(EcomSupplierController.class, "/all"),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/all");
|
|
|
+ assertTrue(hasEndpoint(EcomSupplierController.class, "/save"),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/save");
|
|
|
+ assertTrue(hasEndpoint(EcomSupplierController.class, "/update"),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/update");
|
|
|
+ assertTrue(hasEndpoint(EcomSupplierController.class, "/delete"),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/delete");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(SC-A-06):AdminSupplierController 不应是 410 废弃壳 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 异常:AdminSupplierController.list 应提供真实功能,非 410 废弃壳")
|
|
|
+ void adminSupplier_list_notDeprecated410() {
|
|
|
+ Result<?> r = adminSupplierController.list(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/supplier/list 应提供真实功能(SC-A-06);"
|
|
|
+ + "当前返回 code=" + r.getCode()
|
|
|
+ + "(410 废弃壳)。契约要求实现真实 CRUD 而非仅返回 410 提示转向 supply-system。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 异常:AdminSupplierController.save 应提供真实功能,非 410 废弃壳")
|
|
|
+ void adminSupplier_save_notDeprecated410() {
|
|
|
+ Result<?> r = adminSupplierController.save(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/supplier/save 应提供真实功能(SC-A-06);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 异常:AdminSupplierController.update 应提供真实功能,非 410 废弃壳")
|
|
|
+ void adminSupplier_update_notDeprecated410() {
|
|
|
+ Result<?> r = adminSupplierController.update(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/supplier/update 应提供真实功能(SC-A-06);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 异常:AdminSupplierController.delete 应提供真实功能,非 410 废弃壳")
|
|
|
+ void adminSupplier_delete_notDeprecated410() {
|
|
|
+ Result<?> r = adminSupplierController.delete(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/supplier/delete 应提供真实功能(SC-A-06);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(SC-S-01):EcomSupplierController 不应是 410 废弃壳 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 异常:EcomSupplierController.list 应提供真实功能,非 410 废弃壳")
|
|
|
+ void ecomSupplier_list_notDeprecated410() {
|
|
|
+ Result<?> r = ecomSupplierController.list(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/ecom-supplier/list 应提供真实功能(SC-S-01 入驻申请载体);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 异常:EcomSupplierController.all 应提供真实功能,非 410 废弃壳")
|
|
|
+ void ecomSupplier_all_notDeprecated410() {
|
|
|
+ Result<?> r = ecomSupplierController.all();
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/ecom-supplier/all 应提供真实功能(SC-S-01 下拉选择载体);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 异常:EcomSupplierController.save 应提供真实功能,非 410 废弃壳(入驻申请提交端口)")
|
|
|
+ void ecomSupplier_save_notDeprecated410() {
|
|
|
+ // 入驻申请核心写入口;空参即可触达契约门(契约关注端点不被 410 阻塞)
|
|
|
+ Result<?> r = ecomSupplierController.save(null);
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/ecom-supplier/save 应提供真实功能(SC-S-01 入驻申请提交);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。"
|
|
|
+ + "TC-S-01 将此处登记为入驻申请的正式写入端点。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 异常:EcomSupplierController.update 应提供真实功能,非 410 废弃壳")
|
|
|
+ void ecomSupplier_update_notDeprecated410() {
|
|
|
+ Result<?> r = ecomSupplierController.update(null);
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/ecom-supplier/update 应提供真实功能(SC-S-01);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 异常:EcomSupplierController.delete 应提供真实功能,非 410 废弃壳")
|
|
|
+ void ecomSupplier_delete_notDeprecated410() {
|
|
|
+ Result<?> r = ecomSupplierController.delete(Collections.emptyMap());
|
|
|
+ assertNotNull(r);
|
|
|
+ assertFalse(isDeprecated410(r),
|
|
|
+ "RED[admin.supplier.manage] /api/admin/ecom-supplier/delete 应提供真实功能(SC-S-01);"
|
|
|
+ + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流(SC-A-06):有在售商品的供应商停用/删除需先下架 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-06 边界:SupplyManageController.delete 对有在售商品的供应商应拒绝(当前未做前置校验)")
|
|
|
+ void supplyManage_delete_onSaleSupplier_rejected() {
|
|
|
+ // 模拟「有在售商品的供应商」:vendorType=product_supplier 且 vendorStatus=approved
|
|
|
+ User supplier = new User();
|
|
|
+ supplier.setId(1L);
|
|
|
+ supplier.setPhone("13800001001");
|
|
|
+ supplier.setVendorType("product_supplier");
|
|
|
+ supplier.setVendorStatus("approved");
|
|
|
+
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(supplier);
|
|
|
+ when(userMapper.deleteById(1L)).thenReturn(1);
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("id", 1L);
|
|
|
+ Result<Boolean> result = supplyManageController.delete(body);
|
|
|
+
|
|
|
+ // 契约要求:有在售商品的供应商删除前应先下架,否则应拒收;当前实现无条件 deleteById
|
|
|
+ assertTrue(result.getCode() != 200,
|
|
|
+ "RED[admin.supplier.manage] 有在售商品的供应商 delete 应被拒绝(SC-A-06 边界:"
|
|
|
+ + "有在售商品的供应商停用需先下架);当前实现无条件执行 deleteById 返回 code="
|
|
|
+ + result.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流(SC-S-01):入驻申请资质不全应驳回,不得自动通过 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-01 边界:SupplyManageController.create 无资质材料不应自动 vendorStatus=approved")
|
|
|
+ void supplyManage_create_notAutoApprove() {
|
|
|
+ // 手机号未占用
|
|
|
+ when(userMapper.selectOne(any())).thenReturn(null);
|
|
|
+ when(userMapper.insert(any())).thenAnswer(inv -> {
|
|
|
+ Object u = inv.getArgument(0);
|
|
|
+ ((User) u).setId(999L);
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("phone", "13800002002");
|
|
|
+ Result<User> result = supplyManageController.create(body);
|
|
|
+
|
|
|
+ assertNotNull(result.getData(),
|
|
|
+ "RED[admin.supplier.manage] create 应返回创建后的用户实体;当前 data=" + result.getData());
|
|
|
+ assertFalse("approved".equals(result.getData().getVendorStatus()),
|
|
|
+ "RED[admin.supplier.manage] 未经验核的入驻申请应保持待审状态(如 pending),"
|
|
|
+ + "不得 vendorStatus=approved(SC-S-01:提交资质 → 平台审核 → 入驻成功);"
|
|
|
+ + "当前实现直接 approve,违背审核流契约。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流(SC-S-02):负价格/负库存拦截 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-02 边界:SupplierProductController.create 负库存应 400 拦截,不落库")
|
|
|
+ void supplierProduct_create_negativeStock_rejected() {
|
|
|
+ stubSupplierProductContext(makeSystem(1L, "体系A"));
|
|
|
+
|
|
|
+ Product p = new Product();
|
|
|
+ p.setName("负库存测试商品");
|
|
|
+ p.setStock(-5);
|
|
|
+ p.setPrice(100);
|
|
|
+ when(productService.adminCreate(any(), any())).thenReturn(Result.success(null));
|
|
|
+
|
|
|
+ ProductCreateRequest req = new ProductCreateRequest();
|
|
|
+ req.setProduct(p);
|
|
|
+
|
|
|
+ Result<?> result = supplierProductController.create(req, 1L);
|
|
|
+
|
|
|
+ assertTrue(result.getCode() == 400 || result.getCode() < 200,
|
|
|
+ "RED[admin.supplier.manage] 负库存商品 (-5) 创建应 400 拦截(TC-S-02-E),"
|
|
|
+ + "当前透传至 adminCreate 直接落库(code=" + result.getCode() + ");"
|
|
|
+ + "字段合法性校验应在 Controller 层前置拦截。");
|
|
|
+ verify(productService, never()).adminCreate(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-S-02 边界:SupplierProductController.create 负价格应 400 拦截,不落库")
|
|
|
+ void supplierProduct_create_negativePrice_rejected() {
|
|
|
+ stubSupplierProductContext(makeSystem(1L, "体系A"));
|
|
|
+
|
|
|
+ Product p = new Product();
|
|
|
+ p.setName("负价格测试商品");
|
|
|
+ p.setStock(10);
|
|
|
+ p.setPrice(-100);
|
|
|
+ when(productService.adminCreate(any(), any())).thenReturn(Result.success(null));
|
|
|
+
|
|
|
+ ProductCreateRequest req = new ProductCreateRequest();
|
|
|
+ req.setProduct(p);
|
|
|
+
|
|
|
+ Result<?> result = supplierProductController.create(req, 1L);
|
|
|
+
|
|
|
+ assertTrue(result.getCode() == 400 || result.getCode() < 200,
|
|
|
+ "RED[admin.supplier.manage] 负价格商品 (-100 分) 创建应 400 拦截(TC-S-02-E),"
|
|
|
+ + "当前透传至 adminCreate 直接落库(code=" + result.getCode() + ");"
|
|
|
+ + "字段合法性校验应在 Controller 层前置拦截。");
|
|
|
+ verify(productService, never()).adminCreate(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 权限流(TC-S-01-P / SC-A-06-P):admin 角色守卫 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-S-01-P/SC-A-06-P 权限:AdminSupplierController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void adminSupplier_hasRoleGuard() {
|
|
|
+ assertTrue(hasRoleGuard(AdminSupplierController.class),
|
|
|
+ "RED[admin.supplier.manage] AdminSupplierController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-A-06 管理员接口约束);"
|
|
|
+ + "当前方法均无 role 参数(对标 AdminAuditController/AdminArticleController 模式)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-S-01-P/SC-A-06-P 权限:EcomSupplierController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void ecomSupplier_hasRoleGuard() {
|
|
|
+ assertTrue(hasRoleGuard(EcomSupplierController.class),
|
|
|
+ "RED[admin.supplier.manage] EcomSupplierController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-S-01 / SC-A-06 管理员接口约束);"
|
|
|
+ + "当前方法均无 role 参数。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-S-01-P/SC-A-06-P 权限:SupplyManageController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void supplyManage_hasRoleGuard() {
|
|
|
+ assertTrue(hasRoleGuard(SupplyManageController.class),
|
|
|
+ "RED[admin.supplier.manage] SupplyManageController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-A-06 管理员接口约束);"
|
|
|
+ + "当前方法均无 role 参数(对标 AdminAuditController/AdminArticleController 模式)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
+
|
|
|
+ private void stubSupplierProductContext(SupplySystem system) {
|
|
|
+ when(supplySystemService.getByAdminId(any())).thenReturn(system);
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupplySystem makeSystem(Long id, String name) {
|
|
|
+ SupplySystem s = new SupplySystem();
|
|
|
+ s.setId(id);
|
|
|
+ s.setName(name);
|
|
|
+ return s;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 反射辅助 ====================
|
|
|
+
|
|
|
+ /** 控制器是否暴露指定 POST 路径端点 */
|
|
|
+ private boolean hasEndpoint(Class<?> controller, String path) {
|
|
|
+ for (java.lang.reflect.Method m : controller.getDeclaredMethods()) {
|
|
|
+ PostMapping pm = m.getAnnotation(PostMapping.class);
|
|
|
+ if (pm != null) {
|
|
|
+ for (String v : pm.value()) {
|
|
|
+ if (path.equals(v)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 控制器是否含 @RequestAttribute("role") 参数守卫 */
|
|
|
+ private boolean hasRoleGuard(Class<?> controller) {
|
|
|
+ for (java.lang.reflect.Method m : controller.getDeclaredMethods()) {
|
|
|
+ for (java.lang.reflect.Parameter p : m.getParameters()) {
|
|
|
+ RequestAttribute ra = p.getAnnotation(RequestAttribute.class);
|
|
|
+ if (ra != null && "role".equals(ra.value())) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 结果是否为 410 废弃码 */
|
|
|
+ private boolean isDeprecated410(Result<?> r) {
|
|
|
+ return r != null && Integer.valueOf(410).equals(r.getCode());
|
|
|
+ }
|
|
|
+}
|