package com.mydreamjob.application; import com.fasterxml.jackson.databind.ObjectMapper; import com.mydreamjob.application.dto.CandidateDetailView; import com.mydreamjob.application.dto.CandidateItemView; import com.mydreamjob.application.dto.CandidateListResponse; import com.mydreamjob.auth.User; import com.mydreamjob.auth.User.Role; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyService; import org.junit.jupiter.api.BeforeEach; 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.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("integration-test") class ApplicationControllerTest { @Autowired private MockMvc mockMvc; @Autowired private com.mydreamjob.auth.JwtTokenProvider jwtTokenProvider; @MockBean private ApplicationService applicationService; @MockBean private CompanyService companyService; @MockBean private UserRepository userRepository; @MockBean private RedisTemplate redisTemplate; private final ObjectMapper mapper = new ObjectMapper(); private static final Long COMPANY_USER_ID = 10L; private static final Long STUDENT_USER_ID = 20L; private static final Long ADMIN_USER_ID = 30L; private static final Long COMPANY_ID = 1L; private static final Long JOB_ID = 50L; private static final Long APP_ID = 100L; @BeforeEach @SuppressWarnings("unchecked") void setUp() { ValueOperations ops = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(ops); when(redisTemplate.hasKey(any())).thenReturn(false); // Default: COMPANY user exists User companyUser = new User(); companyUser.setRoles(Set.of(Role.COMPANY)); when(userRepository.findById(COMPANY_USER_ID)).thenReturn(Optional.of(companyUser)); // STUDENT user User studentUser = new User(); studentUser.setRoles(Set.of(Role.STUDENT)); when(userRepository.findById(STUDENT_USER_ID)).thenReturn(Optional.of(studentUser)); // ADMIN user User adminUser = new User(); adminUser.setRoles(Set.of(Role.ADMIN)); when(userRepository.findById(ADMIN_USER_ID)).thenReturn(Optional.of(adminUser)); } private String companyToken() { return com.mydreamjob.auth.support.TestAuthTokens.bearer(COMPANY_USER_ID, "COMPANY"); } private String studentToken() { return com.mydreamjob.auth.support.TestAuthTokens.bearer(STUDENT_USER_ID, "STUDENT"); } private String adminToken() { return com.mydreamjob.auth.support.TestAuthTokens.bearer(ADMIN_USER_ID, "ADMIN"); } private void mockCompanyExists() { Company company = Company.builder().id(COMPANY_ID).ownerUserId(COMPANY_USER_ID).name("测试企业").build(); when(companyService.findCompanyByOwnerUserId(COMPANY_USER_ID)).thenReturn(company); } // ── 正常访问 ──────────────────────────────────────────────── @Test void list_companyNormal_shouldReturn200() throws Exception { mockCompanyExists(); CandidateListResponse resp = CandidateListResponse.builder() .items(List.of()).total(0).page(0).size(20).totalPages(0).build(); when(applicationService.listCompanyCandidates(anyLong(), isNull(), isNull(), isNull(), isNull(), isNull(), anyInt(), anyInt())) .thenReturn(resp); mockMvc.perform(get("/company/candidates") .header("Authorization", companyToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.total").value(0)); } @Test void detail_companyNormal_shouldReturn200() throws Exception { mockCompanyExists(); CandidateDetailView detail = CandidateDetailView.builder() .applicationId(APP_ID).candidateName("张三").build(); when(applicationService.getCompanyCandidateDetail(COMPANY_ID, APP_ID)).thenReturn(detail); mockMvc.perform(get("/company/candidates/" + APP_ID) .header("Authorization", companyToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.candidateName").value("张三")); } // ── 权限 ──────────────────────────────────────────────────── @Test void list_noToken_shouldReturn401() throws Exception { mockMvc.perform(get("/company/candidates")) .andExpect(status().isUnauthorized()); } @Test void list_student_shouldReturn403() throws Exception { mockMvc.perform(get("/company/candidates") .header("Authorization", studentToken())) .andExpect(status().isForbidden()); } @Test void list_admin_shouldReturn403() throws Exception { mockMvc.perform(get("/company/candidates") .header("Authorization", adminToken())) .andExpect(status().isForbidden()); } @Test void detail_student_shouldReturn403() throws Exception { mockMvc.perform(get("/company/candidates/" + APP_ID) .header("Authorization", studentToken())) .andExpect(status().isForbidden()); } @Test void detail_noToken_shouldReturn401() throws Exception { mockMvc.perform(get("/company/candidates/" + APP_ID)) .andExpect(status().isUnauthorized()); } @Test void list_companyNotFound_shouldReturn404() throws Exception { when(companyService.findCompanyByOwnerUserId(COMPANY_USER_ID)).thenReturn(null); mockMvc.perform(get("/company/candidates") .header("Authorization", companyToken())) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value(5001)); } // ── 越权 ──────────────────────────────────────────────────── @Test void detail_wrongCompany_shouldReturn403() throws Exception { mockCompanyExists(); when(applicationService.getCompanyCandidateDetail(COMPANY_ID, APP_ID)) .thenThrow(new BusinessException(ErrorCode.APPLICATION_OWNER_MISMATCH)); mockMvc.perform(get("/company/candidates/" + APP_ID) .header("Authorization", companyToken())) .andExpect(status().isForbidden()) .andExpect(jsonPath("$.code").value(8004)); } // ── 参数校验-分页 ──────────────────────────────────────────── @Test void list_pageNegative_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?page=-1") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } @Test void list_sizeZero_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?size=0") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } @Test void list_sizeTooLarge_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?size=101") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } // ── 参数校验-status ────────────────────────────────────────── @Test void list_invalidStatus_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?status=INVALID") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } // ── 参数校验-sort/order ────────────────────────────────────── @Test void list_invalidSort_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?sort=invalid") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } @Test void list_invalidOrder_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?order=invalid") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } // ── 参数校验-keyword ───────────────────────────────────────── @Test void list_keywordTooLong_shouldReturn400() throws Exception { mockCompanyExists(); String longKeyword = "a".repeat(101); mockMvc.perform(get("/company/candidates?keyword=" + longKeyword) .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } // ── 参数校验-jobId ─────────────────────────────────────────── @Test void list_jobIdZero_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?jobId=0") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } @Test void list_jobIdNegative_shouldReturn400() throws Exception { mockCompanyExists(); mockMvc.perform(get("/company/candidates?jobId=-1") .header("Authorization", companyToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(1001)); } // ── 详情-不存在 ───────────────────────────────────────────── @Test void detail_appNotFound_shouldReturn404() throws Exception { mockCompanyExists(); when(applicationService.getCompanyCandidateDetail(COMPANY_ID, APP_ID)) .thenThrow(new BusinessException(ErrorCode.APPLICATION_NOT_FOUND)); mockMvc.perform(get("/company/candidates/" + APP_ID) .header("Authorization", companyToken())) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value(8002)); } // ── 敏感字段隔离 ──────────────────────────────────────────── @Test void list_shouldNotExposeSensitiveFields() throws Exception { mockCompanyExists(); CandidateItemView item = CandidateItemView.builder() .applicationId(APP_ID).jobId(JOB_ID).jobTitle("Java").status("SUBMITTED") .candidateName("张三").candidateSchool("清华").candidateEducation("本科") .candidateGraduationYear((short)2026).candidateAvatarUrl("https://img/1.png") .createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build(); CandidateListResponse resp = CandidateListResponse.builder() .items(List.of(item)).total(1).page(0).size(20).totalPages(1).build(); when(applicationService.listCompanyCandidates(anyLong(), isNull(), isNull(), isNull(), isNull(), isNull(), anyInt(), anyInt())) .thenReturn(resp); mockMvc.perform(get("/company/candidates") .header("Authorization", companyToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.items[0].candidateName").value("张三")) .andExpect(jsonPath("$.data.items[0].studentUserId").doesNotExist()) .andExpect(jsonPath("$.data.items[0].phone").doesNotExist()); } @Test void detail_shouldNotExposeSensitiveFields_butIncludeCoverLetter() throws Exception { mockCompanyExists(); CandidateDetailView detail = CandidateDetailView.builder() .applicationId(APP_ID).jobId(JOB_ID).jobTitle("Java") .jobCity("北京").jobEmploymentType("全职").status("SUBMITTED") .coverLetter("这是我的自我介绍") .candidateName("张三").candidateAvatarUrl("https://img/1.png") .candidateBio("bio").candidateLocation("北京").candidateSchool("清华") .candidateEducation("本科").candidateGraduationYear((short)2026) .candidateSkills(List.of("Java", "Spring")) .createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()) .build(); when(applicationService.getCompanyCandidateDetail(COMPANY_ID, APP_ID)).thenReturn(detail); mockMvc.perform(get("/company/candidates/" + APP_ID) .header("Authorization", companyToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.coverLetter").value("这是我的自我介绍")) .andExpect(jsonPath("$.data.phone").doesNotExist()) .andExpect(jsonPath("$.data.passwordHash").doesNotExist()) .andExpect(jsonPath("$.data.resumeUrl").doesNotExist()); } }