feat(approval): 内部协调单表单、提交预检与双部门负责人审批 (#23)
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package com.gxwebsoft.approval;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 内部协调单:草稿、字段校验、提交预检、部门快照与双节点审批的集成测试。
|
||||
*
|
||||
* <p>不开启测试事务:提交与审批动作各自提交,便于验证跨库解析与节点推进;
|
||||
* 数据由 {@link ApprovalTestData#cleanup()} 清理。</p>
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalCoordinationIT {
|
||||
|
||||
private static final int TENANT = TestAuthentication.TENANT_ID;
|
||||
private static final String LIST = "approval:instance:list";
|
||||
private static final String SUBMIT = "approval:instance:submit";
|
||||
private static final String APPROVE = "approval:instance:approve";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private Integer reportOrgId;
|
||||
private Integer acceptOrgId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
reportOrgId = data.createOrganization("协调报送部门" + data.uniqueSuffix(), 136);
|
||||
acceptOrgId = data.createOrganization("协调受理部门" + data.uniqueSuffix(), 136);
|
||||
reporter = data.createUser("协调报送人" + data.uniqueSuffix(), reportOrgId);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsDraftWithServerSideReportFields() throws Exception {
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "设备借用协调",
|
||||
"借用体育馆音响设备一天", "活动结束后归还");
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
assertThat(data.queryMaster("SELECT doc_type FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("coordination");
|
||||
assertThat(data.queryMaster("SELECT flow_code FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("coordination_internal");
|
||||
assertThat(data.queryMaster("SELECT reporter_id FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isEqualTo(reporter.userId);
|
||||
assertThat(data.queryMaster("SELECT reporter_name FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo(reporter.username);
|
||||
assertThat(data.queryMaster("SELECT report_org_id FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isEqualTo(reportOrgId);
|
||||
assertThat(data.queryMaster("SELECT report_org_name FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isNotBlank();
|
||||
// 受理部门在提交时才写入实例快照
|
||||
assertThat(data.queryMaster("SELECT accept_org_id FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isNull();
|
||||
assertThat(data.queryMaster("SELECT accept_org_id FROM approval_coordination_form WHERE instance_id = ?",
|
||||
Integer.class, instanceId)).isEqualTo(acceptOrgId);
|
||||
assertThat(data.queryMaster("SELECT title FROM approval_coordination_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isEqualTo("设备借用协调");
|
||||
assertThat(data.queryMaster("SELECT remark FROM approval_coordination_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isEqualTo("活动结束后归还");
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, reportOrgId, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.docType").value("coordination"))
|
||||
.andExpect(jsonPath("$.data.docTypeName").value("内部协调单"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("未提交"))
|
||||
.andExpect(jsonPath("$.data.title").value("设备借用协调"))
|
||||
.andExpect(jsonPath("$.data.content").value("借用体育馆音响设备一天"))
|
||||
.andExpect(jsonPath("$.data.remark").value("活动结束后归还"))
|
||||
.andExpect(jsonPath("$.data.acceptOrgName").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(true))
|
||||
.andExpect(jsonPath("$.data.bizNo").doesNotExist());
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page"), reporter, reportOrgId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId))
|
||||
.andExpect(jsonPath("$.data.list[0].docTypeName").value("内部协调单"))
|
||||
.andExpect(jsonPath("$.data.list[0].title").value("设备借用协调"))
|
||||
.andExpect(jsonPath("$.data.list[0].acceptOrgName").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.list[0].statusName").value("未提交"));
|
||||
|
||||
// 流程定义按租户幂等,节点顺序固定;一期不产生外部协调流程
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_flow_definition "
|
||||
+ "WHERE tenant_id = ? AND flow_code = 'coordination_internal' AND version = 1", TENANT)).isEqualTo(1);
|
||||
String nodes = data.queryMaster("SELECT nodes_json FROM approval_flow_definition "
|
||||
+ "WHERE tenant_id = ? AND flow_code = 'coordination_internal' AND version = 1", String.class, TENANT);
|
||||
assertThat(nodes).contains("reportLeader").contains("acceptLeader").contains("dispatch")
|
||||
.contains("confirm").contains("archive");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_flow_definition "
|
||||
+ "WHERE tenant_id = ? AND flow_code = 'coordination_external'", TENANT)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesDraftOnlyForOwnerInEditableState() throws Exception {
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "原标题", "原内容", null);
|
||||
Integer otherAcceptOrg = data.createOrganization("备用受理部门" + data.uniqueSuffix(), 136);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coordination"), reporter, reportOrgId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(otherAcceptOrg, "新标题", "新内容", "补充说明", instanceId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value((int) instanceId));
|
||||
|
||||
assertThat(data.queryMaster("SELECT title FROM approval_coordination_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isEqualTo("新标题");
|
||||
assertThat(data.queryMaster("SELECT accept_org_id FROM approval_coordination_form WHERE instance_id = ?",
|
||||
Integer.class, instanceId)).isEqualTo(otherAcceptOrg);
|
||||
assertThat(data.queryMaster("SELECT remark FROM approval_coordination_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isEqualTo("补充说明");
|
||||
|
||||
ApprovalTestData.TestUser outsider = data.createUser("协调路人" + data.uniqueSuffix(), reportOrgId);
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coordination"), outsider, reportOrgId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(otherAcceptOrg, "越权标题", "内容", null, instanceId)))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权操作该单据")));
|
||||
|
||||
// 提交后不可再修改
|
||||
grantDepartmentLeader(reportOrgId, 1);
|
||||
grantDepartmentLeader(otherAcceptOrg, 1);
|
||||
grantArchivist();
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
submitCoordination(instanceId, reporter, reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coordination"), reporter, reportOrgId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(acceptOrgId, "提交后改标题", "内容", null, instanceId)))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不可修改")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesCoordinationFields() throws Exception {
|
||||
expectSaveFailure(body(null, "标题", "内容", null, null), "请选择受理部门");
|
||||
expectSaveFailure(body(acceptOrgId, "", "内容", null, null), "标题不能为空");
|
||||
expectSaveFailure(body(acceptOrgId, repeat("标", 101), "内容", null, null), "标题不能超过100字");
|
||||
expectSaveFailure(body(acceptOrgId, "标题", "", null, null), "内容不能为空");
|
||||
expectSaveFailure(body(acceptOrgId, "标题", repeat("内", 2001), null, null), "内容不能超过2000字");
|
||||
expectSaveFailure(body(acceptOrgId, "标题", "内容", repeat("备", 501), null), "备注不能超过500字");
|
||||
|
||||
Integer disabledOrg = data.createOrganization("停用受理部门" + data.uniqueSuffix(), 136);
|
||||
data.disableOrganization(disabledOrg);
|
||||
expectSaveFailure(body(disabledOrg, "标题", "内容", null, null), "受理部门不存在或已停用");
|
||||
|
||||
// 备注为可选字段,留空保存成功
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "无备注", "内容", " ");
|
||||
assertThat(data.queryMaster("SELECT remark FROM approval_coordination_form WHERE instance_id = ?",
|
||||
String.class, instanceId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenReportLeaderRoleMissing() throws Exception {
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "缺报送负责人", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("报送部门负责人角色缺配")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenAcceptLeaderRoleMissing() throws Exception {
|
||||
grantDepartmentLeader(reportOrgId, 1);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "缺受理负责人", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("受理部门负责人角色缺配")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenAcceptOrganizationDisabled() throws Exception {
|
||||
grantDepartmentLeader(reportOrgId, 1);
|
||||
grantDepartmentLeader(acceptOrgId, 1);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "受理部门停用", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
data.disableOrganization(acceptOrgId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("受理部门已停用")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
assertThat(data.queryMaster("SELECT accept_org_id FROM approval_instance WHERE id = ?",
|
||||
Integer.class, instanceId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitsWithXtBizNoAndSnapshotsBothOrganizations() throws Exception {
|
||||
ApprovalTestData.TestUser reportLeader = grantDepartmentLeader(reportOrgId, 1).get(0);
|
||||
grantDepartmentLeader(acceptOrgId, 1);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "提交协调单", "协调内容", "备注内容");
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("审批中"))
|
||||
.andExpect(jsonPath("$.data.docTypeName").value("内部协调单"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("报送部门负责人"))
|
||||
.andExpect(jsonPath("$.data.acceptOrgName").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.attachments.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.records[0].action").value("SUBMIT"))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canSubmit").value(false));
|
||||
|
||||
String bizNo = bizNo(instanceId);
|
||||
assertThat(bizNo).matches("XT\\d{8}-\\d{4}");
|
||||
assertThat(bizNo.substring(2, 10)).isEqualTo(todayFromDatabase());
|
||||
|
||||
assertThat(data.queryMaster("SELECT round FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isEqualTo(1);
|
||||
assertThat(data.queryMaster("SELECT submit_time FROM approval_instance WHERE id = ?", Object.class, instanceId))
|
||||
.isNotNull();
|
||||
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("reportLeader");
|
||||
assertThat(data.queryMaster("SELECT current_node_type FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("APPROVE");
|
||||
assertThat(data.queryMaster("SELECT report_org_id FROM approval_instance WHERE id = ?",
|
||||
Integer.class, instanceId)).isEqualTo(reportOrgId);
|
||||
assertThat(data.queryMaster("SELECT accept_org_id FROM approval_instance WHERE id = ?",
|
||||
Integer.class, instanceId)).isEqualTo(acceptOrgId);
|
||||
assertThat(data.queryMaster("SELECT accept_org_name FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId))
|
||||
.isEqualTo(data.server().queryForObject(
|
||||
"SELECT organization_name FROM sys_organization WHERE organization_id = ?",
|
||||
String.class, acceptOrgId));
|
||||
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(reportLeader.userId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvesReportThenAcceptLeaderAndReachesDispatch() throws Exception {
|
||||
ApprovalTestData.TestUser reportLeader = grantDepartmentLeader(reportOrgId, 1).get(0);
|
||||
ApprovalTestData.TestUser acceptLeader = grantDepartmentLeader(acceptOrgId, 1).get(0);
|
||||
grantArchivist();
|
||||
ApprovalTestData.TestUser acceptMember = data.createUser("受理部门成员" + data.uniqueSuffix(), acceptOrgId);
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "双节点审批", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
submitCoordination(instanceId, reporter, reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/todo/page"), reportLeader, reportOrgId, LIST))
|
||||
.andExpect(jsonPath("$.data.count").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].currentNodeName").value("报送部门负责人"));
|
||||
|
||||
long reportTask = pendingTaskId(instanceId, reportLeader.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + reportTask + "/approve"),
|
||||
reportLeader, reportOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意协调\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("受理部门负责人"))
|
||||
.andExpect(jsonPath("$.data.myTask").doesNotExist());
|
||||
|
||||
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("acceptLeader");
|
||||
assertThat(pendingTaskIds(instanceId)).containsExactly(acceptLeader.userId);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/todo/page"), acceptLeader, acceptOrgId, LIST))
|
||||
.andExpect(jsonPath("$.data.count").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId));
|
||||
|
||||
long acceptTask = pendingTaskId(instanceId, acceptLeader.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + acceptTask + "/approve"),
|
||||
acceptLeader, acceptOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"可以协调\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("派单"));
|
||||
|
||||
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("dispatch");
|
||||
assertThat(data.queryMaster("SELECT current_node_type FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("DISPATCH");
|
||||
assertThat(pendingTaskIds(instanceId)).contains(acceptMember.userId);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'APPROVE'", instanceId)).isEqualTo(2);
|
||||
assertThat(data.queryMaster("SELECT opinion FROM approval_task WHERE id = ?", String.class, reportTask))
|
||||
.isEqualTo("同意协调");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSamePersonToHandleBothApprovalNodes() throws Exception {
|
||||
ApprovalTestData.TestUser both = data.createUser("双部门负责人" + data.uniqueSuffix(), reportOrgId);
|
||||
ApprovalTestData.TestRole reportRole = data.createRole("dept_leader_" + reportOrgId, "部门负责人");
|
||||
ApprovalTestData.TestRole acceptRole = data.createRole("dept_leader_" + acceptOrgId, "部门负责人");
|
||||
data.bind(both.userId, reportRole.roleId);
|
||||
data.bind(both.userId, acceptRole.roleId);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "同人连做", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
submitCoordination(instanceId, reporter, reportOrgId);
|
||||
|
||||
long firstTask = pendingTaskId(instanceId, both.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + firstTask + "/approve"), both, reportOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("受理部门负责人"));
|
||||
|
||||
long secondTask = pendingTaskId(instanceId, both.userId);
|
||||
assertThat(secondTask).isNotEqualTo(firstTask);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + secondTask + "/approve"), both, reportOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("派单"));
|
||||
|
||||
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
|
||||
String.class, instanceId)).isEqualTo("dispatch");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'APPROVE' AND operator_id = ?", instanceId, both.userId)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsThenResubmitsKeepingBizNo() throws Exception {
|
||||
ApprovalTestData.TestUser reportLeader = grantDepartmentLeader(reportOrgId, 1).get(0);
|
||||
grantDepartmentLeader(acceptOrgId, 1);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "驳回重提协调", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
submitCoordination(instanceId, reporter, reportOrgId);
|
||||
String bizNo = bizNo(instanceId);
|
||||
|
||||
long taskId = pendingTaskId(instanceId, reportLeader.userId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/reject"),
|
||||
reportLeader, reportOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\" \"}"))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("驳回意见")));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/reject"),
|
||||
reportLeader, reportOrgId, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"opinion\":\"协调范围不清\"}"))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("REJECTED"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("已驳回"))
|
||||
.andExpect(jsonPath("$.data.round").value(2));
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, reportOrgId, LIST))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(true))
|
||||
.andExpect(jsonPath("$.data.permissions.canSubmit").value(true))
|
||||
.andExpect(jsonPath("$.data.bizNo").value(bizNo));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.round").value(3))
|
||||
.andExpect(jsonPath("$.data.bizNo").value(bizNo))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("报送部门负责人"));
|
||||
|
||||
assertThat(pendingTaskId(instanceId, reportLeader.userId)).isPositive();
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'RESUBMIT' AND round = 3", instanceId)).isEqualTo(1);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
|
||||
+ "AND action = 'REJECT' AND opinion = '协调范围不清'", instanceId)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawsAndResubmitsBeforeAnyApproval() throws Exception {
|
||||
grantDepartmentLeader(reportOrgId, 1);
|
||||
grantDepartmentLeader(acceptOrgId, 1);
|
||||
grantArchivist();
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "撤回协调", "内容", null);
|
||||
uploadAttachment(instanceId, reporter, reportOrgId);
|
||||
submitCoordination(instanceId, reporter, reportOrgId);
|
||||
String bizNo = bizNo(instanceId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/withdraw"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("WITHDRAWN"))
|
||||
.andExpect(jsonPath("$.data.round").value(2))
|
||||
.andExpect(jsonPath("$.data.bizNo").value(bizNo));
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, reportOrgId, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.round").value(3))
|
||||
.andExpect(jsonPath("$.data.bizNo").value(bizNo))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("报送部门负责人"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hidesDraftFromOutsider() throws Exception {
|
||||
long instanceId = saveCoordination(reporter, reportOrgId, acceptOrgId, "草稿保密", "内容", null);
|
||||
ApprovalTestData.TestUser outsider = data.createUser("协调外人" + data.uniqueSuffix(), reportOrgId);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), outsider, reportOrgId, LIST))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权查看该单据")));
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private long saveCoordination(ApprovalTestData.TestUser user, Integer reportOrg, Integer acceptOrg,
|
||||
String title, String content, String remark) throws Exception {
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/coordination"),
|
||||
user, reportOrg, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(acceptOrg, title, content, remark, null)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
JsonNode node = objectMapper.readTree(response);
|
||||
long id = node.path("data").asLong();
|
||||
data.trackInstance(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
private void expectSaveFailure(String body, String expectedMessage) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/coordination"), reporter, reportOrgId, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(expectedMessage)));
|
||||
}
|
||||
|
||||
private void submitCoordination(long instanceId, ApprovalTestData.TestUser user, Integer org) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), user, org, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private void uploadAttachment(long instanceId, ApprovalTestData.TestUser user, Integer org) throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sign.png", "image/png",
|
||||
new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A});
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload")
|
||||
.file(file)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.param("category", "SIGNED")
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, org, TENANT, SUBMIT))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private List<ApprovalTestData.TestUser> grantDepartmentLeader(int orgId, int memberCount) {
|
||||
ApprovalTestData.TestRole role = data.createRole("dept_leader_" + orgId, "部门负责人");
|
||||
List<ApprovalTestData.TestUser> members = new ArrayList<>();
|
||||
for (int i = 0; i < memberCount; i++) {
|
||||
ApprovalTestData.TestUser member = data.createUser("协调负责人" + data.uniqueSuffix(), orgId);
|
||||
data.bind(member.userId, role.roleId);
|
||||
members.add(member);
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
private void grantArchivist() {
|
||||
ApprovalTestData.TestRole role = data.createRole("approval_archivist", "收文员");
|
||||
ApprovalTestData.TestUser member = data.createUser("协调收文员" + data.uniqueSuffix(), reportOrgId);
|
||||
data.bind(member.userId, role.roleId);
|
||||
}
|
||||
|
||||
private long pendingTaskId(long instanceId, int userId) {
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? AND status = 'PENDING' LIMIT 1", Long.class, instanceId, userId);
|
||||
assertThat(taskId).as("待办任务应存在").isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private List<Integer> pendingTaskIds(long instanceId) {
|
||||
return data.master().queryForList("SELECT assignee_id FROM approval_task "
|
||||
+ "WHERE instance_id = ? AND status = 'PENDING' ORDER BY id", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder,
|
||||
ApprovalTestData.TestUser user, Integer org,
|
||||
String... authorities) {
|
||||
return builder.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, org, TENANT, authorities)));
|
||||
}
|
||||
|
||||
private String body(Integer acceptOrgId, String title, String content, String remark, Long id) {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (id != null) {
|
||||
sb.append("\"id\":").append(id).append(',');
|
||||
}
|
||||
sb.append("\"acceptOrgId\":").append(acceptOrgId == null ? "null" : acceptOrgId).append(',')
|
||||
.append("\"title\":").append(quote(title)).append(',')
|
||||
.append("\"content\":").append(quote(content)).append(',')
|
||||
.append("\"remark\":").append(remark == null ? "null" : quote(remark))
|
||||
.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String quote(String value) {
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
|
||||
private String repeat(String value, int count) {
|
||||
return value.repeat(count);
|
||||
}
|
||||
|
||||
private String instanceStatus(long instanceId) {
|
||||
return data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String bizNo(long instanceId) {
|
||||
return data.queryMaster("SELECT biz_no FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private String todayFromDatabase() {
|
||||
return data.queryMaster("SELECT DATE_FORMAT(CURDATE(), '%Y%m%d')", String.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user