You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

362 lines
16 KiB

package com.mydreamjob.job;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mydreamjob.auth.User;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.company.Company;
import com.mydreamjob.job.dto.JobCreateRequest;
import com.mydreamjob.job.dto.JobUpdateRequest;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
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.boot.test.mock.mockito.MockBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 岗位展示字段(experience / education / benefits / salaryMonths / workLocation)与企业摘要字段的
* 端到端行为:写入、清空、校验、公开响应与企业摘要复用。
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("integration-test")
@Transactional
class JobDisplayFieldsIntegrationTest {
private static final String CITY = "DISPLAYTESTCITY";
private static final String BENEFITS_JSON = "[\"五险一金\",\"弹性工作\"]";
@Autowired EntityManager em;
@Autowired JobService jobService;
@Autowired ObjectMapper objectMapper;
@Autowired MockMvc mvc;
@MockBean RedisTemplate<String, Object> redis;
private Long companyId;
private String ownerPhone;
@BeforeEach
void fixture() {
ownerPhone = "188" + String.format("%08d", Math.abs(System.nanoTime() % 100_000_000L));
User owner = User.builder().phone(ownerPhone).nickname("display-company")
.role(User.Role.COMPANY).roles(Set.of(User.Role.COMPANY))
.active(true).verified(true).build();
em.persist(owner);
Company company = Company.builder()
.ownerUserId(owner.getId())
.name("展示字段测试企业")
.logoUrl("https://logo.example/display.png")
.description("企业简介-展示")
.industry("互联网")
.companySize("100-499人")
.website("https://display.example")
.address("企业登记地址不应外泄")
.verified(true)
.build();
em.persist(company);
companyId = company.getId();
em.flush();
em.clear();
}
// ── 创建:带全部新字段,各类视图一致 ────────────────────────
@Test
void createWithAllNewFieldsViewsStayConsistent() throws Exception {
Job job = jobService.createJob(companyId, fullCreateRequest("后端工程师"));
// benefits_json 以 TEXT 保存 JSON 数组字符串(非 JSON 列)
assertEquals(BENEFITS_JSON, job.getBenefitsJson());
Map<String, Object> view = jobService.toView(job);
assertEquals("3-5年", view.get("experience"));
assertEquals("本科", view.get("education"));
assertEquals(List.of("五险一金", "弹性工作"), view.get("benefits"));
assertEquals(14, view.get("salaryMonths"));
assertEquals("上海市浦东新区", view.get("workLocation"));
// 公开详情(需 PUBLISHED/CLOSED)
job.setStatus("PUBLISHED");
em.flush();
Map<String, Object> pub = jobService.getPublicJob(job.getId());
assertEquals(companyId, pub.get("companyId"));
assertEquals("3-5年", pub.get("experience"));
assertEquals("本科", pub.get("education"));
assertEquals(List.of("五险一金", "弹性工作"), pub.get("benefits"));
assertEquals(14, pub.get("salaryMonths"));
assertEquals("上海市浦东新区", pub.get("workLocation"));
}
// ── 旧请求兼容:不传新字段 ─────────────────────────────────
@Test
void legacyRequestWithoutNewFieldsStaysCompatible() {
JobCreateRequest req = new JobCreateRequest();
req.setTitle("旧请求岗位");
req.setDescription("岗位描述");
req.setCity(CITY);
req.setEmploymentType("FULL_TIME");
Job job = jobService.createJob(companyId, req);
assertNullFieldsOn(job);
Map<String, Object> view = jobService.toView(job);
assertEquals(null, view.get("experience"));
assertEquals(null, view.get("salaryMonths"));
assertEquals(List.of(), view.get("benefits"));
job.setStatus("PUBLISHED");
em.flush();
assertEquals(List.of(), jobService.getPublicJob(job.getId()).get("benefits"));
}
// ── 更新语义 ───────────────────────────────────────────────
@Test
void updateWithNullFieldsKeepsOriginalValues() {
Job job = jobService.createJob(companyId, fullCreateRequest("更新保持"));
Job updated = jobService.updateJob(companyId, job.getId(), new JobUpdateRequest());
assertEquals("3-5年", updated.getExperience());
assertEquals("本科", updated.getEducation());
assertEquals(14, updated.getSalaryMonths());
assertEquals("上海市浦东新区", updated.getWorkLocation());
assertEquals(BENEFITS_JSON, updated.getBenefitsJson());
}
@Test
void updateWithBlankStringClearsToNull() {
Job job = jobService.createJob(companyId, fullCreateRequest("更新清空"));
JobUpdateRequest req = new JobUpdateRequest();
req.setExperience(" ");
req.setEducation("");
req.setWorkLocation(" ");
Job updated = jobService.updateJob(companyId, job.getId(), req);
assertEquals(null, updated.getExperience());
assertEquals(null, updated.getEducation());
assertEquals(null, updated.getWorkLocation());
em.flush();
em.clear();
Job reloaded = jobService.getJob(job.getId());
assertEquals(null, reloaded.getExperience());
assertEquals(null, reloaded.getEducation());
assertEquals(null, reloaded.getWorkLocation());
}
@Test
void updateWithNonBlankValueTrimsEnds() {
Job job = jobService.createJob(companyId, fullCreateRequest("更新去空白"));
JobUpdateRequest req = new JobUpdateRequest();
req.setExperience(" 5-10年 ");
req.setWorkLocation(" 北京市海淀区 ");
Job updated = jobService.updateJob(companyId, job.getId(), req);
assertEquals("5-10年", updated.getExperience());
assertEquals("北京市海淀区", updated.getWorkLocation());
}
@Test
void updateWithEmptyBenefitsListClears() {
Job job = jobService.createJob(companyId, fullCreateRequest("福利清空"));
JobUpdateRequest req = new JobUpdateRequest();
req.setBenefits(List.of());
Job updated = jobService.updateJob(companyId, job.getId(), req);
assertEquals(null, updated.getBenefitsJson());
assertEquals(List.of(), jobService.toView(updated).get("benefits"));
}
@Test
void updateWithNullSalaryMonthsKeepsOriginal() {
Job job = jobService.createJob(companyId, fullCreateRequest("月数保持"));
jobService.updateJob(companyId, job.getId(), new JobUpdateRequest());
assertEquals(14, jobService.getJob(job.getId()).getSalaryMonths());
}
// ── salaryMonths 校验 1..24 ────────────────────────────────
@Test
void salaryMonthsBoundaries() {
Job job = jobService.createJob(companyId, fullCreateRequest("月数边界"));
JobUpdateRequest one = new JobUpdateRequest();
one.setSalaryMonths(1);
assertEquals(1, jobService.updateJob(companyId, job.getId(), one).getSalaryMonths());
JobUpdateRequest twentyFour = new JobUpdateRequest();
twentyFour.setSalaryMonths(24);
assertEquals(24, jobService.updateJob(companyId, job.getId(), twentyFour).getSalaryMonths());
for (int invalid : new int[]{0, 25, -1}) {
JobUpdateRequest bad = new JobUpdateRequest();
bad.setSalaryMonths(invalid);
assertBadRequest(() -> jobService.updateJob(companyId, job.getId(), bad));
}
}
// ── benefits 校验与去重保序 ────────────────────────────────
@Test
void benefitsValidationAndDeduplication() {
Job job = jobService.createJob(companyId, fullCreateRequest("福利校验"));
List<String> tooMany = new ArrayList<>();
for (int i = 0; i < 21; i++) tooMany.add("benefit-" + i);
assertBadRequest(() -> jobService.updateJob(companyId, job.getId(), updateWithBenefits(tooMany)));
assertBadRequest(() -> jobService.updateJob(companyId, job.getId(),
updateWithBenefits(List.of("x".repeat(41)))));
assertBadRequest(() -> jobService.updateJob(companyId, job.getId(),
updateWithBenefits(List.of("五险一金", " "))));
assertBadRequest(() -> jobService.updateJob(companyId, job.getId(),
updateWithBenefits(Arrays.asList("五险一金", null))));
Job updated = jobService.updateJob(companyId, job.getId(),
updateWithBenefits(List.of("A", "B", "A", "C", "B")));
assertEquals("[\"A\",\"B\",\"C\"]", updated.getBenefitsJson());
assertEquals(List.of("A", "B", "C"), jobService.toView(updated).get("benefits"));
}
// ── 公开响应:新字段 + 企业摘要,且无敏感字段泄漏 ──────────
@Test
void publicListAndDetailExposeNewFieldsAndCompanySummary() throws Exception {
Job job = jobService.createJob(companyId, fullCreateRequest("公开字段岗位"));
job.setStatus("PUBLISHED");
em.flush();
em.clear();
String listBody = mvc.perform(get("/jobs").param("city", CITY).param("size", "50"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
JsonNode data = objectMapper.readTree(listBody).path("data");
assertEquals(1, data.path("total").asLong());
JsonNode item = data.path("items").get(0);
assertEquals(job.getId().longValue(), item.path("id").asLong());
assertEquals("3-5年", item.path("experience").asText());
assertEquals("本科", item.path("education").asText());
assertEquals(14, item.path("salaryMonths").asInt());
assertEquals("上海市浦东新区", item.path("workLocation").asText());
assertTrue(item.path("benefits").isArray());
assertEquals(2, item.path("benefits").size());
assertEquals("五险一金", item.path("benefits").get(0).asText());
JsonNode company = item.path("company");
assertNotNull(company);
assertEquals(companyId.longValue(), company.path("id").asLong());
assertEquals("展示字段测试企业", company.path("name").asText());
assertEquals("https://logo.example/display.png", company.path("logoUrl").asText());
assertEquals("企业简介-展示", company.path("description").asText());
assertEquals("互联网", company.path("industry").asText());
assertEquals("100-499人", company.path("companySize").asText());
assertTrue(company.path("verified").asBoolean());
String detailBody = mvc.perform(get("/jobs/public/" + job.getId()))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
JsonNode detail = objectMapper.readTree(detailBody).path("data");
assertEquals(companyId.longValue(), detail.path("companyId").asLong());
assertEquals("3-5年", detail.path("experience").asText());
assertEquals("100-499人", detail.path("company").path("companySize").asText());
assertEquals(List.of("五险一金", "弹性工作"), objectMapper.convertValue(
detail.path("benefits"),
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)));
}
@Test
void publicResponsesDoNotLeakSensitiveFields() throws Exception {
Job job = jobService.createJob(companyId, fullCreateRequest("隐私体检岗位"));
job.setStatus("PUBLISHED");
em.flush();
em.clear();
String listBody = mvc.perform(get("/jobs").param("city", CITY)).andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
String detailBody = mvc.perform(get("/jobs/public/" + job.getId())).andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
for (String body : new String[]{listBody, detailBody}) {
assertTrue(!body.contains("ownerUserId"), "公开响应不得包含 ownerUserId");
assertTrue(!body.contains(ownerPhone), "公开响应不得包含企业主手机号");
assertTrue(!body.contains("企业登记地址不应外泄"), "公开响应不得包含企业登记地址");
assertTrue(!body.contains("password"), "公开响应不得包含密码字段");
}
}
// ── 草稿岗位公开不可见 ────────────────────────────────────
@Test
void draftJobIsNotVisibleOnPublicDetail() throws Exception {
Job draft = jobService.createJob(companyId, fullCreateRequest("草稿岗位"));
em.flush();
BusinessException ex = assertThrows(BusinessException.class,
() -> jobService.getPublicJob(draft.getId()));
assertEquals(ErrorCode.JOB_NOT_FOUND.getCode(), ex.getErrorCodeInt());
mvc.perform(get("/jobs/public/" + draft.getId())).andExpect(status().isNotFound());
}
// ── Helper ────────────────────────────────────────────────
private JobCreateRequest fullCreateRequest(String title) {
JobCreateRequest req = new JobCreateRequest();
req.setTitle(title);
req.setDescription("岗位描述");
req.setRequirements("岗位要求");
req.setCity(CITY);
req.setEmploymentType("FULL_TIME");
req.setSalaryMin(20000);
req.setSalaryMax(40000);
req.setExperience("3-5年");
req.setEducation("本科");
req.setBenefits(List.of("五险一金", "弹性工作"));
req.setSalaryMonths(14);
req.setWorkLocation("上海市浦东新区");
return req;
}
private JobUpdateRequest updateWithBenefits(List<String> benefits) {
JobUpdateRequest req = new JobUpdateRequest();
req.setBenefits(benefits);
return req;
}
private void assertNullFieldsOn(Job job) {
assertEquals(null, job.getExperience());
assertEquals(null, job.getEducation());
assertEquals(null, job.getBenefitsJson());
assertEquals(null, job.getSalaryMonths());
assertEquals(null, job.getWorkLocation());
}
private void assertBadRequest(Executable callable) {
BusinessException ex = assertThrows(BusinessException.class, callable);
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
}
}