develop: 开发员工管理模块(端到端验证)
This commit is contained in:
parent
0b5e437650
commit
661eb156b7
@ -1,22 +1,20 @@
|
|||||||
package com.hr.staff;
|
package com.hr.staff;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.hr.staff.repository.StaffEmployeeRepository;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.hr.staff.security.AuthUser;
|
||||||
import com.hr.staff.security.JwtService;
|
import com.hr.staff.security.JwtService;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
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.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.delete;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
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.post;
|
||||||
@ -29,231 +27,203 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
*/
|
*/
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles("dev")
|
||||||
|
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||||
class StaffApiIntegrationTest {
|
class StaffApiIntegrationTest {
|
||||||
|
|
||||||
private static final AtomicInteger SEQ = new AtomicInteger(1);
|
@Autowired
|
||||||
private static final AtomicInteger ID_SEQ = new AtomicInteger(1);
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
MockMvc mockMvc;
|
private JwtService jwtService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ObjectMapper objectMapper;
|
private StaffEmployeeRepository employeeRepository;
|
||||||
|
|
||||||
@Autowired
|
private String adminToken() {
|
||||||
JwtService jwtService;
|
return "Bearer " + jwtService.generateToken(new AuthUser(1L, "admin", Set.of("ADMIN"), 1003L));
|
||||||
|
|
||||||
private String token(String role, Long deptId) {
|
|
||||||
return jwtService.generate(9000L + SEQ.get(), role.toLowerCase() + "_user", Set.of(role), deptId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String uniqueNo() {
|
private String hrToken() {
|
||||||
return "EMP" + String.format("%08d", SEQ.incrementAndGet());
|
return "Bearer " + jwtService.generateToken(new AuthUser(2L, "hr", Set.of("HR"), 1003L));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String employeeToken(Long departmentId) {
|
||||||
* 生成符合身份证号格式且每次调用都不重复的 18 位身份证号。
|
return "Bearer " + jwtService.generateToken(new AuthUser(10L, "employee", Set.of("EMPLOYEE"), departmentId));
|
||||||
* 结构:地区码(6) + 出生日期(8) + 顺序码(3) + 校验位(1)。
|
|
||||||
*/
|
|
||||||
private String uniqueIdNumber() {
|
|
||||||
int n = ID_SEQ.getAndIncrement() % 1000;
|
|
||||||
return String.format("41012319900101%03d4", n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String maskedId(String idNumber) {
|
private String createBody(String employeeNo, String name, String idNumber, Long departmentId) {
|
||||||
return idNumber.substring(0, 3)
|
return """
|
||||||
+ "*".repeat(idNumber.length() - 6)
|
{
|
||||||
+ idNumber.substring(idNumber.length() - 3);
|
"employeeNo": "%s",
|
||||||
|
"name": "%s",
|
||||||
|
"idNumber": "%s",
|
||||||
|
"gender": 1,
|
||||||
|
"departmentId": %d,
|
||||||
|
"position": "Java开发工程师",
|
||||||
|
"hireDate": "2024-01-10",
|
||||||
|
"phone": "13800138000",
|
||||||
|
"email": "zhangsan@example.com",
|
||||||
|
"emergencyContact": "张父",
|
||||||
|
"emergencyPhone": "13900139000",
|
||||||
|
"status": "active",
|
||||||
|
"extra": {
|
||||||
|
"education": "本科",
|
||||||
|
"technicalLevel": "P6",
|
||||||
|
"salaryGrade": "S3",
|
||||||
|
"address": "北京市朝阳区"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(employeeNo, name, idNumber, departmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> createBody(String employeeNo, String idNumber) {
|
private void createEmployee(String employeeNo, String name, String idNumber, Long departmentId) throws Exception {
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
mockMvc.perform(post("/api/v1/staff")
|
||||||
body.put("employeeNo", employeeNo);
|
.header("Authorization", adminToken())
|
||||||
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)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(createBody(employeeNo, idNumber))))
|
.content(createBody(employeeNo, name, idNumber, departmentId)))
|
||||||
.andExpect(status().isCreated())
|
.andExpect(status().isCreated())
|
||||||
.andExpect(jsonPath("$.code").value(201))
|
.andExpect(jsonPath("$.code").value(201));
|
||||||
.andExpect(jsonPath("$.data.employeeNo").value(employeeNo))
|
}
|
||||||
.andReturn();
|
|
||||||
return objectMapper.readTree(result.getResponse().getContentAsString())
|
private Long employeeId(String employeeNo) {
|
||||||
.path("data").path("id").asLong();
|
return employeeRepository.findByEmployeeNo(employeeNo).orElseThrow().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createListDetailFlow() throws Exception {
|
void test01_createListAndDetail() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240001", "张三", "110101199001011234", 1001L);
|
||||||
String employeeNo = uniqueNo();
|
|
||||||
String idNumber = uniqueIdNumber();
|
|
||||||
long id = create(admin, employeeNo, idNumber);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff")
|
mockMvc.perform(get("/api/v1/staff")
|
||||||
.header("Authorization", "Bearer " + admin)
|
.header("Authorization", adminToken())
|
||||||
.param("keyword", employeeNo))
|
.param("page", "1").param("size", "20"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
.andExpect(jsonPath("$.data.totalElements").value(1))
|
.andExpect(jsonPath("$.data.totalElements").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.content[0].employeeNo").value("EMP20240001"))
|
||||||
.andExpect(jsonPath("$.data.content[0].phone").value("138****8000"));
|
.andExpect(jsonPath("$.data.content[0].phone").value("138****8000"));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
Long id = employeeId("EMP20240001");
|
||||||
.header("Authorization", "Bearer " + admin))
|
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber)))
|
.andExpect(jsonPath("$.data.idNumber").value("110************234"))
|
||||||
|
.andExpect(jsonPath("$.data.phone").value("138****8000"))
|
||||||
|
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000"))
|
||||||
.andExpect(jsonPath("$.data.extra.technicalLevel").value("P6"))
|
.andExpect(jsonPath("$.data.extra.technicalLevel").value("P6"))
|
||||||
.andExpect(jsonPath("$.data.emergencyPhone").value("13900139000"));
|
.andExpect(jsonPath("$.data.extra.salaryGrade").value("S3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void duplicateEmployeeNoConflict() throws Exception {
|
void test02_duplicateEmployeeNoReturns409() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240002", "李四", "110101199001011235", 1001L);
|
||||||
String employeeNo = uniqueNo();
|
|
||||||
create(admin, employeeNo);
|
|
||||||
|
|
||||||
mockMvc.perform(post("/api/v1/staff")
|
mockMvc.perform(post("/api/v1/staff")
|
||||||
.header("Authorization", "Bearer " + admin)
|
.header("Authorization", adminToken())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(createBody(employeeNo, uniqueIdNumber()))))
|
.content(createBody("EMP20240002", "王五", "110101199001011236", 1002L)))
|
||||||
.andExpect(status().isConflict())
|
.andExpect(status().isConflict())
|
||||||
.andExpect(jsonPath("$.code").value(409));
|
.andExpect(jsonPath("$.code").value(409));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void employeeMaskingAndDataScope() throws Exception {
|
void test03_employeeDataPermissionAndMasking() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240003", "赵六", "110101199001011237", 1001L);
|
||||||
String employeeNo = uniqueNo();
|
Long id = employeeId("EMP20240003");
|
||||||
String idNumber = uniqueIdNumber();
|
|
||||||
long id = create(admin, employeeNo, idNumber);
|
|
||||||
|
|
||||||
// 同部门员工:脱敏、敏感字段为 null
|
// 本部门员工可查看,但敏感字段脱敏
|
||||||
String sameDeptEmp = token("EMPLOYEE", 1001L);
|
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1001L)))
|
||||||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
|
||||||
.header("Authorization", "Bearer " + sameDeptEmp))
|
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.idNumber").value(maskedId(idNumber)))
|
.andExpect(jsonPath("$.data.idNumber").value("110************237"))
|
||||||
.andExpect(jsonPath("$.data.extra.technicalLevel").isEmpty())
|
.andExpect(jsonPath("$.data.phone").value("138****8000"))
|
||||||
.andExpect(jsonPath("$.data.emergencyPhone").value("139****9000"));
|
.andExpect(jsonPath("$.data.emergencyPhone").value("139****9000"))
|
||||||
|
.andExpect(jsonPath("$.data.extra.technicalLevel").doesNotExist());
|
||||||
|
|
||||||
// 跨部门员工:禁止查看
|
// 跨部门员工无权限
|
||||||
String crossDeptEmp = token("EMPLOYEE", 1002L);
|
mockMvc.perform(get("/api/v1/staff/" + id).header("Authorization", employeeToken(1002L)))
|
||||||
mockMvc.perform(get("/api/v1/staff/{id}", id)
|
.andExpect(status().isForbidden())
|
||||||
.header("Authorization", "Bearer " + crossDeptEmp))
|
.andExpect(jsonPath("$.code").value(403));
|
||||||
.andExpect(status().isForbidden());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void updateAndChangeLog() throws Exception {
|
void test04_updateGeneratesChangeLog() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240004", "孙七", "110101199001011238", 1001L);
|
||||||
String hr = token("HR", null);
|
Long id = employeeId("EMP20240004");
|
||||||
String employeeNo = uniqueNo();
|
|
||||||
long id = create(admin, employeeNo);
|
|
||||||
|
|
||||||
Map<String, Object> update = new LinkedHashMap<>();
|
mockMvc.perform(put("/api/v1/staff/" + id)
|
||||||
update.put("position", "高级Java开发工程师");
|
.header("Authorization", adminToken())
|
||||||
mockMvc.perform(put("/api/v1/staff/{id}", id)
|
|
||||||
.header("Authorization", "Bearer " + hr)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(update)))
|
.content("""
|
||||||
|
{"position": "高级Java开发工程师", "extra": {"technicalLevel": "P7"}}
|
||||||
|
"""))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.position").value("高级Java开发工程师"));
|
.andExpect(jsonPath("$.data.position").value("高级Java开发工程师"));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/{id}/change-logs", id)
|
mockMvc.perform(get("/api/v1/staff/" + id + "/change-logs")
|
||||||
.header("Authorization", "Bearer " + admin))
|
.header("Authorization", adminToken()))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.content[0].changeType").value("POSITION_CHANGE"))
|
.andExpect(jsonPath("$.data.totalElements").value(2))
|
||||||
.andExpect(jsonPath("$.data.content[0].newValue").value("高级Java开发工程师"));
|
.andExpect(jsonPath("$.data.content[0].changeType").value("LEVEL_CHANGE"))
|
||||||
|
.andExpect(jsonPath("$.data.content[1].changeType").value("POSITION_CHANGE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void checkEmployeeNo() throws Exception {
|
void test05_checkEmployeeNo() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240005", "周八", "110101199001011239", 1001L);
|
||||||
String employeeNo = uniqueNo();
|
|
||||||
create(admin, employeeNo);
|
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
||||||
.header("Authorization", "Bearer " + admin)
|
.header("Authorization", adminToken())
|
||||||
.param("employeeNo", employeeNo))
|
.param("employeeNo", "EMP20240005"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.exists").value(true));
|
.andExpect(jsonPath("$.data.exists").value(true));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
mockMvc.perform(get("/api/v1/staff/check/employee-no")
|
||||||
.header("Authorization", "Bearer " + admin)
|
.header("Authorization", adminToken())
|
||||||
.param("employeeNo", "NOT_EXIST"))
|
.param("employeeNo", "NOT_EXIST"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.exists").value(false));
|
.andExpect(jsonPath("$.data.exists").value(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void deleteFlow() throws Exception {
|
void test06_logicalDeleteAndRepeatProtection() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240006", "吴九", "110101199001011240", 1001L);
|
||||||
String employeeNo = uniqueNo();
|
Long id = employeeId("EMP20240006");
|
||||||
long id = create(admin, employeeNo);
|
|
||||||
|
|
||||||
mockMvc.perform(delete("/api/v1/staff/{id}", id)
|
mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||||
.header("Authorization", "Bearer " + admin))
|
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.status").value("inactive"));
|
.andExpect(jsonPath("$.data.status").value("inactive"));
|
||||||
|
|
||||||
// 重复删除应失败
|
mockMvc.perform(delete("/api/v1/staff/" + id).header("Authorization", adminToken()))
|
||||||
mockMvc.perform(delete("/api/v1/staff/{id}", id)
|
.andExpect(status().isBadRequest())
|
||||||
.header("Authorization", "Bearer " + admin))
|
.andExpect(jsonPath("$.code").value(400));
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void batchDeleteFlow() throws Exception {
|
void test07_batchDelete() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240007", "郑十", "110101199001011241", 1001L);
|
||||||
long id1 = create(admin, uniqueNo());
|
createEmployee("EMP20240008", "钱一", "110101199001011242", 1001L);
|
||||||
long id2 = create(admin, uniqueNo());
|
Long id1 = employeeId("EMP20240007");
|
||||||
|
Long id2 = employeeId("EMP20240008");
|
||||||
|
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
mockMvc.perform(post("/api/v1/staff/batch-delete")
|
||||||
body.put("ids", new long[]{id1, id2});
|
.header("Authorization", adminToken())
|
||||||
MvcResult result = mockMvc.perform(post("/api/v1/staff/batch-delete")
|
|
||||||
.header("Authorization", "Bearer " + admin)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(body)))
|
.content("{\"ids\": [" + id1 + ", " + id2 + "]}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andReturn();
|
.andExpect(jsonPath("$.data.successCount").value(2))
|
||||||
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
.andExpect(jsonPath("$.data.failedList.length()").value(0));
|
||||||
assertThat(data.path("successCount").asInt()).isEqualTo(2);
|
|
||||||
assertThat(data.path("failedList").size()).isEqualTo(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void auditLogsAndDepartments() throws Exception {
|
void test08_auditLogsAndDepartments() throws Exception {
|
||||||
String admin = token("ADMIN", null);
|
createEmployee("EMP20240009", "冯二", "110101199001011243", 1001L);
|
||||||
create(admin, uniqueNo());
|
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/audit-logs")
|
mockMvc.perform(get("/api/v1/staff/audit-logs")
|
||||||
.header("Authorization", "Bearer " + admin))
|
.header("Authorization", adminToken())
|
||||||
|
.param("operation", "CREATE"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.totalElements").isNumber());
|
.andExpect(jsonPath("$.data.totalElements").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.content[0].operation").value("CREATE"));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/v1/staff/departments")
|
mockMvc.perform(get("/api/v1/staff/departments").header("Authorization", adminToken()))
|
||||||
.header("Authorization", "Bearer " + admin))
|
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data.length()").value(3));
|
.andExpect(jsonPath("$.data.length()").value(3));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user