Răsfoiți Sursa

test(tdd): S-cfc-200 admin.supplier.manage 契约级单测(TDD-RED:AdminSupplier/EcomSupplier 410废弃桩 + SC-A-06停用在售商品守卫未落地,预期10红6绿)

能力键 admin.supplier.manage(SC-A-06 供应商管理 + SC-S-01 入驻申请)。
设计载体三 Controller:AdminSupplier/EcomSupplier/SupplyManageController。
- 10 红:AdminSupplierController + EcomSupplierController 为 410 废弃桩(list/save/update/delete + all),
  SC-A-06 边界「有在售商品的供应商停用需先下架」全库无在售商品守卫。
- 6 绿证:SupplyManageController 已落地(入驻手机号校验/默认 approved+supplier_admin/detail 不存在)。
纯反射+Mockito,无 Spring/DB/外部服务;不改动 src/main。GREEN 阶段据契约签名落地或重映射活跃载体。
Agnes Agent 2 ore în urmă
părinte
comite
806ebf4aad

+ 365 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminSupplierManageContractTest.java

@@ -0,0 +1,365 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.RealNameBackfillService;
+import com.etotem.cfc.service.UserService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测(TDD-RED,S-cfc-200):能力键 {@code admin.supplier.manage}
+ * (平台运营·供应商管理 SC-A-06 + 商品服务商入驻申请 SC-S-01)。
+ *
+ * <p><b>需求/设计溯源</b>:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} <b>SC-A-06</b>(供应商管理:列表/审核/停用,
+ *       边界「有在售商品的供应商停用需先下架」)、<b>SC-S-01</b>(入驻申请:提交资质→平台审核→入驻成功,
+ *       边界「资质不全驳回;重复申请幂等」)、<b>SC-S-02</b>(商品管理上架/编辑)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} <b>§4.13</b> / <b>§4.60.6</b>——
+ *       <b>AdminSupplierController</b> {@code /api/admin/supplier}(list/save/update/delete)、
+ *       <b>EcomSupplierController</b> {@code /api/admin/ecom-supplier}(list/all/save/update/delete)、
+ *       <b>SupplyManageController</b> {@code /api/admin/supply-manage}(list/detail/create/update/delete)。</li>
+ *   <li>映射:{@code docs/tdd/orchestration/sc-key-map.md}——SC-A-06 / SC-S-01 → {@code admin.supplier.manage}
+ *       (S-cfc-200;AdminSupplier/EcomSupplier/SupplyManageController)。</li>
+ * </ul>
+ * SC-S-02(商品管理上架/编辑)已由合入主链的 {@code VendorProductManageScS02ContractTest}(ProductService 层)
+ * 覆盖,本类不重复,仅登记映射。</p>
+ *
+ * <p><b>实现现状(src/main 逐类核实)</b>:
+ * <ul>
+ *   <li>{@code AdminSupplierController} / {@code EcomSupplierController}:<b>410 废弃桩</b>——两个设计载体
+ *       仅返回 {@code Result.error(410, "该接口已废弃,请使用 /api/admin/supply-system/...")},
+ *       契约所需的供应商 CRUD 行为<b>未落地</b>(红)。API_REFERENCE §4.61 载体置换提示:活跃载体为
+ *       {@code SupplySystemController}/{@code SupplySystemService}。</li>
+ *   <li>{@code SupplyManageController}:已落地(list/detail/create/update/delete + 入驻校验/角色/层级),
+ *       为<b>功能载体</b>——本测试对其已实现行为保留绿证断言(实现现状证据)。</li>
+ *   <li>SC-A-06 边界「有在售商品的供应商停用需先下架」:<b>全库未实现</b>——{@code SupplySystemService.toggleStatus}
+ *       直接翻转 active/disabled,{@code SupplyManageController.update} 直接置 vendorStatus,二者均<b>无在售商品守卫</b>
+ *       (红)。</li>
+ * </ul></p>
+ *
+ * <p><b>测试风格</b>:与同批 RED 契约测试({@code TeamProfitAdminManageContractTest} 等)一致——纯反射/Mockito
+ * 锚定契约宿主,无 Spring 上下文 / 无 DB / 无外部服务。契约所需行为缺失处断言失败(红);已落地行为保留绿证断言。</p>
+ *
+ * <p><b>契约方法签名(GREEN 阶段据此落地)</b>:
+ * <ul>
+ *   <li>{@code AdminSupplierController /api/admin/supplier}:list/save/update/delete 须提供功能化供应商管理(返回 200,
+ *       非 410 废弃桩);</li>
+ *   <li>{@code EcomSupplierController /api/admin/ecom-supplier}:list/all/save/update/delete 同上;</li>
+ *   <li>SC-A-06 停用边界:停用(vendorStatus→disabled / toggle-status)前须校验供应商在售商品(on_shelf),
+ *       存在则拒绝并提示「请先下架在售商品」。</li>
+ * </ul></p>
+ */
+class AdminSupplierManageContractTest {
+
+    /** 设计载体:管理端供应商主体管理(§4.13 / §4.60.6,SC-A-06) */
+    private static final String ADMIN_SUPPLIER_CTRL =
+            "com.etotem.cfc.controller.admin.AdminSupplierController";
+
+    /** 设计载体:管理端电商供应商(§4.13 / §4.60.6,SC-A-06/SC-S-01) */
+    private static final String ECOM_SUPPLIER_CTRL =
+            "com.etotem.cfc.controller.admin.EcomSupplierController";
+
+    /** 设计载体:管理端供应商管理(§4.60.6,SC-A-06/SC-S-01)——已落地功能载体 */
+    private static final String SUPPLY_MANAGE_CTRL =
+            "com.etotem.cfc.controller.admin.SupplyManageController";
+
+    /** 活跃载体(API_REFERENCE §4.61 载体置换提示):供应体系服务 */
+    private static final String SUPPLY_SYSTEM_SERVICE =
+            "com.etotem.cfc.service.SupplySystemService";
+
+    // ==================== 契约宿主锚定(三设计载体均应存在) ====================
+
+    @Test
+    @DisplayName("锚定-1:AdminSupplierController(/api/admin/supplier)应存在(SC-A-06)")
+    void anchor_adminSupplierControllerExists() {
+        assertTrue(loadClass(ADMIN_SUPPLIER_CTRL) != null,
+                "[admin.supplier.manage] AdminSupplierController 不存在:契约要求 /api/admin/supplier 提供"
+                        + " list/save/update/delete 供应商主体管理(SC-A-06)。");
+    }
+
+    @Test
+    @DisplayName("锚定-2:EcomSupplierController(/api/admin/ecom-supplier)应存在(SC-A-06/SC-S-01)")
+    void anchor_ecomSupplierControllerExists() {
+        assertTrue(loadClass(ECOM_SUPPLIER_CTRL) != null,
+                "[admin.supplier.manage] EcomSupplierController 不存在:契约要求 /api/admin/ecom-supplier 提供"
+                        + " list/all/save/update/delete 电商供应商管理。");
+    }
+
+    @Test
+    @DisplayName("锚定-3:SupplyManageController(/api/admin/supply-manage)应存在(SC-A-06/SC-S-01)")
+    void anchor_supplyManageControllerExists() {
+        assertTrue(loadClass(SUPPLY_MANAGE_CTRL) != null,
+                "[admin.supplier.manage] SupplyManageController 不存在:契约要求 /api/admin/supply-manage 提供"
+                        + " list/detail/create/update/delete 供应商管理。");
+    }
+
+    // ==================== SC-A-06:AdminSupplierController 应提供功能化供应商 CRUD(红:当前为 410 废弃桩) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R1:AdminSupplierController.list 应返回功能化供应商列表(200),当前为 410 废弃桩")
+    void adminSupplierList_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "list", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.list 契约要求返回分页供应商列表(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage()
+                        + "(410 废弃桩,SC-A-06 供应商列表未在设计载体落地)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R2:AdminSupplierController.save 应创建供应商(200),当前为 410 废弃桩")
+    void adminSupplierSave_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "save", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.save 契约要求创建供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R3:AdminSupplierController.update 应更新供应商(200),当前为 410 废弃桩")
+    void adminSupplierUpdate_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "update", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.update 契约要求更新供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R4:AdminSupplierController.delete 应删除供应商(200),当前为 410 废弃桩")
+    void adminSupplierDelete_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "delete", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.delete 契约要求删除供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    // ==================== SC-A-06/SC-S-01:EcomSupplierController 应提供功能化供应商 CRUD(红:当前为 410 废弃桩) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R5:EcomSupplierController.list 应返回功能化供应商列表(200),当前为 410 废弃桩")
+    void ecomSupplierList_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "list", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.list 契约要求返回供应商列表(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R6:EcomSupplierController.all 应返回全量供应商下拉(200),当前为 410 废弃桩")
+    void ecomSupplierAll_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "all");
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.all 契约要求返回全量供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01-R1:EcomSupplierController.save 应创建供应商/入驻(200),当前为 410 废弃桩")
+    void ecomSupplierSave_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "save", (Object) new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.save 契约要求创建供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R7:EcomSupplierController.update 应更新供应商(200),当前为 410 废弃桩")
+    void ecomSupplierUpdate_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "update", (Object) new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.update 契约要求更新供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R8:EcomSupplierController.delete 应删除供应商(200),当前为 410 废弃桩")
+    void ecomSupplierDelete_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "delete", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.delete 契约要求删除供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    // ==================== SC-A-06 停用边界:有在售商品的供应商停用需先下架(红:全库未实现守卫) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R9:停用供应商前须校验无在售商品(「有在售商品的供应商停用需先下架」守卫缺失)")
+    void supplierDisable_boundary_guardOnSaleProducts() {
+        // 边界(应用场景总览 SC-A-06):有在售商品的供应商停用需先下架。
+        // 停用入口:SupplyManageController.update(vendorStatus→disabled) 与 SupplySystemService.toggleStatus。
+        // 契约:停用前必须校验供应商在售商品(on_shelf),存在则拒绝并提示「请先下架在售商品」。
+        Object manage = contractHost(SUPPLY_MANAGE_CTRL);
+        boolean manageGuard = hasField(manage.getClass(), "productMapper")
+                || hasContractMethod(manage, "hasOnShelfProducts", Long.class);
+        Object sysSvc = contractHost(SUPPLY_SYSTEM_SERVICE);
+        boolean sysGuard = hasContractMethod(sysSvc, "hasOnShelfProducts", Long.class);
+        assertTrue(manageGuard || sysGuard,
+                "RED[admin.supplier.manage] SC-A-06 边界「有在售商品的供应商停用需先下架」未实现:"
+                        + "停用入口(SupplyManageController.update / SupplySystemService.toggleStatus)直接翻转状态,"
+                        + "无在售商品守卫 —— GREEN 阶段须在停用前校验该供应商 on_shelf 商品,存在则拒绝并提示先下架。");
+    }
+
+    // ==================== SC-S-01 入驻申请:已落地功能载体保留绿证断言(实现现状证据) ====================
+
+    @Test
+    @DisplayName("SC-S-01-1:入驻 create 手机号缺失应驳回(资质校验,功能载体已落地)")
+    void supplyManageCreate_phoneRequired_validation() throws Exception {
+        SupplyManageControllerFixture fx = supplyManageFixture(mock(UserMapper.class));
+        Map<String, Object> params = new HashMap<>();
+        params.put("nickname", "新供应商");
+        Result<?> r = (Result<?>) call(fx.ctrl, "create", params);
+        assertEquals(500, r.getCode(), "入驻申请手机号缺失须驳回(资质不全驳回边界)");
+        assertTrue(r.getMessage() != null && r.getMessage().contains("手机号不能为空"),
+                "入驻申请手机号缺失应提示「手机号不能为空」,实际: " + r.getMessage());
+    }
+
+    @Test
+    @DisplayName("SC-S-01-2:入驻 create 默认审核通过(vendorStatus=approved + supplier_admin 角色,功能载体已落地)")
+    void supplyManageCreate_createsApprovedSupplier() throws Exception {
+        UserMapper userMapper = mock(UserMapper.class);
+        when(userMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+        when(userMapper.insert(any(User.class))).thenAnswer(inv -> {
+            ((User) inv.getArgument(0)).setId(88L);
+            return 1;
+        });
+        SupplyManageControllerFixture fx = supplyManageFixture(userMapper);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("phone", "13800138000");
+        Result<?> r = (Result<?>) call(fx.ctrl, "create", params);
+
+        assertEquals(200, r.getCode(), "入驻 create 应成功建档");
+        User created = (User) r.getData();
+        assertEquals("approved", created.getVendorStatus(), "入驻默认审核通过(SC-S-01:平台审核→入驻成功)");
+        assertEquals("supplier_admin", created.getRole(), "入驻用户应具备供应商管理角色");
+        verify(userMapper).selectOne(any(Wrapper.class));
+        verify(userMapper).insert(any(User.class));
+    }
+
+    @Test
+    @DisplayName("SC-A-06-3:detail 不存在的供应商应返回「供应商不存在」(功能载体已落地)")
+    void supplyManageDetail_supplierNotFound() throws Exception {
+        UserMapper userMapper = mock(UserMapper.class);
+        when(userMapper.selectById(999L)).thenReturn(null);
+        SupplyManageControllerFixture fx = supplyManageFixture(userMapper);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("id", 999L);
+        Result<?> r = (Result<?>) call(fx.ctrl, "detail", params);
+
+        assertEquals(500, r.getCode(), "不存在的供应商须返回错误");
+        assertTrue(r.getMessage() != null && r.getMessage().contains("供应商不存在"),
+                "不存在的供应商应提示「供应商不存在」,实际: " + r.getMessage());
+    }
+
+    // ==================== 反射辅助(锚定契约宿主,避免编译期依赖未落地类) ====================
+
+    private static final class SupplyManageControllerFixture {
+        final Object ctrl;
+        SupplyManageControllerFixture(Object ctrl) { this.ctrl = ctrl; }
+    }
+
+    private SupplyManageControllerFixture supplyManageFixture(UserMapper userMapper) throws Exception {
+        Object ctrl = Class.forName(SUPPLY_MANAGE_CTRL).getDeclaredConstructor().newInstance();
+        inject(ctrl, "userMapper", userMapper);
+        inject(ctrl, "userService", mock(UserService.class));
+        inject(ctrl, "realNameBackfillService", mock(RealNameBackfillService.class));
+        return new SupplyManageControllerFixture(ctrl);
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private boolean hasField(Class<?> clazz, String field) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(field)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasContractMethod(Object host, String method, Class<?>... paramTypes) {
+        for (Method m : host.getClass().getMethods()) {
+            if (m.getName().equals(method) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 实例化契约宿主;宿主缺失即断言失败(红)。 */
+    private Object contractHost(String fqcn) {
+        try {
+            Class<?> clazz = Class.forName(fqcn);
+            return clazz.getDeclaredConstructor().newInstance();
+        } catch (ClassNotFoundException e) {
+            fail("RED[admin.supplier.manage] 契约宿主 " + simpleName(fqcn) + " 未实现(实现未落地):"
+                    + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        } catch (Exception e) {
+            fail("契约宿主 " + fqcn + " 实例化失败:" + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        }
+    }
+
+    private void inject(Object target, String field, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    /** 按方法名 + 参数个数调用(宽松适配基本/包装类型);宿主或方法缺失即断言失败。 */
+    private Object call(Object host, String method, Object... args) {
+        for (Method m : host.getClass().getMethods()) {
+            if (m.getName().equals(method) && m.getParameterCount() == args.length) {
+                try {
+                    return m.invoke(host, args);
+                } catch (Exception e) {
+                    fail("契约方法 " + simpleName(host.getClass().getName()) + "#" + method
+                            + " 调用失败:" + e.getMessage());
+                    throw new IllegalStateException("unreachable");
+                }
+            }
+        }
+        fail("RED[admin.supplier.manage] 契约方法 " + simpleName(host.getClass().getName()) + "#" + method
+                + " 未实现");
+        throw new IllegalStateException("unreachable");
+    }
+
+    private String simpleName(String fqcn) {
+        int idx = fqcn.lastIndexOf('.');
+        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
+    }
+}