261 lines
11 KiB
Java
261 lines
11 KiB
Java
package com.hr.staff;
|
||
|
||
import com.fasterxml.jackson.databind.JsonNode;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.hr.staff.security.JwtService;
|
||
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.test.web.servlet.MockMvc;
|
||
import org.springframework.test.web.servlet.MvcResult;
|
||
|
||
import java.util.LinkedHashMap;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
||
import static org.assertj.core.api.Assertions.assertThat;
|
||
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.post;
|
||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||
|
||
/**
|
||
* 员工管理模块端到端集成测试(H2 内存库 + MockMvc + JWT)。
|
||
*/
|
||
@SpringBootTest
|
||
@AutoConfigureMockMvc
|
||
class StaffApiIntegrationTest {
|
||
|
||
private static final AtomicInteger SEQ = new AtomicInteger(1);
|
||
private static final AtomicInteger ID_SEQ = new AtomicInteger(1);
|
||
|
||
@Autowired
|
||
MockMvc mockMvc;
|
||
|
||
@Autowired
|
||
ObjectMapper objectMapper;
|
||
|
||
@Autowired
|
||
JwtService jwtService;
|
||
|
||
private String token(String role, Long deptId) {
|
||
return jwtService.generate(9000L + SEQ.get(), role.toLowerCase() + "_user", Set.of(role), deptId);
|
||
}
|
||
|
||
private String uniqueNo() {
|
||
return "EMP" + String.format("%08d", SEQ.incrementAndGet());
|
||
}
|
||
|
||
/**
|
||
* 生成符合身份证号格式且每次调用都不重复的 18 位身份证号。
|
||
* 结构:地区码(6) + 出生日期(8) + 顺序码(3) + 校验位(1)。
|
||
*/
|
||
private String uniqueIdNumber() {
|
||
int n = ID_SEQ.getAndIncrement() % 1000;
|
||
return String.format("41012319900101%03d4", n);
|
||
}
|
||
|
||
private String maskedId(String idNumber) {
|
||
return idNumber.substring(0, 3)
|
||
+ "*".repeat(idNumber.length() - 6)
|
||
+ idNumber.substring(idNumber.length() - 3);
|
||
}
|
||
|
||
private Map<String, Object> createBody(String employeeNo, String idNumber) {
|
||
Map<String, Object> body = new LinkedHashMap<>();
|
||
body.put("employeeNo", employeeNo);
|
||
body.put("name", "张三");
|
||
body.put("idNumber", idNumber);
|
||
body.put("gender", 1);
|
||
body.put("departmentId", 1001L);
|
||
body.put("position", "Java开发工程师");
|
||
body.put("hireDate", "2024-01-10");
|
||
body.put("phone", "13800138000");
|
||
body.put("email", "zhangsan@example.com");
|
||
body.put("emergencyContact", "张父");
|
||
body.put("emergencyPhone", "13900139000");
|
||
Map<String, Object> extra = new LinkedHashMap<>();
|
||
extra.put("education", "本科");
|
||
extra.put("technicalLevel", "P6");
|
||
extra.put("salaryGrade", "S3");
|
||
extra.put("address", "北京市朝阳区");
|
||
body.put("extra", extra);
|
||
return body;
|
||
}
|
||
|
||
private long create(String token, String employeeNo) throws Exception {
|
||
return create(token, employeeNo, uniqueIdNumber());
|
||
}
|
||
|
||
private long create(String token, String employeeNo, String idNumber) throws Exception {
|
||
MvcResult result = mockMvc.perform(post("/api/v1/staff")
|
||
.header("Authorization", "Bearer " + token)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(objectMapper.writeValueAsString(createBody(employeeNo, idNumber))))
|
||
.andExpect(status().isCreated())
|
||
.andExpect(jsonPath("$.code").value(201))
|
||
.andExpect(jsonPath("$.data.employeeNo").value(employeeNo))
|
||
.andReturn();
|
||
return objectMapper.readTree(result.getResponse().getContentAsString())
|
||
.path("data").path("id").asLong();
|
||
}
|
||
|
||
@Test
|
||
void createListDetailFlow() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String employeeNo = uniqueNo();
|
||
String idNumber = uniqueIdNumber();
|
||
long id = create(admin, employeeNo, idNumber);
|
||
|
||
mockMvc.perform(get("/api/v1/staff")
|
||
.header("Authorization", "Bearer " + admin)
|
||
.param("keyword", employeeNo))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.totalElements").value(1))
|
||
.andExpect(jsonPath("$.data.content[0].phone").value("138****8000"));
|
||
|
||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber)))
|
||
.andExpect(jsonPath("$.data.extra.technicalLevel").value("P6"))
|
||
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000"));
|
||
}
|
||
|
||
@Test
|
||
void duplicateEmployeeNoConflict() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String employeeNo = uniqueNo();
|
||
create(admin, employeeNo);
|
||
|
||
mockMvc.perform(post("/api/v1/staff")
|
||
.header("Authorization", "Bearer " + admin)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(objectMapper.writeValueAsString(createBody(employeeNo, uniqueIdNumber()))))
|
||
.andExpect(status().isConflict())
|
||
.andExpect(jsonPath("$.code").value(409));
|
||
}
|
||
|
||
@Test
|
||
void employeeMaskingAndDataScope() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String employeeNo = uniqueNo();
|
||
String idNumber = uniqueIdNumber();
|
||
long id = create(admin, employeeNo, idNumber);
|
||
|
||
// 同部门员工:脱敏、敏感字段为 null
|
||
String sameDeptEmp = token("EMPLOYEE", 1001L);
|
||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + sameDeptEmp))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber)))
|
||
.andExpect(jsonPath("$.data.extra.technicalLevel").isEmpty())
|
||
.andExpect(jsonPath("$.data.emergencyPhone").value("139****9000"));
|
||
|
||
// 跨部门员工:禁止查看
|
||
String crossDeptEmp = token("EMPLOYEE", 1002L);
|
||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + crossDeptEmp))
|
||
.andExpect(status().isForbidden());
|
||
}
|
||
|
||
@Test
|
||
void updateAndChangeLog() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String hr = token("HR", null);
|
||
String employeeNo = uniqueNo();
|
||
long id = create(admin, employeeNo);
|
||
|
||
Map<String, Object> update = new LinkedHashMap<>();
|
||
update.put("position", "高级Java开发工程师");
|
||
mockMvc.perform(put("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + hr)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(objectMapper.writeValueAsString(update)))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.position").value("高级Java开发工程师"));
|
||
|
||
mockMvc.perform(get("/api/v1/staff/{id}/change-logs", id)
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.content[0].changeType").value("POSITION_CHANGE"))
|
||
.andExpect(jsonPath("$.data.content[0].newValue").value("高级Java开发工程师"));
|
||
}
|
||
|
||
@Test
|
||
void checkEmployeeNo() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String employeeNo = uniqueNo();
|
||
create(admin, employeeNo);
|
||
|
||
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
||
.header("Authorization", "Bearer " + admin)
|
||
.param("employeeNo", employeeNo))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.exists").value(true));
|
||
|
||
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
||
.header("Authorization", "Bearer " + admin)
|
||
.param("employeeNo", "NOT_EXIST"))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.exists").value(false));
|
||
}
|
||
|
||
@Test
|
||
void deleteFlow() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
String employeeNo = uniqueNo();
|
||
long id = create(admin, employeeNo);
|
||
|
||
mockMvc.perform(delete("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.status").value("inactive"));
|
||
|
||
// 重复删除应失败
|
||
mockMvc.perform(delete("/api/v1/staff/{id}", id)
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isBadRequest());
|
||
}
|
||
|
||
@Test
|
||
void batchDeleteFlow() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
long id1 = create(admin, uniqueNo());
|
||
long id2 = create(admin, uniqueNo());
|
||
|
||
Map<String, Object> body = new LinkedHashMap<>();
|
||
body.put("ids", new long[]{id1, id2});
|
||
MvcResult result = mockMvc.perform(post("/api/v1/staff/batch-delete")
|
||
.header("Authorization", "Bearer " + admin)
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content(objectMapper.writeValueAsString(body)))
|
||
.andExpect(status().isOk())
|
||
.andReturn();
|
||
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||
assertThat(data.path("successCount").asInt()).isEqualTo(2);
|
||
assertThat(data.path("failedList").size()).isEqualTo(0);
|
||
}
|
||
|
||
@Test
|
||
void auditLogsAndDepartments() throws Exception {
|
||
String admin = token("ADMIN", null);
|
||
create(admin, uniqueNo());
|
||
|
||
mockMvc.perform(get("/api/v1/staff/audit-logs")
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.totalElements").isNumber());
|
||
|
||
mockMvc.perform(get("/api/v1/staff/departments")
|
||
.header("Authorization", "Bearer " + admin))
|
||
.andExpect(status().isOk())
|
||
.andExpect(jsonPath("$.data.length()").value(3));
|
||
}
|
||
}
|