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 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 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 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 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 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 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()); } }