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.
 
 
 

316 lines
15 KiB

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