Kaynağa Gözat

feat(controller): 特价活动 Controller + 白名单

Sisyphus Agent 5 gün önce
ebeveyn
işleme
3c5a49e339

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -47,6 +47,7 @@ private static final String[] PUBLIC_PATHS = {
          "/api/auth/internal/",
          "/api/product/list",
          "/api/product/detail",
+         "/api/special-price/info",
          "/api/product/type-list",
          "/api/product/related-products",
          "/api/mini-game/list",

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -110,6 +110,7 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/account-roles",
                 "/api/product/list",
                 "/api/product/detail",
+                "/api/special-price/info",
                 "/api/product/type-list",
                 "/api/product/related-products",
                 "/api/mini-game/list",
@@ -159,6 +160,7 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/account-roles",
                 "/api/product/list",
                 "/api/product/detail",
+                "/api/special-price/info",
                 "/api/product/type-list",
                 "/api/product/related-products",
                 "/api/mini-game/list",

+ 235 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminSpecialPriceController.java

@@ -0,0 +1,235 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.SpecialPriceDeal;
+import com.etotem.cfc.service.SpecialPriceService;
+import com.etotem.cfc.service.WechatService;
+import com.etotem.cfc.util.ParamUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 管理端-限时特价活动管理(扫码闪购)。
+ * 设计见 docs/superpowers/specs/2026-10-20-limited-time-special-price-design.md
+ */
+@RestController
+@RequestMapping("/api/admin/special-price")
+public class AdminSpecialPriceController {
+
+    @Resource
+    private SpecialPriceService specialPriceService;
+
+    @Resource
+    private WechatService wechatService;
+
+    /**
+     * 分页列表。请求体:{page, size, keyword, runStatus}
+     * runStatus: ACTIVE/NOT_STARTED/SOLD_OUT/ENDED(NOW() 推导)或 draft/disabled(存储状态),空=全部。
+     */
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                            @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        int page = intParam(params.get("page"), 1);
+        int size = intParam(params.get("size"), 20);
+        if (page < 1) page = 1;
+        if (size < 1) size = 20;
+        if (size > 100) size = 100;
+        String keyword = params.get("keyword") != null ? params.get("keyword").toString() : null;
+        String runStatus = params.get("runStatus") != null ? params.get("runStatus").toString() : null;
+        return Result.success(specialPriceService.list(page, size, keyword, runStatus));
+    }
+
+    @PostMapping("/create")
+    public Result<?> create(@RequestBody Map<String, Object> params,
+                            @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        try {
+            SpecialPriceDeal deal = new SpecialPriceDeal();
+            deal.setProductId(ParamUtils.getLong(params.get("productId")));
+            deal.setName(params.get("name") != null ? params.get("name").toString() : null);
+            deal.setSpecialPrice(intParam(params.get("specialPrice")));
+            deal.setStartTime(parseDate(params.get("startTime")));
+            deal.setEndTime(parseDate(params.get("endTime")));
+            deal.setTotalQuantity(intParam(params.get("totalQuantity")));
+            deal.setPerUserLimit(intParam(params.get("perUserLimit")));
+            Integer allowCoupon = intParam(params.get("allowCoupon"));
+            deal.setAllowCoupon(allowCoupon != null ? allowCoupon : 0);
+
+            if (deal.getProductId() == null) return Result.error("请选择商品");
+            if (deal.getName() == null || deal.getName().trim().isEmpty()) return Result.error("活动名称不能为空");
+            if (deal.getSpecialPrice() == null) return Result.error("特价不能为空");
+            if (deal.getStartTime() == null || deal.getEndTime() == null) return Result.error("开始/结束时间不能为空");
+            if (deal.getTotalQuantity() == null || deal.getTotalQuantity() <= 0) return Result.error("总供应量必须大于0");
+            if (deal.getPerUserLimit() == null || deal.getPerUserLimit() < 1) return Result.error("每人限购件数不能小于1");
+
+            return Result.success(specialPriceService.create(deal));
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/update")
+    public Result<?> update(@RequestBody Map<String, Object> params,
+                            @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        try {
+            Long id = ParamUtils.getLong(params.get("id"));
+            if (id == null) return Result.error("活动ID不能为空");
+            SpecialPriceDeal deal = new SpecialPriceDeal();
+            deal.setId(id);
+            deal.setProductId(ParamUtils.getLong(params.get("productId")));
+            deal.setName(params.get("name") != null ? params.get("name").toString() : null);
+            deal.setSpecialPrice(intParam(params.get("specialPrice")));
+            deal.setStartTime(parseDate(params.get("startTime")));
+            deal.setEndTime(parseDate(params.get("endTime")));
+            deal.setTotalQuantity(intParam(params.get("totalQuantity")));
+            deal.setPerUserLimit(intParam(params.get("perUserLimit")));
+            deal.setAllowCoupon(intParam(params.get("allowCoupon")));
+
+            if (deal.getProductId() == null) return Result.error("请选择商品");
+            if (deal.getName() == null || deal.getName().trim().isEmpty()) return Result.error("活动名称不能为空");
+            if (deal.getSpecialPrice() == null) return Result.error("特价不能为空");
+            if (deal.getTotalQuantity() == null || deal.getTotalQuantity() <= 0) return Result.error("总供应量必须大于0");
+            if (deal.getPerUserLimit() == null || deal.getPerUserLimit() < 1) return Result.error("每人限购件数不能小于1");
+
+            specialPriceService.update(deal);
+            return Result.success("修改成功", null);
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 启用/停用。请求体:{id, action: enable|disable}
+     */
+    @PostMapping("/status")
+    public Result<Void> status(@RequestBody Map<String, Object> params,
+                               @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        Long id = ParamUtils.getLong(params.get("id"));
+        String action = params.get("action") != null ? params.get("action").toString() : null;
+        if (id == null) return Result.error("活动ID不能为空");
+        if (!"enable".equals(action) && !"disable".equals(action)) {
+            return Result.error("action 必须为 enable/disable");
+        }
+        try {
+            specialPriceService.updateStatus(id, "enable".equals(action) ? "active" : "disabled");
+            return Result.success("操作成功", null);
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 删除(仅无特价订单时允许)。请求体:{id}
+     */
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestBody Map<String, Object> params,
+                               @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("活动ID不能为空");
+        try {
+            specialPriceService.delete(id);
+            return Result.success("删除成功", null);
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 生成小程序码。请求体:{id}
+     * scene = "id={productId}&sp={dealId}"(≤32 字符),落地页 product-detail。
+     */
+    @PostMapping("/qrcode")
+    public Result<Map<String, Object>> qrcode(@RequestBody Map<String, Object> params,
+                                              @RequestAttribute(value = "role", required = false) String role) {
+        if (!"admin".equals(role)) {
+            return Result.error("无权限:仅管理员可管理特价活动");
+        }
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("活动ID不能为空");
+        SpecialPriceDeal deal = specialPriceService.getById(id);
+        if (deal == null) return Result.error("特价活动不存在");
+
+        String scene = "id=" + deal.getProductId() + "&sp=" + deal.getId();
+        String base64 = wechatService.generateWxacode(scene,
+                "pages/discover-detail/product-detail/product-detail");
+        Map<String, Object> data = new HashMap<>();
+        if (base64 == null || base64.isEmpty()) {
+            data.put("qrCodeBase64", "");
+            data.put("note", "测试模式无法生成真实二维码,请在生产环境操作");
+        } else {
+            data.put("qrCodeBase64", base64.startsWith("data:") ? base64 : "data:image/png;base64," + base64);
+        }
+        data.put("scene", scene);
+        return Result.success(data);
+    }
+
+    // ==================== helpers ====================
+
+    private static int intParam(Object value, int defaultVal) {
+        Integer parsed = intParam(value);
+        return parsed != null ? parsed : defaultVal;
+    }
+
+    private static Integer intParam(Object value) {
+        if (value == null) return null;
+        try {
+            String s = value.toString().trim();
+            if (s.isEmpty()) return null;
+            return Integer.valueOf(s);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 解析时间:支持 epoch 毫秒、ISO-8601(含 T)、"yyyy-MM-dd HH:mm:ss"、"yyyy-MM-dd"。
+     */
+    private static Date parseDate(Object value) {
+        if (value == null) return null;
+        String s = value.toString().trim();
+        if (s.isEmpty()) return null;
+        try {
+            return new Date(Long.parseLong(s));
+        } catch (NumberFormatException ignored) {
+        }
+        String norm = s.replace(' ', 'T');
+        if (norm.indexOf('T') < 0) {
+            norm = norm + "T00:00:00";
+        } else if (norm.length() < 19) {
+            norm = norm + (norm.endsWith("T") ? "00:00:00" : ":00");
+        }
+        try {
+            return Date.from(java.time.LocalDateTime.parse(norm)
+                    .atZone(java.time.ZoneId.systemDefault()).toInstant());
+        } catch (Exception e) {
+            try {
+                return Date.from(java.time.LocalDate.parse(s.trim())
+                        .atStartOfDay(java.time.ZoneId.systemDefault()).toInstant());
+            } catch (Exception e2) {
+                throw new IllegalArgumentException("时间格式不正确: " + s);
+            }
+        }
+    }
+}

+ 78 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/special/SpecialPriceController.java

@@ -0,0 +1,78 @@
+package com.etotem.cfc.controller.special;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.SpecialPriceDeal;
+import com.etotem.cfc.service.SpecialPriceService;
+import com.etotem.cfc.util.ParamUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 限时特价(扫码闪购)小程序端接口。
+ * 设计见 docs/superpowers/specs/2026-10-20-limited-time-special-price-design.md
+ *
+ * <p>注意:{@code /api/special-price/info} 处于白名单(免登录),{@code JwtInterceptor} 命中
+ * PUBLIC_PATHS 后直接 return true,不写入任何 request attribute(连匿名 userId 也不写)。
+ * 因此本控制器禁止使用 {@code @RequestAttribute("userId")},{@code userPurchased/userRemaining}
+ * 恒为 0;每人限购由下单接口 {@code ProductOrderService.createSpecialDealOrder} 兜底校验。</p>
+ */
+@RestController
+@RequestMapping("/api/special-price")
+public class SpecialPriceController {
+
+    @Resource
+    private SpecialPriceService specialPriceService;
+
+    /**
+     * 查询特价活动信息(公开)。
+     * 请求体:{@code {dealId}}
+     * 返回 data 字段:id, productId, name, specialPrice, startTime, endTime, runStatus,
+     * totalQuantity, soldQuantity, remainingQuantity, perUserLimit, allowCoupon,
+     * userPurchased, userRemaining。runStatus 严格取值
+     * DRAFT/ACTIVE/NOT_STARTED/SOLD_OUT/ENDED/DISABLED/INVALID。
+     */
+    @PostMapping("/info")
+    public Result<Map<String, Object>> info(@RequestBody Map<String, Object> params) {
+        Long dealId = ParamUtils.getLong(params.get("dealId"));
+        Map<String, Object> data = new HashMap<>();
+        data.put("dealId", dealId);
+        if (dealId == null) {
+            data.put("runStatus", "INVALID");
+            data.put("message", "活动不存在");
+            return Result.success(data);
+        }
+        SpecialPriceDeal deal = specialPriceService.getById(dealId);
+        if (deal == null) {
+            data.put("runStatus", "INVALID");
+            data.put("message", "活动不存在");
+            return Result.success(data);
+        }
+        int total = deal.getTotalQuantity() != null ? deal.getTotalQuantity() : 0;
+        int sold = deal.getSoldQuantity() != null ? deal.getSoldQuantity() : 0;
+        String runStatus = SpecialPriceService.computeRunStatus(
+                deal.getStatus(), deal.getStartTime(), deal.getEndTime(), sold, total);
+
+        data.put("id", deal.getId());
+        data.put("productId", deal.getProductId());
+        data.put("name", deal.getName());
+        data.put("specialPrice", deal.getSpecialPrice());
+        data.put("startTime", deal.getStartTime());
+        data.put("endTime", deal.getEndTime());
+        data.put("runStatus", runStatus);
+        data.put("totalQuantity", total);
+        data.put("soldQuantity", sold);
+        data.put("remainingQuantity", Math.max(total - sold, 0));
+        data.put("perUserLimit", deal.getPerUserLimit() != null ? deal.getPerUserLimit() : 1);
+        data.put("allowCoupon", deal.getAllowCoupon() != null ? deal.getAllowCoupon() : 0);
+        // 白名单路径不注入用户属性:恒为 0,每人限购由下单接口兜底校验
+        data.put("userPurchased", 0);
+        data.put("userRemaining", 0);
+        return Result.success(data);
+    }
+}

+ 137 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminSpecialPriceControllerTest.java

@@ -0,0 +1,137 @@
+package com.etotem.cfc.integration.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminSpecialPriceController;
+import com.etotem.cfc.entity.SpecialPriceDeal;
+import com.etotem.cfc.service.SpecialPriceService;
+import com.etotem.cfc.service.WechatService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+/**
+ * 管理端-限时特价活动接口测试。
+ * 模式参考 ActivityControllerTest / ProductControllerTest(@SpringBootTest + @MockBean 直接调 controller)。
+ */
+@SpringBootTest
+public class AdminSpecialPriceControllerTest {
+
+    @Resource
+    private AdminSpecialPriceController controller;
+
+    @MockBean
+    private SpecialPriceService specialPriceService;
+
+    @MockBean
+    private WechatService wechatService;
+
+    private Map<String, Object> validCreateParams() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("productId", 100L);
+        params.put("name", "开学季特价");
+        params.put("specialPrice", 9900);
+        params.put("startTime", "2026-10-01 00:00:00");
+        params.put("endTime", "2026-10-31 23:59:59");
+        params.put("totalQuantity", 100);
+        params.put("perUserLimit", 2);
+        params.put("allowCoupon", 0);
+        return params;
+    }
+
+    @Test
+    public void create_success() {
+        when(specialPriceService.create(any(SpecialPriceDeal.class)))
+                .thenAnswer(inv -> inv.getArgument(0));
+
+        Result<?> result = controller.create(validCreateParams(), "admin");
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void create_specialPriceNotLowerThanProductPrice_rejected() {
+        when(specialPriceService.create(any(SpecialPriceDeal.class)))
+                .thenThrow(new IllegalArgumentException("特价必须低于商品原价"));
+
+        Result<?> result = controller.create(validCreateParams(), "admin");
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("特价必须低于商品原价"));
+    }
+
+    @Test
+    public void create_activeDealExists_rejected() {
+        when(specialPriceService.create(any(SpecialPriceDeal.class)))
+                .thenThrow(new IllegalStateException("该商品已有进行中的特价活动,无法新建/启用"));
+
+        Result<?> result = controller.create(validCreateParams(), "admin");
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("已有进行中的特价活动"));
+    }
+
+    @Test
+    public void create_missingTotalQuantity_rejectedBeforeService() {
+        Map<String, Object> params = validCreateParams();
+        params.put("totalQuantity", null);
+
+        Result<?> result = controller.create(params, "admin");
+        assertEquals(500, result.getCode());
+        assertNotNull(result.getMessage());
+    }
+
+    @Test
+    public void status_enable_success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("id", 1L);
+        params.put("action", "enable");
+
+        Result<Void> result = controller.status(params, "admin");
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void delete_withOrders_rejectedWithOrderCount() {
+        doThrow(new IllegalStateException("该活动已有 3 笔特价订单,不可删除(请改为停用)"))
+                .when(specialPriceService).delete(1L);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("id", 1L);
+        Result<Void> result = controller.delete(params, "admin");
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("3 笔特价订单"));
+    }
+
+    @Test
+    public void qrcode_success_returnsDataUrlAndScene() {
+        SpecialPriceDeal deal = new SpecialPriceDeal();
+        deal.setId(1L);
+        deal.setProductId(100L);
+        when(specialPriceService.getById(1L)).thenReturn(deal);
+        when(wechatService.generateWxacode(anyString(), anyString())).thenReturn("QUJDREVG");
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("id", 1L);
+        Result<Map<String, Object>> result = controller.qrcode(params, "admin");
+        assertEquals(200, result.getCode());
+        assertEquals("id=100&sp=1", result.getData().get("scene"));
+        assertTrue(result.getData().get("qrCodeBase64").toString().startsWith("data:image/png;base64,"));
+    }
+
+    @Test
+    public void nonAdminRole_rejected() {
+        Result<?> result = controller.create(validCreateParams(), "teacher");
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("无权限"));
+    }
+}

+ 89 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/SpecialPriceControllerTest.java

@@ -0,0 +1,89 @@
+package com.etotem.cfc.integration.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.special.SpecialPriceController;
+import com.etotem.cfc.entity.SpecialPriceDeal;
+import com.etotem.cfc.service.SpecialPriceService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * 限时特价小程序端接口测试。
+ * 模式参考 ActivityControllerTest / ProductControllerTest(@SpringBootTest + @MockBean 直接调 controller)。
+ */
+@SpringBootTest
+public class SpecialPriceControllerTest {
+
+    @Resource
+    private SpecialPriceController controller;
+
+    @MockBean
+    private SpecialPriceService specialPriceService;
+
+    private Map<String, Object> dealParams(Long dealId) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("dealId", dealId);
+        return params;
+    }
+
+    private SpecialPriceDeal activeDeal() {
+        SpecialPriceDeal deal = new SpecialPriceDeal();
+        deal.setId(1L);
+        deal.setProductId(100L);
+        deal.setName("开学季特价");
+        deal.setSpecialPrice(9900);
+        deal.setStartTime(new Date(System.currentTimeMillis() - 86400_000L));
+        deal.setEndTime(new Date(System.currentTimeMillis() + 86400_000L));
+        deal.setTotalQuantity(100);
+        deal.setSoldQuantity(5);
+        deal.setPerUserLimit(3);
+        deal.setAllowCoupon(0);
+        deal.setStatus("active");
+        return deal;
+    }
+
+    @Test
+    public void info_active_returnsACTIVEWithExpectedFields() {
+        when(specialPriceService.getById(1L)).thenReturn(activeDeal());
+
+        Result<Map<String, Object>> result = controller.info(dealParams(1L));
+        assertEquals(200, result.getCode());
+        Map<String, Object> data = result.getData();
+        assertEquals("ACTIVE", data.get("runStatus"));
+        assertEquals(9900, data.get("specialPrice"));
+        assertEquals(95, data.get("remainingQuantity"));
+        assertEquals(3, data.get("perUserLimit"));
+        assertEquals(0, data.get("allowCoupon"));
+        // 白名单路径不注入用户属性:恒为 0
+        assertEquals(0, data.get("userPurchased"));
+        assertEquals(0, data.get("userRemaining"));
+    }
+
+    @Test
+    public void info_notExists_returnsINVALID() {
+        when(specialPriceService.getById(999L)).thenReturn(null);
+
+        Result<Map<String, Object>> result = controller.info(dealParams(999L));
+        assertEquals(200, result.getCode());
+        assertEquals("INVALID", result.getData().get("runStatus"));
+    }
+
+    @Test
+    public void info_anonymous_noAuthRequired() {
+        // 白名单路径:无 token / 无 request attribute 时同样可访问(登录态不影响)
+        when(specialPriceService.getById(1L)).thenReturn(activeDeal());
+
+        Result<Map<String, Object>> result = controller.info(dealParams(1L));
+        assertEquals(200, result.getCode());
+        assertEquals("ACTIVE", result.getData().get("runStatus"));
+    }
+}