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 createBody(String employeeNo, String idNumber) { Map 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 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 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 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)); } }