feat(approval): 业务完成确认单草稿、附件、提交与引擎起步 (#19)
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.delete;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 业务完成确认单:草稿、附件、提交预检与引擎起步的集成测试。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
@Transactional
|
||||
class ApprovalCompletionDraftIT {
|
||||
|
||||
private static final int TENANT = TestAuthentication.TENANT_ID;
|
||||
private static final String LIST = "approval:instance:list";
|
||||
private static final String SUBMIT = "approval:instance:submit";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private Integer organizationId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
organizationId = data.createOrganization("测试部门" + data.uniqueSuffix(), 136);
|
||||
reporter = data.createUser("报送人" + data.uniqueSuffix(), organizationId);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsDraftWithoutBizNoAndListsAsUnsubmitted() throws Exception {
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "场馆维修");
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
assertThat(bizNo(instanceId)).isNull();
|
||||
assertThat(data.queryMaster("SELECT reporter_id FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isEqualTo(reporter.userId);
|
||||
assertThat(data.queryMaster("SELECT report_org_id FROM approval_instance WHERE id = ?", Integer.class, instanceId))
|
||||
.isEqualTo(organizationId);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, organizationId, TENANT, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.statusName").value("未提交"))
|
||||
.andExpect(jsonPath("$.data.name").value("场馆维修"))
|
||||
.andExpect(jsonPath("$.data.docTypeName").value("业务完成确认单"))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(true))
|
||||
.andExpect(jsonPath("$.data.bizNo").doesNotExist());
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page"), reporter, organizationId, TENANT, LIST))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) instanceId))
|
||||
.andExpect(jsonPath("$.data.list[0].statusName").value("未提交"))
|
||||
.andExpect(jsonPath("$.data.list[0].title").value("场馆维修"))
|
||||
.andExpect(jsonPath("$.data.list[0].bizNo").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesCompletionFields() throws Exception {
|
||||
expectSaveFailure(body("", "实施单位", "10.00", "服务内容"), "名称不能为空");
|
||||
expectSaveFailure(body(repeat("名", 101), "实施单位", "10.00", "服务内容"), "名称不能超过100字");
|
||||
expectSaveFailure(body("名称", "", "10.00", "服务内容"), "实施单位不能为空");
|
||||
expectSaveFailure(body("名称", "实施单位", "-1", "服务内容"), "费用必须为0到99999999.99之间的两位小数");
|
||||
expectSaveFailure(body("名称", "实施单位", "100000000", "服务内容"), "费用必须为0到99999999.99之间的两位小数");
|
||||
expectSaveFailure(body("名称", "实施单位", "1.234", "服务内容"), "费用必须为0到99999999.99之间的两位小数");
|
||||
expectSaveFailure(body("名称", "实施单位", "10.00", ""), "服务内容不能为空");
|
||||
expectSaveFailure(body("名称", "实施单位", "10.00", repeat("内", 2001)), "服务内容不能超过2000字");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenReporterHasNoOrganization() throws Exception {
|
||||
ApprovalTestData.TestUser noOrgReporter = data.createUser("无部门" + data.uniqueSuffix(), null);
|
||||
long instanceId = createCompletion(noOrgReporter, null, TENANT, "无部门单据");
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
noOrgReporter, null, TENANT, 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 rejectsSubmitWhenDepartmentLeaderRoleMissing() throws Exception {
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "缺负责人");
|
||||
uploadAttachment(instanceId, reporter, organizationId, "image/png");
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("报送部门负责人角色缺配")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenArchivistRoleMissing() throws Exception {
|
||||
grantDepartmentLeader(organizationId, TENANT);
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "缺收文员");
|
||||
uploadAttachment(instanceId, reporter, organizationId, "image/png");
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("收文员角色缺配")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSubmitWhenAttachmentMissing() throws Exception {
|
||||
grantDepartmentLeader(organizationId, TENANT);
|
||||
grantArchivist(TENANT);
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "无附件");
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("请至少上传一张附件")));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("DRAFT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitsAndStartsDepartmentLeaderCountersign() throws Exception {
|
||||
List<ApprovalTestData.TestUser> leaders = grantDepartmentLeader(organizationId, TENANT, 2);
|
||||
grantArchivist(TENANT);
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "完工确认");
|
||||
long attachmentId = uploadAttachment(instanceId, reporter, organizationId, "image/png");
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.data.statusName").value("审批中"))
|
||||
.andExpect(jsonPath("$.data.currentNodeName").value("报送部门负责人"))
|
||||
.andExpect(jsonPath("$.data.permissions.canEdit").value(false))
|
||||
.andExpect(jsonPath("$.data.permissions.canSubmit").value(false))
|
||||
.andExpect(jsonPath("$.data.attachments.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.records[0].action").value("SUBMIT"))
|
||||
.andExpect(jsonPath("$.data.records[0].actionName").value("提交"));
|
||||
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("PENDING");
|
||||
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("deptLeader");
|
||||
assertThat(data.queryMaster("SELECT current_node_type FROM approval_instance WHERE id = ?", String.class, instanceId))
|
||||
.isEqualTo("APPROVE");
|
||||
String bizNo = bizNo(instanceId);
|
||||
assertThat(bizNo).matches("WG\\d{8}-\\d{4,}");
|
||||
assertThat(bizNo.substring(2, 10)).isEqualTo(todayFromDatabase());
|
||||
|
||||
List<Integer> assignees = data.master().queryForList(
|
||||
"SELECT assignee_id FROM approval_task WHERE instance_id = ? AND status = 'PENDING' ORDER BY id",
|
||||
Integer.class, instanceId);
|
||||
assertThat(assignees).containsExactlyInAnyOrder(leaders.get(0).userId, leaders.get(1).userId);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_record WHERE instance_id = ? AND action = 'SUBMIT'",
|
||||
instanceId)).isEqualTo(1);
|
||||
|
||||
// 提交后字段与附件均只读
|
||||
mockMvc.perform(authed(post("/api/approval/instance/completion"), reporter, organizationId, TENANT, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body("改名", "实施单位", "10.00", "服务内容", instanceId)))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不可修改")));
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "locked.png", "image/png", pngBytes());
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload")
|
||||
.file(file)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.param("category", "OTHER")
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不可修改")));
|
||||
|
||||
// 重复提交保持审批中
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"),
|
||||
reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(jsonPath("$.code").value(1));
|
||||
assertThat(instanceStatus(instanceId)).isEqualTo("PENDING");
|
||||
assertThat(attachmentId).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocatesUniqueDailySequenceByTenant() throws Exception {
|
||||
grantDepartmentLeader(organizationId, TENANT);
|
||||
grantArchivist(TENANT);
|
||||
|
||||
long first = submitCompletion(createCompletion(reporter, organizationId, TENANT, "单号一"));
|
||||
long second = submitCompletion(createCompletion(reporter, organizationId, TENANT, "单号二"));
|
||||
|
||||
String firstNo = bizNo(first);
|
||||
String secondNo = bizNo(second);
|
||||
String date = todayFromDatabase();
|
||||
assertThat(firstNo).matches("WG" + date + "-\\d{4,}");
|
||||
assertThat(secondNo).matches("WG" + date + "-\\d{4,}");
|
||||
int firstSeq = Integer.parseInt(firstNo.substring(firstNo.indexOf('-') + 1));
|
||||
int secondSeq = Integer.parseInt(secondNo.substring(secondNo.indexOf('-') + 1));
|
||||
assertThat(secondSeq).isEqualTo(firstSeq + 1);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_instance WHERE tenant_id = ? AND biz_no = ?",
|
||||
TENANT, firstNo)).isEqualTo(1);
|
||||
|
||||
// 另一租户使用同一天同一前缀独立取号,互不冲突
|
||||
int otherTenant = 10023;
|
||||
Integer otherOrg = data.createOrganization("外租户部门" + data.uniqueSuffix(), 136, otherTenant);
|
||||
ApprovalTestData.TestUser otherReporter = data.createUser("外租户报送人" + data.uniqueSuffix(), otherOrg, otherTenant);
|
||||
ApprovalTestData.TestRole otherLeaderRole = data.createRole("dept_leader_" + otherOrg, "外租户负责人", otherTenant);
|
||||
ApprovalTestData.TestUser otherLeader = data.createUser("外租户负责人" + data.uniqueSuffix(), otherOrg, otherTenant);
|
||||
data.bindTenant(otherLeader.userId, otherLeaderRole.roleId, otherTenant);
|
||||
ApprovalTestData.TestRole otherArchivistRole = data.createRole("approval_archivist", "外租户收文员", otherTenant);
|
||||
ApprovalTestData.TestUser otherArchivist = data.createUser("外租户收文员" + data.uniqueSuffix(), otherOrg, otherTenant);
|
||||
data.bindTenant(otherArchivist.userId, otherArchivistRole.roleId, otherTenant);
|
||||
|
||||
long otherInstance = submitCompletionFor(createCompletion(otherReporter, otherOrg, otherTenant, "外租户单据"),
|
||||
otherReporter, otherOrg, otherTenant);
|
||||
String otherNo = bizNo(otherInstance);
|
||||
assertThat(otherNo).matches("WG" + date + "-\\d{4,}");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_instance WHERE tenant_id = ? AND biz_no = ?",
|
||||
otherTenant, otherNo)).isEqualTo(1);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_instance WHERE tenant_id = ? AND biz_no = ?",
|
||||
TENANT, firstNo)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void enforcesAttachmentRules() throws Exception {
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "附件规则");
|
||||
|
||||
MockMultipartFile text = new MockMultipartFile("file", "evil.txt", "text/plain",
|
||||
"x".getBytes(StandardCharsets.UTF_8));
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload").file(text)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不支持")));
|
||||
|
||||
MockMultipartFile mismatch = new MockMultipartFile("file", "fake.jpg", "application/pdf", pngBytes());
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload").file(mismatch)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("不支持")));
|
||||
|
||||
MockMultipartFile tooLarge = new MockMultipartFile("file", "big.png", "image/png",
|
||||
new byte[20 * 1024 * 1024 + 1]);
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload").file(tooLarge)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("20MB")));
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
uploadAttachment(instanceId, reporter, organizationId, "image/png");
|
||||
}
|
||||
MockMultipartFile tenth = new MockMultipartFile("file", "tenth.png", "image/png", pngBytes());
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload").file(tenth)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username, organizationId, TENANT, SUBMIT))))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("9张")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletesAttachmentLogicallyAndKeepsFileRecord() throws Exception {
|
||||
long instanceId = createCompletion(reporter, organizationId, TENANT, "删除附件");
|
||||
long attachmentId = uploadAttachment(instanceId, reporter, organizationId, "image/png");
|
||||
Integer fileId = data.queryMaster("SELECT file_id FROM approval_attachment WHERE id = ?", Integer.class, attachmentId);
|
||||
|
||||
mockMvc.perform(authed(delete("/api/approval/attachment/" + attachmentId), reporter, organizationId, TENANT, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
assertThat(data.queryMaster("SELECT deleted FROM approval_attachment WHERE id = ?", Integer.class, attachmentId))
|
||||
.isEqualTo(1);
|
||||
assertThat(data.countServer("SELECT COUNT(*) FROM sys_file_record WHERE id = ? AND deleted = 0", fileId))
|
||||
.isEqualTo(1);
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + instanceId), reporter, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.data.attachments.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mineListIsOrderedAndScoped() throws Exception {
|
||||
grantDepartmentLeader(organizationId, TENANT);
|
||||
grantArchivist(TENANT);
|
||||
long first = submitCompletion(createCompletion(reporter, organizationId, TENANT, "先提交"));
|
||||
long second = submitCompletion(createCompletion(reporter, organizationId, TENANT, "后提交"));
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page"), reporter, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.count").value(2))
|
||||
.andExpect(jsonPath("$.data.list[0].id").value((int) second))
|
||||
.andExpect(jsonPath("$.data.list[1].id").value((int) first))
|
||||
.andExpect(jsonPath("$.data.list[0].statusName").value("审批中"))
|
||||
.andExpect(jsonPath("$.data.list[0].bizNo").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.list[0].currentNodeName").value("报送部门负责人"));
|
||||
|
||||
ApprovalTestData.TestUser outsider = data.createUser("路人" + data.uniqueSuffix(), organizationId);
|
||||
mockMvc.perform(authed(get("/api/approval/mine/page"), outsider, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.data.count").value(0));
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + first), outsider, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权查看该单据")));
|
||||
|
||||
ApprovalTestData.TestUser archivist = grantArchivist(TENANT);
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + first), archivist, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value((int) first));
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftStaysInvisibleEvenToAllVisibleArchivist() throws Exception {
|
||||
ApprovalTestData.TestUser archivist = grantArchivist(TENANT);
|
||||
long draft = createCompletion(reporter, organizationId, TENANT, "草稿保密");
|
||||
|
||||
mockMvc.perform(authed(get("/api/approval/instance/" + draft), archivist, organizationId, TENANT, LIST))
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("无权查看该单据")));
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
private void expectSaveFailure(String body, String expectedMessage) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/completion"), reporter, organizationId, TENANT, 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 long createCompletion(ApprovalTestData.TestUser user, Integer org, int tenant, String name) throws Exception {
|
||||
String body = body(name, "广西体育中心实施部", "123.45", "完成场馆设施维修与验收");
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/completion"), user, org, tenant, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.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 long submitCompletion(long instanceId) throws Exception {
|
||||
return submitCompletionFor(instanceId, reporterFor(instanceId), organizationIdFor(instanceId), TENANT);
|
||||
}
|
||||
|
||||
private long submitCompletionFor(long instanceId, ApprovalTestData.TestUser user, Integer org, int tenant)
|
||||
throws Exception {
|
||||
uploadAttachment(instanceId, user, org, tenant, "image/png");
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), user, org, tenant, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private long uploadAttachment(long instanceId, ApprovalTestData.TestUser user, Integer org,
|
||||
String contentType) throws Exception {
|
||||
return uploadAttachment(instanceId, user, org, TENANT, contentType);
|
||||
}
|
||||
|
||||
private long uploadAttachment(long instanceId, ApprovalTestData.TestUser user, Integer org, int tenant,
|
||||
String contentType) throws Exception {
|
||||
String filename = "sign." + extensionOf(contentType);
|
||||
MockMultipartFile file = new MockMultipartFile("file", filename, contentType, pngBytes());
|
||||
String response = 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))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
return objectMapper.readTree(response).path("data").path("id").asLong();
|
||||
}
|
||||
|
||||
private String extensionOf(String contentType) {
|
||||
switch (contentType) {
|
||||
case "image/png":
|
||||
return "png";
|
||||
case "image/jpeg":
|
||||
return "jpg";
|
||||
default:
|
||||
return "bin";
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] pngBytes() {
|
||||
return new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder, ApprovalTestData.TestUser user,
|
||||
Integer org, int tenant, String... authorities) {
|
||||
return builder.header("tenantId", tenant)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, org, tenant, authorities)));
|
||||
}
|
||||
|
||||
private ApprovalTestData.TestUser reporterFor(long instanceId) {
|
||||
Integer reporterId = data.queryMaster("SELECT reporter_id FROM approval_instance WHERE id = ?",
|
||||
Integer.class, instanceId);
|
||||
return new ApprovalTestData.TestUser(reporterId, "reporter-" + reporterId, "报送人");
|
||||
}
|
||||
|
||||
private Integer organizationIdFor(long instanceId) {
|
||||
return data.queryMaster("SELECT report_org_id FROM approval_instance WHERE id = ?", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private List<ApprovalTestData.TestUser> grantDepartmentLeader(int orgId, int tenant) {
|
||||
return grantDepartmentLeader(orgId, tenant, 1);
|
||||
}
|
||||
|
||||
private List<ApprovalTestData.TestUser> grantDepartmentLeader(int orgId, int tenant, int memberCount) {
|
||||
ApprovalTestData.TestRole role = data.createRole("dept_leader_" + orgId, "部门负责人", tenant);
|
||||
List<ApprovalTestData.TestUser> members = new ArrayList<>();
|
||||
for (int i = 0; i < memberCount; i++) {
|
||||
ApprovalTestData.TestUser member = data.createUser("负责人" + data.uniqueSuffix(), orgId, tenant);
|
||||
data.bindTenant(member.userId, role.roleId, tenant);
|
||||
members.add(member);
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
private ApprovalTestData.TestUser grantArchivist(int tenant) {
|
||||
ApprovalTestData.TestRole role = data.createRole("approval_archivist", "收文员", tenant);
|
||||
ApprovalTestData.TestUser member = data.createUser("收文员" + data.uniqueSuffix(), organizationId, tenant);
|
||||
data.bindTenant(member.userId, role.roleId, tenant);
|
||||
return member;
|
||||
}
|
||||
|
||||
private String body(String name, String implementOrg, String fee, String serviceContent) {
|
||||
return body(name, implementOrg, fee, serviceContent, null);
|
||||
}
|
||||
|
||||
private String body(String name, String implementOrg, String fee, String serviceContent, Long id) {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (id != null) {
|
||||
sb.append("\"id\":").append(id).append(',');
|
||||
}
|
||||
sb.append("\"name\":").append(quote(name)).append(',')
|
||||
.append("\"implementOrg\":").append(quote(implementOrg)).append(',')
|
||||
.append("\"fee\":").append(fee).append(',')
|
||||
.append("\"serviceContent\":").append(quote(serviceContent))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.gxwebsoft.support;
|
||||
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
import org.springframework.jdbc.support.KeyHolder;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 审批集成测试夹具:负责在共享测试库中创建并清理用户/角色/组织与审批实例。
|
||||
*
|
||||
* <p>sys_* 位于 server 库、审批表位于 master 库,分别使用独立 JdbcTemplate,
|
||||
* 避免测试事务与动态数据源切换互相影响。</p>
|
||||
*/
|
||||
public class ApprovalTestData {
|
||||
|
||||
private static final AtomicInteger SEQ = new AtomicInteger();
|
||||
|
||||
private final JdbcTemplate master;
|
||||
private final JdbcTemplate server;
|
||||
|
||||
private final List<Integer> userIds = new ArrayList<>();
|
||||
private final List<Integer> roleIds = new ArrayList<>();
|
||||
private final List<Integer> organizationIds = new ArrayList<>();
|
||||
private final List<Long> instanceIds = new ArrayList<>();
|
||||
|
||||
public ApprovalTestData(DataSource dataSource) {
|
||||
this.master = new JdbcTemplate(dataSource);
|
||||
DataSource serverDataSource = ((DynamicRoutingDataSource) dataSource).getDataSource("server");
|
||||
this.server = new JdbcTemplate(serverDataSource);
|
||||
}
|
||||
|
||||
/** 唯一后缀,避免共享库中的账号/角色/组织重名。 */
|
||||
public String uniqueSuffix() {
|
||||
return System.nanoTime() + "-" + SEQ.incrementAndGet();
|
||||
}
|
||||
|
||||
public TestUser createUser(String realName, Integer organizationId, int tenantId) {
|
||||
String suffix = uniqueSuffix();
|
||||
String username = "it_" + suffix;
|
||||
Integer id = insert(server,
|
||||
"INSERT INTO sys_user (username, password, nickname, real_name, organization_id, status, deleted, tenant_id) "
|
||||
+ "VALUES (?, 'it', ?, ?, ?, 0, 0, ?)",
|
||||
username, realName, realName, organizationId, tenantId);
|
||||
userIds.add(id);
|
||||
return new TestUser(id, username, realName);
|
||||
}
|
||||
|
||||
public TestUser createUser(String realName, Integer organizationId) {
|
||||
return createUser(realName, organizationId, TestAuthentication.TENANT_ID);
|
||||
}
|
||||
|
||||
public TestRole createRole(String roleCode, String roleName, int tenantId) {
|
||||
Integer id = insert(server,
|
||||
"INSERT INTO sys_role (role_name, role_code, sort_number, deleted, tenant_id) VALUES (?, ?, 1, 0, ?)",
|
||||
roleName, roleCode, tenantId);
|
||||
roleIds.add(id);
|
||||
return new TestRole(id, roleCode);
|
||||
}
|
||||
|
||||
public TestRole createRole(String roleCode, String roleName) {
|
||||
return createRole(roleCode, roleName, TestAuthentication.TENANT_ID);
|
||||
}
|
||||
|
||||
public void bind(int userId, int roleId) {
|
||||
insert(server,
|
||||
"INSERT INTO sys_user_role (user_id, role_id, tenant_id) VALUES (?, ?, ?)",
|
||||
userId, roleId, TestAuthentication.TENANT_ID);
|
||||
}
|
||||
|
||||
public void bindTenant(int userId, int roleId, int tenantId) {
|
||||
insert(server,
|
||||
"INSERT INTO sys_user_role (user_id, role_id, tenant_id) VALUES (?, ?, ?)",
|
||||
userId, roleId, tenantId);
|
||||
}
|
||||
|
||||
public Integer createOrganization(String name, int parentId, int tenantId) {
|
||||
Integer id = insert(server,
|
||||
"INSERT INTO sys_organization (organization_name, parent_id, organization_type, deleted, tenant_id) "
|
||||
+ "VALUES (?, ?, 'dept', 0, ?)",
|
||||
name, parentId, tenantId);
|
||||
organizationIds.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public Integer createOrganization(String name, int parentId) {
|
||||
return createOrganization(name, parentId, TestAuthentication.TENANT_ID);
|
||||
}
|
||||
|
||||
public void disableOrganization(int organizationId) {
|
||||
server.update("UPDATE sys_organization SET deleted = 1 WHERE organization_id = ?", organizationId);
|
||||
}
|
||||
|
||||
public void setUserOrganization(int userId, Integer organizationId) {
|
||||
server.update("UPDATE sys_user SET organization_id = ? WHERE user_id = ?", organizationId, userId);
|
||||
}
|
||||
|
||||
public Integer countServer(String sql, Object... args) {
|
||||
return server.queryForObject(sql, Integer.class, args);
|
||||
}
|
||||
|
||||
public Integer countMaster(String sql, Object... args) {
|
||||
return master.queryForObject(sql, Integer.class, args);
|
||||
}
|
||||
|
||||
public <T> T queryMaster(String sql, Class<T> type, Object... args) {
|
||||
return master.queryForObject(sql, type, args);
|
||||
}
|
||||
|
||||
public JdbcTemplate master() {
|
||||
return master;
|
||||
}
|
||||
|
||||
public JdbcTemplate server() {
|
||||
return server;
|
||||
}
|
||||
|
||||
/** 记录实例 id,便于非事务用例结束后清理。 */
|
||||
public void trackInstance(Long instanceId) {
|
||||
if (instanceId != null) {
|
||||
instanceIds.add(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理本次用例创建的核心库数据与审批数据。 */
|
||||
public void cleanup() {
|
||||
try {
|
||||
for (Long instanceId : instanceIds) {
|
||||
deleteInstance(instanceId);
|
||||
}
|
||||
instanceIds.clear();
|
||||
for (Integer userId : userIds) {
|
||||
server.update("DELETE FROM sys_user_role WHERE user_id = ?", userId);
|
||||
server.update("DELETE FROM sys_file_record WHERE create_user_id = ?", userId);
|
||||
server.update("DELETE FROM sys_user WHERE user_id = ?", userId);
|
||||
}
|
||||
userIds.clear();
|
||||
for (Integer roleId : roleIds) {
|
||||
server.update("DELETE FROM sys_user_role WHERE role_id = ?", roleId);
|
||||
server.update("DELETE FROM sys_role WHERE role_id = ?", roleId);
|
||||
}
|
||||
roleIds.clear();
|
||||
for (Integer organizationId : organizationIds) {
|
||||
server.update("DELETE FROM sys_organization WHERE organization_id = ?", organizationId);
|
||||
}
|
||||
organizationIds.clear();
|
||||
} catch (Exception ignored) {
|
||||
// 清理失败不阻断测试报告;残留行均由唯一后缀标识。
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteInstance(Long instanceId) {
|
||||
master.update("DELETE FROM approval_attachment WHERE instance_id = ?", instanceId);
|
||||
master.update("DELETE FROM approval_record WHERE instance_id = ?", instanceId);
|
||||
master.update("DELETE FROM approval_task WHERE instance_id = ?", instanceId);
|
||||
master.update("DELETE FROM approval_completion_form WHERE instance_id = ?", instanceId);
|
||||
master.update("DELETE FROM approval_coordination_form WHERE instance_id = ?", instanceId);
|
||||
master.update("DELETE FROM approval_instance WHERE id = ?", instanceId);
|
||||
}
|
||||
|
||||
private Integer insert(JdbcTemplate jdbcTemplate, String sql, Object... args) {
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
statement.setObject(i + 1, args[i]);
|
||||
}
|
||||
return statement;
|
||||
}, keyHolder);
|
||||
Number key = keyHolder.getKey();
|
||||
if (key == null) {
|
||||
throw new IllegalStateException("未取得自增主键: " + sql);
|
||||
}
|
||||
return key.intValue();
|
||||
}
|
||||
|
||||
public static class TestUser {
|
||||
public final int userId;
|
||||
public final String username;
|
||||
public final String realName;
|
||||
|
||||
public TestUser(int userId, String username, String realName) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.realName = realName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestRole {
|
||||
public final int roleId;
|
||||
public final String roleCode;
|
||||
|
||||
TestRole(int roleId, String roleCode) {
|
||||
this.roleId = roleId;
|
||||
this.roleCode = roleCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,20 @@ public final class TestAuthentication {
|
||||
}
|
||||
|
||||
public static Authentication admin(String... authorities) {
|
||||
return user(USER_ID, "admin", null, TENANT_ID, authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造指定用户/组织/租户的登录态,供审批流集成测试使用。
|
||||
*
|
||||
* @param userId 用户id
|
||||
* @param username 账号(同时作为姓名)
|
||||
* @param organizationId 所属部门id,可为 null
|
||||
* @param tenantId 租户id
|
||||
* @param authorities 权限串
|
||||
*/
|
||||
public static Authentication user(int userId, String username, Integer organizationId,
|
||||
int tenantId, String... authorities) {
|
||||
List<Menu> menus = Arrays.stream(authorities)
|
||||
.map(authority -> {
|
||||
Menu menu = new Menu();
|
||||
@@ -32,9 +46,12 @@ public final class TestAuthentication {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
User user = new User();
|
||||
user.setUserId(USER_ID);
|
||||
user.setUsername("admin");
|
||||
user.setTenantId(TENANT_ID);
|
||||
user.setUserId(userId);
|
||||
user.setUsername(username);
|
||||
user.setNickname(username);
|
||||
user.setRealName(username);
|
||||
user.setOrganizationId(organizationId);
|
||||
user.setTenantId(tenantId);
|
||||
user.setStatus(0);
|
||||
user.setAuthorities(menus);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user