[v2.1] B 审批代理补齐:增 ByBiz/RecordUrl/TaskPageUrl 端点,Approve 透传 edgeName;身份仍全取 CAS 会话前端不可传;补 Controller/Client 单测
build / build-push (push) Successful in 1m17s
build / build-push (push) Successful in 1m17s
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.config.Config;
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.service.flow.NoticeApprovalClient;
|
||||
import com.yunhai.notice.service.flow.NoticeFlowEmbedService;
|
||||
import com.yunhai.notice.service.flow.NoticeFlowGuard;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
@@ -15,15 +19,19 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 应用内审批(第三步 T-C4):"我的待审核"数据面。前端不直调 oapi——
|
||||
* operatorId/operatorName 一律取 CAS 登录态(BoaUser),请求参数里不收身份字段,
|
||||
* 应用内审批(第三步 T-C4):"我的待审核"数据面 + v2.1 业务详情卡片/记录/完整办理换址。
|
||||
* 前端不直调 oapi——operatorId/operatorName 一律取 CAS 登录态(BoaUser),请求参数里不收身份字段,
|
||||
* 防伪造办理人;对端 flow-center 再按任务办理人复核(双层)。
|
||||
* <ul>
|
||||
* <li>GET Approval/Todo:待办分页(bizType=notice 固定);</li>
|
||||
* <li>GET Approval/Instances:办件分页(status 可选);</li>
|
||||
* <li>GET Approval/ByBiz:当前人在该通知上的待办(0/1/N,卡片据此决定可写还是只读);</li>
|
||||
* <li>GET Approval/RecordUrl:审批记录抽屉地址(token 只读页,复用 {@link NoticeFlowEmbedService});</li>
|
||||
* <li>GET Approval/TaskPageUrl:完整办理任务页地址(先确认 taskId 属于当前人待办);</li>
|
||||
* <li>POST Approval/Approve / Reject:办理(body 仅 opinion/edgeName)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@@ -33,6 +41,9 @@ import java.util.Map;
|
||||
public class NoticeApprovalController {
|
||||
|
||||
private final NoticeApprovalClient approvalClient;
|
||||
private final NoticeFlowEmbedService embedService;
|
||||
private final NoticeRepository noticeRepository;
|
||||
private final NoticeFlowGuard noticeFlowGuard;
|
||||
|
||||
/** 我的待审核(列表页数据源) */
|
||||
@GetMapping("Todo")
|
||||
@@ -72,6 +83,79 @@ public class NoticeApprovalController {
|
||||
body == null ? null : body.get("opinion"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前人在该通知上的待办(v2.1 卡片数据源)。身份只取会话;{@code serid} 先过与记录页
|
||||
* 同一套可见性守卫(未发布通知仅提交人/管理员/办理人/被抄送人可见)。
|
||||
* <p>出参 {@code tasks} 的 0/1/N 由前端据此决定"只读 / 可办理 / 多个待办引导去列表",
|
||||
* 本端不猜选、不合并。办件不存在与"有办件但不是我的待办"同形(空 tasks),不用于探测。
|
||||
*/
|
||||
@GetMapping("ByBiz")
|
||||
public Map<String, Object> byBiz(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||
Operator op = requireOperator(user);
|
||||
requireVisible(serid, user);
|
||||
return approvalClient.byBiz(op.schId, op.userId, serid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批记录抽屉地址(token 只读页):直接复用 {@link NoticeFlowEmbedService#embedUrl},
|
||||
* 那里已做通知存在性 + 未发布可见性 + "尚未进入流程"三重校验,且 token 现取现发不缓存。
|
||||
* 与 {@code /api/notice/flow/Embed} 同一条实现,避免两个入口对同一 Drawer 漂移出不同守卫口径。
|
||||
*/
|
||||
@GetMapping("RecordUrl")
|
||||
public Map<String, String> recordUrl(@RequestParam("serid") Integer serid, BoaUser user) {
|
||||
requireOperator(user);
|
||||
return embedService.embedUrl(serid, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整办理任务页地址(CAS 会话页,无 token)。
|
||||
* <p>先打 byBiz 确认 {@code taskId} 确实在<b>当前人</b>该通知的待办里才签发——任务页是写面,
|
||||
* 顺手签发一个别人的 taskId 等于替他人挂一个办理入口。flow-center 侧仍会按 assigneeId
|
||||
* 逐字复核,本处只是不把无效地址散播到前端。
|
||||
*/
|
||||
@GetMapping("TaskPageUrl")
|
||||
public Map<String, Object> taskPageUrl(@RequestParam("serid") Integer serid,
|
||||
@RequestParam("taskId") Long taskId, BoaUser user) {
|
||||
Operator op = requireOperator(user);
|
||||
requireVisible(serid, user);
|
||||
if (!isMyPendingTask(serid, taskId, op)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "该任务不是你在本通知上的待办");
|
||||
}
|
||||
return approvalClient.taskPageUrl(op.schId, taskId);
|
||||
}
|
||||
|
||||
/** byBiz 出参里是否含该 taskId(对端 tasks 元素为 {taskId,nodeId,nodeName,arriveTime,outlets}) */
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean isMyPendingTask(Integer serid, Long taskId, Operator op) {
|
||||
Map<String, Object> data = approvalClient.byBiz(op.schId, op.userId, serid);
|
||||
Object tasks = data == null ? null : data.get("tasks");
|
||||
if (!(tasks instanceof List)) {
|
||||
return false;
|
||||
}
|
||||
for (Object row : (List<Object>) tasks) {
|
||||
if (row instanceof Map) {
|
||||
Object id = ((Map<String, Object>) row).get("taskId");
|
||||
if (id instanceof Number && taskId.equals(((Number) id).longValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知可见性守卫(口径与 {@link NoticeFlowEmbedService#embedUrl} 逐字一致):存在性 +
|
||||
* 未发布(status!=1)走 {@link NoticeFlowGuard}。已发布通知照常放行(全校可见,审批数据面
|
||||
* 只对办理人出 tasks)。
|
||||
*/
|
||||
private void requireVisible(Integer noticeId, BoaUser user) {
|
||||
Notice notice = noticeRepository.findById(noticeId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "通知不存在: " + noticeId));
|
||||
if (notice.getStatus() == null || notice.getStatus() != 1) {
|
||||
noticeFlowGuard.requireUnpublishedVisible(notice, user);
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录态提参:无身份字段可被请求注入;姓名兜底账号(与 NoticeFlowService 提交人口径一致) */
|
||||
private static Operator requireOperator(BoaUser user) {
|
||||
if (user == null || user.getId() == null) {
|
||||
|
||||
@@ -72,6 +72,41 @@ public class NoticeApprovalClient {
|
||||
return openCall.get(TARGET, "/oapi/flow/instances", query, Map.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前人在本通知上的待办(v2.1 业务详情卡片数据源,{@code /oapi/flow/tasks/by-biz})。
|
||||
* <p>返回 Map 即对端 {@code ByBizTasksVo} JSON:{@code tasks[]} 0/1/N 三种计数都走 HTTP 200,
|
||||
* <b>调用方不得据 tasks 反推"这个通知上有没有别人的待办"</b>——办件不存在、错学校、错 bizType、
|
||||
* 非办理人四种情形出参完全同形(都是空 tasks),这是对端的防探测设计,本端不要试图区分。
|
||||
* <p>{@code currentNodes} 是节点 <b>id</b> 逗号串(办件原文口径),人读名称取 {@code tasks[].nodeName}。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> byBiz(Integer schId, Integer operatorId, Integer noticeId) {
|
||||
requireEnabled();
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("schId", String.valueOf(schId));
|
||||
query.put("bizType", FlowCenterClient.BIZ_TYPE);
|
||||
query.put("bizId", String.valueOf(noticeId));
|
||||
query.put("operatorId", String.valueOf(operatorId));
|
||||
return openCall.get(TARGET, "/oapi/flow/tasks/by-biz", query, Map.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整办理任务页地址({@code /oapi/flow/embed/url?mode=task}):返回 CAS 会话页
|
||||
* {@code {front}/#/task/{taskId}},<b>无 token</b>(写面,办理权由该页加载时的用户面接口按
|
||||
* assigneeId 复核)。仍走 {@link YschOpenCall} 泛化调用——nexus 已发布快照的
|
||||
* {@code YschFlowOpenApi} 只有 embedUrl/todoTasks/instances,没有 taskPageUrl 门面方法,
|
||||
* 而本批纪律是不动 BasePlatform。SDK 补齐后可平替(URL 不变,测试锁死)。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> taskPageUrl(Integer schId, Long taskId) {
|
||||
requireEnabled();
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("schId", String.valueOf(schId));
|
||||
query.put("mode", "task");
|
||||
query.put("taskId", String.valueOf(taskId));
|
||||
return openCall.get(TARGET, "/oapi/flow/embed/url", query, Map.class);
|
||||
}
|
||||
|
||||
/** 同意(opinion 可空;多出口 edgeName 透传) */
|
||||
public void approve(Long taskId, Integer schId, Integer operatorId, String operatorName,
|
||||
String opinion, String edgeName) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package com.yunhai.notice.controller.api;
|
||||
|
||||
import com.yunhai.notice.entity.Notice;
|
||||
import com.yunhai.notice.repository.NoticeRepository;
|
||||
import com.yunhai.notice.service.flow.NoticeApprovalClient;
|
||||
import com.yunhai.notice.service.flow.NoticeFlowEmbedService;
|
||||
import com.yunhai.notice.service.flow.NoticeFlowGuard;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* v2.1 应用内审批代理端点单测(ByBiz / RecordUrl / TaskPageUrl)。
|
||||
* <p>本类的存在意义全在"身份与地址"这两条纪律上:
|
||||
* <ul>
|
||||
* <li>身份只能来自 CAS 会话(BoaUser),请求参数里没有 operatorId/schId 的位置——
|
||||
* 断言的是"传给出站客户端的正是会话里的值",而不是"参数被透传";</li>
|
||||
* <li>任务页是写面:taskId 不在当前人待办里就不签发地址(宁可前端拿不到按钮,
|
||||
* 也不发一个能点到别人办理页的地址)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class NoticeApprovalControllerTest {
|
||||
|
||||
private static final int SCH = 27;
|
||||
private static final int USER_ID = 10006;
|
||||
|
||||
private NoticeApprovalClient approvalClient;
|
||||
private NoticeFlowEmbedService embedService;
|
||||
private NoticeRepository noticeRepository;
|
||||
private NoticeFlowGuard noticeFlowGuard;
|
||||
private NoticeApprovalController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
approvalClient = mock(NoticeApprovalClient.class);
|
||||
embedService = mock(NoticeFlowEmbedService.class);
|
||||
noticeRepository = mock(NoticeRepository.class);
|
||||
noticeFlowGuard = mock(NoticeFlowGuard.class);
|
||||
controller = new NoticeApprovalController(approvalClient, embedService, noticeRepository, noticeFlowGuard);
|
||||
}
|
||||
|
||||
private BoaUser user() {
|
||||
BoaUser user = new BoaUser();
|
||||
user.setId(USER_ID);
|
||||
user.setAccount("liuwenjuan");
|
||||
user.setName("刘文娟");
|
||||
user.setType(0);
|
||||
BoaRoleInfo role = new BoaRoleInfo();
|
||||
role.setSchId(SCH);
|
||||
user.setCurrentRole(role);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void stubNotice(Integer serid, Integer status) {
|
||||
Notice notice = new Notice();
|
||||
notice.setId(serid);
|
||||
notice.setSchId(SCH);
|
||||
notice.setStatus(status);
|
||||
when(noticeRepository.findById(serid)).thenReturn(Optional.of(notice));
|
||||
}
|
||||
|
||||
/** byBiz 出参里的 tasks(模拟对端 JSON 反序列化后的 Map 形态) */
|
||||
private Map<String, Object> tasksOf(Long... taskIds) {
|
||||
List<Object> rows = new ArrayList<>();
|
||||
for (Long id : taskIds) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("taskId", id);
|
||||
row.put("nodeId", "n1");
|
||||
row.put("nodeName", "后勤资产处");
|
||||
row.put("outlets", Collections.emptyList());
|
||||
rows.add(row);
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("instanceId", 123L);
|
||||
data.put("tasks", rows);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ===== ByBiz =====
|
||||
|
||||
/** 身份取自会话:出站用的是 BoaUser 的 schId/userId,前端参数里只有 serid */
|
||||
@Test
|
||||
void byBiz_identityFromSessionNotRequest() {
|
||||
stubNotice(71, 1);
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(tasksOf(442L));
|
||||
Map<String, Object> out = controller.byBiz(71, user());
|
||||
verify(approvalClient).byBiz(eq(SCH), eq(USER_ID), eq(71));
|
||||
assertEquals(1, ((List<?>) out.get("tasks")).size(), "1 条待办原样透出给卡片");
|
||||
}
|
||||
|
||||
/** 已发布通知(status=1)不走未发布守卫;未发布(status=0)必须走 NoticeFlowGuard */
|
||||
@Test
|
||||
void byBiz_unpublishedNotice_goesThroughVisibilityGuard() {
|
||||
stubNotice(70, 0);
|
||||
BoaUser user = user();
|
||||
controller.byBiz(70, user);
|
||||
verify(noticeFlowGuard).requireUnpublishedVisible(any(Notice.class), eq(user));
|
||||
}
|
||||
|
||||
@Test
|
||||
void byBiz_publishedNotice_skipsUnpublishedGuard() {
|
||||
stubNotice(71, 1);
|
||||
controller.byBiz(71, user());
|
||||
verify(noticeFlowGuard, never()).requireUnpublishedVisible(any(Notice.class), any(BoaUser.class));
|
||||
}
|
||||
|
||||
/** 通知不存在 → 拒绝,且不出站(不给流程中心发一个查不到对象的 byBiz) */
|
||||
@Test
|
||||
void byBiz_noticeMissing_rejectedAndNoOutbound() {
|
||||
when(noticeRepository.findById(999)).thenReturn(Optional.empty());
|
||||
BizException e = assertThrows(BizException.class, () -> controller.byBiz(999, user()));
|
||||
assertTrue(e.getMessage().contains("通知不存在"), "文案须是查不到语义:" + e.getMessage());
|
||||
verify(approvalClient, never()).byBiz(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
/** 不可见(守卫抛 FORBIDDEN)→ 原样拒绝 */
|
||||
@Test
|
||||
void byBiz_notVisible_guardThrows() {
|
||||
stubNotice(70, 0);
|
||||
org.mockito.Mockito.doThrow(new BizException(
|
||||
com.yunhai.platform.utils.exception.ErrorCode.FORBIDDEN, "通知不存在或未发布"))
|
||||
.when(noticeFlowGuard).requireUnpublishedVisible(any(Notice.class), any(BoaUser.class));
|
||||
assertThrows(BizException.class, () -> controller.byBiz(70, user()));
|
||||
verify(approvalClient, never()).byBiz(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
/** 未登录(BoaUser 空)→ 拒绝,身份字段无从伪造 */
|
||||
@Test
|
||||
void byBiz_noSession_rejected() {
|
||||
stubNotice(71, 1);
|
||||
assertThrows(BizException.class, () -> controller.byBiz(71, null));
|
||||
BoaUser noSch = user();
|
||||
noSch.setCurrentRole(null);
|
||||
assertThrows(BizException.class, () -> controller.byBiz(71, noSch));
|
||||
}
|
||||
|
||||
/** 0 条:对端 tasks=[] 时不得报错,卡片据此渲染只读 */
|
||||
@Test
|
||||
void byBiz_zeroTasks_passesThroughEmpty() {
|
||||
stubNotice(72, 1);
|
||||
Map<String, Object> empty = tasksOf();
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 72)).thenReturn(empty);
|
||||
assertEquals(0, ((List<?>) controller.byBiz(72, user()).get("tasks")).size());
|
||||
}
|
||||
|
||||
// ===== TaskPageUrl =====
|
||||
|
||||
/** taskId 在当前人待办里 → 签发任务页地址 */
|
||||
@Test
|
||||
void taskPageUrl_taskIsMine_issued() {
|
||||
stubNotice(71, 1);
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(tasksOf(442L, 443L));
|
||||
Map<String, Object> page = new HashMap<>();
|
||||
page.put("pageUrl", "http://localhost:8088/#/task/443");
|
||||
when(approvalClient.taskPageUrl(SCH, 443L)).thenReturn(page);
|
||||
assertEquals("http://localhost:8088/#/task/443",
|
||||
controller.taskPageUrl(71, 443L, user()).get("pageUrl"));
|
||||
}
|
||||
|
||||
/** 核心越权用例:taskId 不属于当前人待办 → 拒绝签发,且不调用换址端点 */
|
||||
@Test
|
||||
void taskPageUrl_taskNotMine_notIssued() {
|
||||
stubNotice(71, 1);
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(tasksOf(442L));
|
||||
BizException e = assertThrows(BizException.class, () -> controller.taskPageUrl(71, 999L, user()));
|
||||
assertTrue(e.getMessage().contains("不是你在本通知上的待办"), "文案须说明拒因:" + e.getMessage());
|
||||
verify(approvalClient, never()).taskPageUrl(anyInt(), anyLong());
|
||||
}
|
||||
|
||||
/** 本人无待办(tasks=[])→ 同样拒绝,不为"空 tasks"开一条签发旁路 */
|
||||
@Test
|
||||
void taskPageUrl_noTasks_rejected() {
|
||||
stubNotice(71, 1);
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(tasksOf());
|
||||
assertThrows(BizException.class, () -> controller.taskPageUrl(71, 442L, user()));
|
||||
verify(approvalClient, never()).taskPageUrl(anyInt(), anyLong());
|
||||
}
|
||||
|
||||
/** 脏出参(tasks 缺位/非列表)按"不在待办"处理,不因解析失败而放行 */
|
||||
@Test
|
||||
void taskPageUrl_malformedPayload_failClosed() {
|
||||
stubNotice(71, 1);
|
||||
Map<String, Object> garbage = new HashMap<>();
|
||||
garbage.put("tasks", "not-a-list");
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(garbage);
|
||||
assertThrows(BizException.class, () -> controller.taskPageUrl(71, 442L, user()));
|
||||
when(approvalClient.byBiz(SCH, USER_ID, 71)).thenReturn(null);
|
||||
assertThrows(BizException.class, () -> controller.taskPageUrl(71, 442L, user()));
|
||||
}
|
||||
|
||||
/** 不可见通知不得换任务页地址(与 ByBiz 同一道守卫) */
|
||||
@Test
|
||||
void taskPageUrl_unvisible_rejected() {
|
||||
stubNotice(70, 0);
|
||||
org.mockito.Mockito.doThrow(new BizException(
|
||||
com.yunhai.platform.utils.exception.ErrorCode.FORBIDDEN, "通知不存在或未发布"))
|
||||
.when(noticeFlowGuard).requireUnpublishedVisible(any(Notice.class), any(BoaUser.class));
|
||||
assertThrows(BizException.class, () -> controller.taskPageUrl(70, 442L, user()));
|
||||
verify(approvalClient, never()).taskPageUrl(anyInt(), anyLong());
|
||||
}
|
||||
|
||||
// ===== RecordUrl =====
|
||||
|
||||
/** 记录页复用 NoticeFlowEmbedService(三重校验与 token 现取现发都在那里),本端只做转发 */
|
||||
@Test
|
||||
void recordUrl_delegatesToEmbedService() {
|
||||
BoaUser user = user();
|
||||
when(embedService.embedUrl(71, user))
|
||||
.thenReturn(Collections.singletonMap("pageUrl", "http://localhost:8088/#/embed/instance?token=x"));
|
||||
assertEquals("http://localhost:8088/#/embed/instance?token=x",
|
||||
controller.recordUrl(71, user).get("pageUrl"));
|
||||
verify(embedService).embedUrl(71, user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordUrl_notLoggedIn_rejected() {
|
||||
assertThrows(BizException.class, () -> controller.recordUrl(71, null));
|
||||
verify(embedService, never()).embedUrl(anyInt(), any(BoaUser.class));
|
||||
}
|
||||
|
||||
// ===== Approve / Reject(回归位:确认改造后 edgeName 不丢、驳回意见仍被拦) =====
|
||||
|
||||
@Test
|
||||
void approve_edgeNameStillPropagated() {
|
||||
Map<String, String> body = new LinkedHashMap<>();
|
||||
body.put("opinion", "同意");
|
||||
body.put("edgeName", "转乙科");
|
||||
controller.approve(442L, user(), body);
|
||||
verify(approvalClient).approve(442L, SCH, USER_ID, "刘文娟", "同意", "转乙科");
|
||||
}
|
||||
|
||||
/** 单出口快速通过:body 为空/不带 edgeName 时,出站就是 null,不擅自补一个出口名 */
|
||||
@Test
|
||||
void approve_withoutEdge_sendsNullEdgeName() {
|
||||
controller.approve(442L, user(), Collections.emptyMap());
|
||||
verify(approvalClient).approve(442L, SCH, USER_ID, "刘文娟", null, null);
|
||||
}
|
||||
|
||||
/** 无会话即拒绝,且不出站——operator 永远不来自请求 */
|
||||
@Test
|
||||
void approve_noSession_rejected() {
|
||||
assertThrows(BizException.class, () -> controller.approve(442L, null, new HashMap<>()));
|
||||
verify(approvalClient, never()).approve(anyLong(), anyInt(), anyInt(), any(), any(), any());
|
||||
}
|
||||
|
||||
/** 驳回意见必填由出站客户端前置强制;控制器原样抛出,不吞成"成功" */
|
||||
@Test
|
||||
void reject_blankOpinion_propagates() {
|
||||
org.mockito.Mockito.doThrow(new BizException(
|
||||
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "驳回必须填写意见"))
|
||||
.when(approvalClient).reject(eq(442L), eq(SCH), eq(USER_ID), eq("刘文娟"), any());
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> controller.reject(442L, user(), Collections.singletonMap("opinion", " ")));
|
||||
assertTrue(e.getMessage().contains("意见"), "须把拦截原因交给前端:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,58 @@ class NoticeApprovalClientTest {
|
||||
properties.setEnabled(false);
|
||||
assertThrows(BizException.class, () -> client.todoTasks(27, 10006, null, null));
|
||||
assertThrows(BizException.class, () -> client.approve(1L, 27, 1, "甲", null, null));
|
||||
assertThrows(BizException.class, () -> client.byBiz(27, 10006, 71));
|
||||
assertThrows(BizException.class, () -> client.taskPageUrl(27, 1L));
|
||||
}
|
||||
|
||||
// ===== v2.1 应用内审批:by-biz 卡片数据源 + 完整办理任务页换址 =====
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void byBiz_urlAndParams_charExact() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
when(call.get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), anyMap(), eq(Map.class))).thenReturn(data);
|
||||
client.byBiz(27, 10006, 71);
|
||||
Map<String, String> expected = new LinkedHashMap<>();
|
||||
expected.put("schId", "27");
|
||||
expected.put("bizType", "notice");
|
||||
expected.put("bizId", "71");
|
||||
expected.put("operatorId", "10006");
|
||||
verify(call).get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"), eq(expected), eq(Map.class));
|
||||
}
|
||||
|
||||
/** by-biz 四个参数一律必传(缺一即可能被对端当"可选过滤"放宽成越权查询) */
|
||||
@Test
|
||||
void byBiz_allFourParamsAlwaysSent() {
|
||||
when(call.get(any(String.class), any(String.class), anyMap(), eq(Map.class))).thenReturn(new HashMap<>());
|
||||
client.byBiz(27, 10006, 71);
|
||||
verify(call).get(eq(TARGET), eq("/oapi/flow/tasks/by-biz"),
|
||||
Mockito.argThat(q -> q.size() == 4 && q.containsKey("schId") && q.containsKey("bizType")
|
||||
&& q.containsKey("bizId") && q.containsKey("operatorId")), eq(Map.class));
|
||||
}
|
||||
|
||||
/** 任务页换址走 mode=task(CAS 页、无 token);本批不动 BasePlatform,故仍是泛化调用 */
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void taskPageUrl_modeTaskContractShape() {
|
||||
Map<String, Object> page = new HashMap<>();
|
||||
page.put("pageUrl", "http://localhost:8088/#/task/442");
|
||||
when(call.get(eq(TARGET), eq("/oapi/flow/embed/url"), anyMap(), eq(Map.class))).thenReturn(page);
|
||||
Map<String, Object> out = client.taskPageUrl(27, 442L);
|
||||
assertEquals("http://localhost:8088/#/task/442", out.get("pageUrl"));
|
||||
Map<String, String> expected = new LinkedHashMap<>();
|
||||
expected.put("schId", "27");
|
||||
expected.put("mode", "task");
|
||||
expected.put("taskId", "442");
|
||||
verify(call).get(eq(TARGET), eq("/oapi/flow/embed/url"), eq(expected), eq(Map.class));
|
||||
}
|
||||
|
||||
/** 流程中心不可达:原样抛出,不吞异常、不伪装成"无待办"(卡片据此提示稍后重试而非渲染只读空态) */
|
||||
@Test
|
||||
void byBiz_unreachable_propagatesNotSwallowed() {
|
||||
when(call.get(any(String.class), any(String.class), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new RuntimeException("connect timeout"));
|
||||
assertThrows(RuntimeException.class, () -> client.byBiz(27, 10006, 71));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user