feat(approval): 派单候选池、派单动作与双方存档 (#24)

This commit is contained in:
2026-10-30 21:42:04 +08:00
parent d395abb6f0
commit 92a982054b
7 changed files with 685 additions and 3 deletions
@@ -0,0 +1,486 @@
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.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
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;
/**
* 派单闭环集成测试:候选池、回退过滤、派单并发、被派单人可见性、双方存档缺一不可、
* 归档与 PC 派单权限。
*
* <p>独立租户隔离角色与用户,避免与并行执行的其它审批 IT 互相污染。</p>
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles({"gxtyzx-test", "test"})
class ApprovalDispatchIT {
private static final int TENANT = 10025;
private static final String LIST = "approval:instance:list";
private static final String SUBMIT = "approval:instance:submit";
private static final String APPROVE = "approval:instance:approve";
private static final String ARCHIVE = "approval:instance:archive";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private DataSource dataSource;
private ApprovalTestData data;
private Integer reportOrgId;
private Integer acceptOrgId;
private ApprovalTestData.TestUser reporter;
private ApprovalTestData.TestUser reportLeader;
private ApprovalTestData.TestUser acceptLeader;
private ApprovalTestData.TestUser acceptMember;
private ApprovalTestData.TestUser archivist;
private ApprovalTestData.TestUser outsider;
private ApprovalTestData.TestUser staffNoOrg;
private ApprovalTestData.TestUser consumerUser;
private ApprovalTestData.TestUser guestUser;
private ApprovalTestData.TestUser noRoleUser;
@BeforeEach
void setUp() {
data = new ApprovalTestData(dataSource);
reportOrgId = data.createOrganization("派单报送部门" + data.uniqueSuffix(), 136, TENANT);
acceptOrgId = data.createOrganization("派单受理部门" + data.uniqueSuffix(), 136, TENANT);
reporter = data.createUser("派单报送人" + data.uniqueSuffix(), reportOrgId, TENANT);
ApprovalTestData.TestRole reportLeaderRole =
data.createRole("dept_leader_" + reportOrgId, "报送部门负责人", TENANT);
reportLeader = data.createUser("派单报送负责人" + data.uniqueSuffix(), reportOrgId, TENANT);
data.bindTenant(reportLeader.userId, reportLeaderRole.roleId, TENANT);
ApprovalTestData.TestRole acceptLeaderRole =
data.createRole("dept_leader_" + acceptOrgId, "受理部门负责人", TENANT);
acceptLeader = data.createUser("派单受理负责人" + data.uniqueSuffix(), acceptOrgId, TENANT);
data.bindTenant(acceptLeader.userId, acceptLeaderRole.roleId, TENANT);
acceptMember = data.createUser("派单受理成员" + data.uniqueSuffix(), acceptOrgId, TENANT);
ApprovalTestData.TestRole archivistRole = data.createRole("approval_archivist", "收文员", TENANT);
archivist = data.createUser("派单收文员" + data.uniqueSuffix(), reportOrgId, TENANT);
data.bindTenant(archivist.userId, archivistRole.roleId, TENANT);
outsider = data.createUser("派单路人" + data.uniqueSuffix(), reportOrgId, TENANT);
// 回退候选:无部门但持有业务角色应保留,仅有 user/guest 角色或无角色的普通消费用户被排除
ApprovalTestData.TestRole staffRole = data.createRole("backoffice_staff", "业务管理员", TENANT);
staffNoOrg = data.createUser("派单无部门业务员" + data.uniqueSuffix(), null, TENANT);
data.bindTenant(staffNoOrg.userId, staffRole.roleId, TENANT);
ApprovalTestData.TestRole userRole = data.createRole("user", "普通用户", TENANT);
consumerUser = data.createUser("派单消费者" + data.uniqueSuffix(), null, TENANT);
data.bindTenant(consumerUser.userId, userRole.roleId, TENANT);
ApprovalTestData.TestRole guestRole = data.createRole("guest", "访客", TENANT);
guestUser = data.createUser("派单访客" + data.uniqueSuffix(), null, TENANT);
data.bindTenant(guestUser.userId, guestRole.roleId, TENANT);
// 无角色且无部门
noRoleUser = data.createUser("派单无角色" + data.uniqueSuffix(), null, TENANT);
}
@AfterEach
void tearDown() {
data.cleanup();
}
@Test
void listsOnlyEnabledAcceptOrgMembers() throws Exception {
long instanceId = reachDispatch("部门候选池");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
// 部门内停用成员不进入候选池
ApprovalTestData.TestUser disabled =
data.createUser("派单停用成员" + data.uniqueSuffix(), acceptOrgId, TENANT);
data.server().update("UPDATE sys_user SET status = 1 WHERE user_id = ?", disabled.userId);
String response = mockMvc.perform(authed(get("/api/approval/task/" + dispatchTask + "/candidates"),
acceptLeader, acceptOrgId, APPROVE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.length()").value(2))
.andExpect(jsonPath("$.data[0].userId").value(acceptLeader.userId))
.andExpect(jsonPath("$.data[0].realName").value(acceptLeader.realName))
.andExpect(jsonPath("$.data[0].username").value(acceptLeader.username))
.andExpect(jsonPath("$.data[0].organizationName").value(
data.server().queryForObject("SELECT organization_name FROM sys_organization WHERE organization_id = ?",
String.class, acceptOrgId)))
.andExpect(jsonPath("$.data[1].userId").value(acceptMember.userId))
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
// 部门有成员时不得混入回退候选
JsonNode candidates = objectMapper.readTree(response).path("data");
for (JsonNode candidate : candidates) {
assertThat(candidate.path("userId").asInt())
.isNotEqualTo(staffNoOrg.userId)
.isNotEqualTo(disabled.userId);
}
}
@Test
void fallsBackToTenantUsersExcludingConsumers() throws Exception {
long instanceId = reachDispatch("回退候选池");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
// 受理部门成员全部调离后部门为空,派单待办仍在负责人手中
data.setUserOrganization(acceptLeader.userId, reportOrgId);
data.setUserOrganization(acceptMember.userId, reportOrgId);
String response = mockMvc.perform(authed(get("/api/approval/task/" + dispatchTask + "/candidates"),
acceptLeader, reportOrgId, APPROVE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
JsonNode candidates = objectMapper.readTree(response).path("data");
List<Integer> ids = new java.util.ArrayList<>();
candidates.forEach(node -> ids.add(node.path("userId").asInt()));
// 无部门但持有业务角色的账号保留(含调离受理部门后的成员)
assertThat(ids).contains(staffNoOrg.userId, reporter.userId, acceptLeader.userId, acceptMember.userId);
// 无部门且角色仅有 user/guest、或无角色的普通消费用户被排除
assertThat(ids).doesNotContain(consumerUser.userId, guestUser.userId, noRoleUser.userId);
}
@Test
void dispatchesToSelectedCandidateAndCreatesBothConfirmTasks() throws Exception {
long instanceId = reachDispatch("派单存档");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
long otherDispatchTask = pendingTaskId(instanceId, acceptMember.userId);
mockMvc.perform(authed(post("/api/approval/task/" + dispatchTask + "/dispatch"),
acceptLeader, acceptOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + acceptMember.userId + "}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.status").value("PENDING"))
.andExpect(jsonPath("$.data.currentNodeName").value("存档"))
.andExpect(jsonPath("$.data.dispatchTargetId").value(acceptMember.userId))
.andExpect(jsonPath("$.data.dispatchTargetName").value(acceptMember.realName))
.andExpect(jsonPath("$.data.myTask").doesNotExist());
// 派单任务完成并快照派单对象,同节点其余派单待办取消
assertThat(data.queryMaster("SELECT status FROM approval_task WHERE id = ?", String.class, dispatchTask))
.isEqualTo("DONE");
assertThat(data.queryMaster("SELECT dispatch_target_id FROM approval_task WHERE id = ?",
Integer.class, dispatchTask)).isEqualTo(acceptMember.userId);
assertThat(data.queryMaster("SELECT status FROM approval_task WHERE id = ?", String.class, otherDispatchTask))
.isEqualTo("CANCELLED");
assertThat(data.queryMaster("SELECT dispatch_target_id FROM approval_instance WHERE id = ?",
Integer.class, instanceId)).isEqualTo(acceptMember.userId);
assertThat(data.queryMaster("SELECT dispatch_target_name FROM approval_instance WHERE id = ?",
String.class, instanceId)).isEqualTo(acceptMember.realName);
assertThat(data.queryMaster("SELECT current_node_key FROM approval_instance WHERE id = ?",
String.class, instanceId)).isEqualTo("confirm");
// 派单记录 + 报送人与被派单人的两条存档待办
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
+ "AND action = 'DISPATCH' AND operator_id = ?", instanceId, acceptLeader.userId)).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND node_type = 'CONFIRM' AND status = 'PENDING'", instanceId)).isEqualTo(2);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND assignee_id = ? AND status = 'PENDING'", instanceId, reporter.userId)).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND assignee_id = ? AND status = 'PENDING'", instanceId, acceptMember.userId)).isEqualTo(1);
// 被派单人不产生审批任务
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND assignee_id = ? AND node_type = 'APPROVE'", instanceId, acceptMember.userId)).isZero();
// 被派单人立即可见:详情、附件与待办
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), acceptMember, acceptOrgId, LIST))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.dispatchTargetId").value(acceptMember.userId))
.andExpect(jsonPath("$.data.attachments.length()").value(1))
.andExpect(jsonPath("$.data.myTask.nodeType").value("CONFIRM"))
.andExpect(jsonPath("$.data.permissions.canConfirm").value(true));
mockMvc.perform(authed(get("/api/approval/todo/page"), acceptMember, acceptOrgId, LIST))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.count").value(1))
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId));
mockMvc.perform(authed(get("/api/approval/todo/count"), acceptMember, acceptOrgId, LIST))
.andExpect(jsonPath("$.data").value(1));
// 重复派单返回「任务已处理」
mockMvc.perform(authed(post("/api/approval/task/" + dispatchTask + "/dispatch"),
acceptLeader, acceptOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + acceptLeader.userId + "}"))
.andExpect(jsonPath("$.code").value(1))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("任务已处理")));
}
@Test
void rejectsTargetOutsideCandidatePool() throws Exception {
long instanceId = reachDispatch("越界派单");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
mockMvc.perform(authed(post("/api/approval/task/" + dispatchTask + "/dispatch"),
acceptLeader, acceptOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + reporter.userId + "}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(1))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("派单对象不在候选范围内")));
mockMvc.perform(authed(post("/api/approval/task/" + dispatchTask + "/dispatch"),
acceptLeader, acceptOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(jsonPath("$.code").value(1))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("请选择派单对象")));
assertThat(data.queryMaster("SELECT status FROM approval_task WHERE id = ?", String.class, dispatchTask))
.isEqualTo("PENDING");
assertThat(data.queryMaster("SELECT dispatch_target_id FROM approval_instance WHERE id = ?",
Integer.class, instanceId)).isNull();
}
@Test
void concurrentDispatchOnlySucceedsOnce() throws Exception {
long instanceId = reachDispatch("并发派单");
long leaderTask = pendingTaskId(instanceId, acceptLeader.userId);
long memberTask = pendingTaskId(instanceId, acceptMember.userId);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(2);
ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger success = new AtomicInteger();
AtomicInteger conflict = new AtomicInteger();
try {
dispatchAsync(executor, start, done, leaderTask, acceptLeader, acceptOrgId,
acceptMember.userId, success, conflict);
dispatchAsync(executor, start, done, memberTask, acceptMember, acceptOrgId,
acceptLeader.userId, success, conflict);
start.countDown();
assertThat(done.await(30, TimeUnit.SECONDS)).isTrue();
} finally {
executor.shutdownNow();
}
assertThat(success.get()).isEqualTo(1);
assertThat(conflict.get()).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
+ "AND action = 'DISPATCH'", instanceId)).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND node_type = 'DISPATCH' AND status = 'DONE'", instanceId)).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND node_type = 'DISPATCH' AND status = 'CANCELLED'", instanceId)).isEqualTo(1);
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
+ "AND node_type = 'CONFIRM' AND status = 'PENDING'", instanceId)).isEqualTo(2);
}
@Test
void requiresBothConfirmsBeforeArchive() throws Exception {
long instanceId = reachDispatch("双方存档");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
dispatch(dispatchTask, acceptLeader, acceptOrgId, acceptMember.userId);
// 只有报送人存档:实例仍审批中
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, reporter.userId) + "/confirm"),
reporter, reportOrgId, APPROVE))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.status").value("PENDING"))
.andExpect(jsonPath("$.data.currentNodeName").value("存档"))
.andExpect(jsonPath("$.data.confirms[0].status").value("DONE"))
.andExpect(jsonPath("$.data.confirms[1].status").value("PENDING"));
assertThat(data.queryMaster("SELECT approve_time FROM approval_instance WHERE id = ?",
Object.class, instanceId)).isNull();
// 被派单人存档后双方齐全 → 审批完成并生成归档任务
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, acceptMember.userId) + "/confirm"),
acceptMember, acceptOrgId, APPROVE))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.status").value("APPROVED"))
.andExpect(jsonPath("$.data.statusName").value("审批完成"))
.andExpect(jsonPath("$.data.approveTime").isNotEmpty())
.andExpect(jsonPath("$.data.currentNodeName").value("归档"));
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? "
+ "AND action = 'CONFIRM'", instanceId)).isEqualTo(2);
long archiveTask = pendingTaskId(instanceId, archivist.userId);
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/archive"), archivist, reportOrgId, ARCHIVE))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
.andExpect(jsonPath("$.data.statusName").value("已归档"))
.andExpect(jsonPath("$.data.archiveTime").isNotEmpty())
.andExpect(jsonPath("$.data.archivistId").value(archivist.userId))
.andExpect(jsonPath("$.data.permissions.canEdit").value(false));
// 归档后只读
mockMvc.perform(authed(post("/api/approval/task/" + archiveTask + "/dispatch"),
archivist, reportOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + acceptMember.userId + "}"))
.andExpect(jsonPath("$.code").value(1));
}
@Test
void rejectsCandidatesAndDispatchForNonDispatchTaskOrOutsider() throws Exception {
long instanceId = reachDispatch("派单校验");
long dispatchTask = pendingTaskId(instanceId, acceptLeader.userId);
// 非派单节点任务
long reportTask = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
+ "AND node_type = 'APPROVE' AND assignee_id = ? LIMIT 1", Long.class, instanceId, reportLeader.userId);
mockMvc.perform(authed(get("/api/approval/task/" + reportTask + "/candidates"), reportLeader, reportOrgId, APPROVE))
.andExpect(jsonPath("$.code").value(1))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("当前任务不支持派单")));
// 他人任务
mockMvc.perform(authed(get("/api/approval/task/" + dispatchTask + "/candidates"), reporter, reportOrgId, APPROVE))
.andExpect(jsonPath("$.code").value(1))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权处理该任务")));
// PC 端与员工端使用同一权限串:缺少 approve 一律 403
mockMvc.perform(authed(get("/api/approval/task/" + dispatchTask + "/candidates"),
acceptLeader, acceptOrgId, LIST))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(403));
mockMvc.perform(authed(post("/api/approval/task/" + dispatchTask + "/dispatch"),
acceptLeader, acceptOrgId, LIST)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + acceptMember.userId + "}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(403));
}
// ---------- helpers ----------
private void dispatchAsync(ExecutorService executor, CountDownLatch start, CountDownLatch done,
long taskId, ApprovalTestData.TestUser actor, Integer org,
int targetUserId, AtomicInteger success, AtomicInteger conflict) {
executor.submit(() -> {
try {
start.await();
boolean ok = mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/dispatch"),
actor, org, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + targetUserId + "}"))
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8)
.contains("\"code\":0");
if (ok) {
success.incrementAndGet();
} else {
conflict.incrementAndGet();
}
} catch (Exception e) {
conflict.incrementAndGet();
} finally {
done.countDown();
}
});
}
private void dispatch(long taskId, ApprovalTestData.TestUser actor, Integer org, int targetUserId)
throws Exception {
mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/dispatch"), actor, org, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"targetUserId\":" + targetUserId + "}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
/**
* 创建并推进内部协调单到派单节点:提交 → 报送负责人通过 → 受理负责人通过。
*/
private long reachDispatch(String title) throws Exception {
String body = "{\"acceptOrgId\":" + acceptOrgId
+ ",\"title\":" + quote(title) + ",\"content\":\"协调内容\"}";
String response = mockMvc.perform(authed(post("/api/approval/instance/coordination"),
reporter, reportOrgId, SUBMIT)
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
long instanceId = objectMapper.readTree(response).path("data").asLong();
data.trackInstance(instanceId);
uploadAttachment(instanceId);
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
reporter, reportOrgId, SUBMIT))
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, reportLeader.userId) + "/approve"),
reportLeader, reportOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"opinion\":\"同意协调\"}"))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.currentNodeName").value("受理部门负责人"));
mockMvc.perform(authed(post("/api/approval/task/" + pendingTaskId(instanceId, acceptLeader.userId) + "/approve"),
acceptLeader, acceptOrgId, APPROVE)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"opinion\":\"同意派单\"}"))
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.currentNodeName").value("派单"))
.andExpect(jsonPath("$.data.permissions.canDispatch").value(true));
return instanceId;
}
private void uploadAttachment(long instanceId) 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(reporter.userId, reporter.username,
reportOrgId, TENANT, SUBMIT))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
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 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 quote(String value) {
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
}