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.
611 lines
27 KiB
611 lines
27 KiB
package com.mydreamjob.job;
|
|
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.job.dto.JobSearchItem;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.Spy;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.springframework.data.domain.Page;
|
|
import org.springframework.data.domain.PageImpl;
|
|
import org.springframework.data.domain.PageRequest;
|
|
import org.springframework.data.domain.Pageable;
|
|
import org.springframework.data.jpa.domain.Specification;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyCollection;
|
|
import static org.mockito.ArgumentMatchers.anyInt;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
/**
|
|
* {@link JobService#searchJobs} 的**编排与参数校验**测试(纯 Mockito)。
|
|
*
|
|
* <p>本类把 {@code JobRepository.findAll(Specification, Pageable)} 与
|
|
* {@code CompanyRepository} 整体 Mock 掉,因此它只能证明参数校验、分页对象、
|
|
* 排序参数传递和 DTO 组装的编排逻辑;**不能证明 Criteria 谓词、SQL 过滤、
|
|
* 排序与分页在真实数据库上的行为**。</p>
|
|
*
|
|
* <p>真实查询覆盖见 Q01 补充的
|
|
* {@link JobSearchSpecificationIntegrationTest}(H2 真实 Repository)、
|
|
* {@link JobSearchServiceIntegrationTest}(H2 真实 Service/EntityManager)、
|
|
* {@link JobSearchSpecificationMySqlIntegrationTest}(真实 MySQL 8)。</p>
|
|
*/
|
|
@ExtendWith(MockitoExtension.class)
|
|
class JobSearchServiceTest {
|
|
|
|
@Mock
|
|
private JobRepository jobRepository;
|
|
|
|
@Mock
|
|
private CompanyRepository companyRepository;
|
|
|
|
@Spy
|
|
@InjectMocks
|
|
private JobService jobService;
|
|
|
|
private Job pubJob1;
|
|
private Job pubJob2;
|
|
private Job pubJobNullSalary;
|
|
private Job draftJob;
|
|
private Job closedJob;
|
|
private Company company1;
|
|
private Company company2;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
company1 = Company.builder()
|
|
.id(1L).name("阿里巴巴").logoUrl("https://logo1.png")
|
|
.industry("IT").verified(true).build();
|
|
company2 = Company.builder()
|
|
.id(2L).name("腾讯科技").logoUrl("https://logo2.png")
|
|
.industry("IT").verified(false).build();
|
|
|
|
pubJob1 = Job.builder()
|
|
.id(1L).companyId(1L).title("Java开发工程师")
|
|
.description("负责后端开发").requirements("3年Java经验")
|
|
.city("北京").employmentType("FULL_TIME")
|
|
.salaryMin(15000).salaryMax(25000)
|
|
.status("PUBLISHED")
|
|
.publishedAt(LocalDateTime.of(2026, 8, 1, 10, 0))
|
|
.createdAt(LocalDateTime.of(2026, 8, 1, 9, 0))
|
|
.build();
|
|
|
|
pubJob2 = Job.builder()
|
|
.id(2L).companyId(2L).title("前端开发工程师")
|
|
.description("负责前端开发").requirements("3年React经验")
|
|
.city("上海").employmentType("FULL_TIME")
|
|
.salaryMin(20000).salaryMax(35000)
|
|
.status("PUBLISHED")
|
|
.publishedAt(LocalDateTime.of(2026, 8, 2, 10, 0))
|
|
.createdAt(LocalDateTime.of(2026, 8, 2, 9, 0))
|
|
.build();
|
|
|
|
pubJobNullSalary = Job.builder()
|
|
.id(5L).companyId(1L).title("无薪资岗位")
|
|
.description("无薪资").requirements("无")
|
|
.city("深圳").employmentType("INTERN")
|
|
.status("PUBLISHED")
|
|
.createdAt(LocalDateTime.of(2026, 8, 3, 9, 0))
|
|
.build();
|
|
|
|
draftJob = Job.builder()
|
|
.id(3L).companyId(1L).title("Draft岗位")
|
|
.description("草稿").city("北京").employmentType("FULL_TIME")
|
|
.status("DRAFT").build();
|
|
|
|
closedJob = Job.builder()
|
|
.id(4L).companyId(1L).title("已关闭岗位")
|
|
.description("已关闭").city("北京").employmentType("FULL_TIME")
|
|
.salaryMin(10000).salaryMax(20000)
|
|
.status("CLOSED")
|
|
.createdAt(LocalDateTime.of(2026, 7, 1, 9, 0))
|
|
.build();
|
|
}
|
|
|
|
// ── 1. 关键词搜索 ───────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_keyword_in_title_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
"Java", null, null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertEquals("Java开发工程师", result.getContent().get(0).getTitle());
|
|
}
|
|
|
|
@Test
|
|
void search_keyword_in_description_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
"后端", null, null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
}
|
|
|
|
@Test
|
|
void search_keyword_in_requirements_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
"React", null, null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
}
|
|
|
|
// ── 2. 城市筛选 ─────────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_city_filter_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, "上海", null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertEquals("上海", result.getContent().get(0).getCity());
|
|
}
|
|
|
|
// ── 3. 用工类型筛选 ─────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_employmentType_filter_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1, pubJob2)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, "FULL_TIME", null, null, null, null, 0, 20);
|
|
|
|
assertEquals(2, result.getTotalElements());
|
|
}
|
|
|
|
// ── 4. 薪资范围筛选 ─────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_salary_range_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, 20000, 30000, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertEquals("前端开发工程师", result.getContent().get(0).getTitle());
|
|
}
|
|
|
|
@Test
|
|
void search_salary_null_shouldNotMatch() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(Collections.emptyList()));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, 10000, 20000, null, null, 0, 20);
|
|
|
|
assertEquals(0, result.getTotalElements());
|
|
}
|
|
|
|
// ── 5. 多条件组合 ───────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_combined_filters_shouldWork() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
"Java", "北京", "FULL_TIME", 10000, 30000, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
}
|
|
|
|
// ── 6. 分页 ─────────────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_pagination_shouldReturnCorrectStructure() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1), PageRequest.of(0, 5), 10));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 5);
|
|
|
|
assertEquals(10, result.getTotalElements());
|
|
assertEquals(2, result.getTotalPages());
|
|
assertEquals(0, result.getNumber());
|
|
assertEquals(5, result.getSize());
|
|
}
|
|
|
|
// ── 7. 排序 latest ──────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_sort_latest_shouldUseCreatedAtDesc() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2, pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, "latest", 0, 20);
|
|
|
|
assertEquals(2, result.getTotalElements());
|
|
assertEquals("前端开发工程师", result.getContent().get(0).getTitle());
|
|
}
|
|
|
|
// ── 8. 排序 salary(通过 spy 模拟内部方法) ─────────────────
|
|
|
|
@Test
|
|
void search_sort_salary_shouldUseSalaryMaxDesc() {
|
|
doReturn(new PageImpl<>(List.of(pubJob2, pubJob1)))
|
|
.when(jobService).findWithSalarySort(any(), anyInt(), anyInt());
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, "salary", 0, 20);
|
|
|
|
assertEquals(2, result.getTotalElements());
|
|
assertEquals("前端开发工程师", result.getContent().get(0).getTitle());
|
|
}
|
|
|
|
// ── 9. 排序 salary NULL 最后 ────────────────────────────────
|
|
|
|
@Test
|
|
void search_sort_salary_nullLast_shouldPutNullLast() {
|
|
// salaryMax=null 的岗位应在最后
|
|
doReturn(new PageImpl<>(List.of(pubJob2, pubJob1, pubJobNullSalary)))
|
|
.when(jobService).findWithSalarySort(any(), anyInt(), anyInt());
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, "salary", 0, 20);
|
|
|
|
assertEquals(3, result.getTotalElements());
|
|
// 前两个有薪资,第三个无薪资
|
|
assertNotNull(result.getContent().get(0).getSalaryMax());
|
|
assertNotNull(result.getContent().get(1).getSalaryMax());
|
|
assertNull(result.getContent().get(2).getSalaryMax());
|
|
}
|
|
|
|
@Test
|
|
void search_sort_salary_nullSalaryMax_shouldBeLast() {
|
|
// 只有一个 null 薪资岗位的场景
|
|
doReturn(new PageImpl<>(List.of(pubJobNullSalary)))
|
|
.when(jobService).findWithSalarySort(any(), anyInt(), anyInt());
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, "salary", 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertNull(result.getContent().get(0).getSalaryMax());
|
|
}
|
|
|
|
// ── 10. 排序 relevance 兜底 ──────────────────────────────────
|
|
|
|
@Test
|
|
void search_sort_relevance_shouldFallbackToLatest() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2, pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, "relevance", 0, 20);
|
|
|
|
assertEquals(2, result.getTotalElements());
|
|
}
|
|
|
|
// ── 11. 空结果 ──────────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_empty_result_shouldReturnEmptyPage() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(Collections.emptyList()));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
"不存在的关键词", null, null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(0, result.getTotalElements());
|
|
assertTrue(result.getContent().isEmpty());
|
|
}
|
|
|
|
// ── 12. 仅 PUBLISHED 可见 ───────────────────────────────────
|
|
|
|
@Test
|
|
void search_DRAFT_shouldNotAppear() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertEquals("PUBLISHED", result.getContent().get(0).getStatus());
|
|
}
|
|
|
|
// ── 13. CLOSED 搜索隐藏 ─────────────────────────────────────
|
|
|
|
@Test
|
|
void search_CLOSED_shouldNotAppear() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 20);
|
|
|
|
assertFalse(result.getContent().stream().anyMatch(j -> "CLOSED".equals(j.getStatus())));
|
|
}
|
|
|
|
// ── 14. 边界 size ───────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_size_1_shouldSucceed() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1), PageRequest.of(0, 1), 10));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 1);
|
|
|
|
assertEquals(1, result.getSize());
|
|
}
|
|
|
|
@Test
|
|
void search_size_50_shouldSucceed() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1), PageRequest.of(0, 50), 1));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 50);
|
|
|
|
assertEquals(50, result.getSize());
|
|
}
|
|
|
|
// ── 15. 边界异常 ────────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_size_0_shouldThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.searchJobs(null, null, null, null, null, null, null, 0, 0));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test
|
|
void search_size_51_shouldThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.searchJobs(null, null, null, null, null, null, null, 0, 51));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test
|
|
void search_page_negative_shouldThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.searchJobs(null, null, null, null, null, null, null, -1, 20));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test
|
|
void search_salaryMin_greater_than_salaryMax_shouldThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.searchJobs(null, null, null, 20000, 10000, null, null, 0, 20));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test
|
|
void search_invalid_sort_shouldThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.searchJobs(null, null, null, null, null, null, "invalid", 0, 20));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
// ── 16. SQL 注入防护 ────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_sql_injection_shouldNotAffect() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(Collections.emptyList()));
|
|
|
|
assertDoesNotThrow(() -> jobService.searchJobs(
|
|
"'; DROP TABLE jobs; --", null, null, null, null, null, null, 0, 20));
|
|
}
|
|
|
|
@Test
|
|
void search_sql_injection_union_shouldNotAffect() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(Collections.emptyList()));
|
|
|
|
assertDoesNotThrow(() -> jobService.searchJobs(
|
|
"' UNION SELECT * FROM users --", null, null, null, null, null, null, 0, 20));
|
|
}
|
|
|
|
// ── 17. 公开字段脱敏 ────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_public_dto_shouldNotLeakInternalFields() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 20);
|
|
|
|
JobSearchItem item = result.getContent().get(0);
|
|
assertNotNull(item.getCompany());
|
|
assertEquals(1L, item.getCompany().getId());
|
|
assertEquals("阿里巴巴", item.getCompany().getName());
|
|
assertEquals("https://logo1.png", item.getCompany().getLogoUrl());
|
|
assertTrue(item.getCompany().getVerified());
|
|
}
|
|
|
|
@Test
|
|
void search_company_without_record_shouldNotNPE() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of());
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, null, null, 0, 20);
|
|
|
|
JobSearchItem item = result.getContent().get(0);
|
|
assertNull(item.getCompany());
|
|
}
|
|
|
|
// ── 18. 筛选选项(聚合查询) ────────────────────────────────
|
|
|
|
@Test
|
|
void filters_shouldUseAggregationQueries() {
|
|
when(jobRepository.findDistinctCitiesPublished()).thenReturn(List.of("北京", "上海"));
|
|
when(jobRepository.findDistinctEmploymentTypesPublished()).thenReturn(List.of("FULL_TIME"));
|
|
when(jobRepository.findMinSalaryPublished()).thenReturn(15000);
|
|
when(jobRepository.findMaxSalaryPublished()).thenReturn(35000);
|
|
|
|
Map<String, Object> options = jobService.getFilterOptions();
|
|
|
|
@SuppressWarnings("unchecked")
|
|
List<String> cities = (List<String>) options.get("cities");
|
|
@SuppressWarnings("unchecked")
|
|
List<String> types = (List<String>) options.get("employmentTypes");
|
|
@SuppressWarnings("unchecked")
|
|
Map<String, Object> salaryRange = (Map<String, Object>) options.get("salaryRange");
|
|
|
|
assertTrue(cities.contains("北京"));
|
|
assertTrue(cities.contains("上海"));
|
|
assertEquals(2, cities.size());
|
|
assertTrue(types.contains("FULL_TIME"));
|
|
assertEquals(15000, salaryRange.get("min"));
|
|
assertEquals(35000, salaryRange.get("max"));
|
|
|
|
// 验证没有调用 findAll(Specification)
|
|
verify(jobRepository, never()).findAll(any(Specification.class));
|
|
}
|
|
|
|
@Test
|
|
void filters_empty_shouldReturnEmpty() {
|
|
when(jobRepository.findDistinctCitiesPublished()).thenReturn(Collections.emptyList());
|
|
when(jobRepository.findDistinctEmploymentTypesPublished()).thenReturn(Collections.emptyList());
|
|
when(jobRepository.findMinSalaryPublished()).thenReturn(null);
|
|
when(jobRepository.findMaxSalaryPublished()).thenReturn(null);
|
|
|
|
Map<String, Object> options = jobService.getFilterOptions();
|
|
|
|
@SuppressWarnings("unchecked")
|
|
List<String> cities = (List<String>) options.get("cities");
|
|
@SuppressWarnings("unchecked")
|
|
List<String> types = (List<String>) options.get("employmentTypes");
|
|
@SuppressWarnings("unchecked")
|
|
Map<String, Object> salaryRange = (Map<String, Object>) options.get("salaryRange");
|
|
|
|
assertTrue(cities.isEmpty());
|
|
assertTrue(types.isEmpty());
|
|
assertNull(salaryRange.get("min"));
|
|
assertNull(salaryRange.get("max"));
|
|
|
|
verify(jobRepository, never()).findAll(any(Specification.class));
|
|
}
|
|
|
|
// ── 19. 旧接口回归 ─────────────────────────────────────────
|
|
|
|
@Test
|
|
void listPublishedJobs_regression_shouldStillWork() {
|
|
when(jobRepository.findPublishedJobs(
|
|
isNull(), isNull(), isNull(), isNull(), isNull(), any()))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
|
|
Page<Map<String, Object>> result = jobService.listPublishedJobs(
|
|
null, null, null, null, null, 0, 20);
|
|
|
|
assertNotNull(result);
|
|
assertEquals(1, result.getTotalElements());
|
|
}
|
|
|
|
@Test
|
|
void listPublishedJobs_salaryMinGreaterThanMax_shouldStillThrow() {
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.listPublishedJobs(null, null, null, 15000, 8000, 0, 20));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test
|
|
void getPublicJob_regression_shouldStillWork() {
|
|
when(jobRepository.findById(1L)).thenReturn(Optional.of(pubJob1));
|
|
|
|
Map<String, Object> result = jobService.getPublicJob(1L);
|
|
|
|
assertNotNull(result);
|
|
assertEquals(1L, result.get("id"));
|
|
}
|
|
|
|
@Test
|
|
void getPublicJob_CLOSED_shouldStillBeVisible() {
|
|
when(jobRepository.findById(4L)).thenReturn(Optional.of(closedJob));
|
|
|
|
Map<String, Object> result = jobService.getPublicJob(4L);
|
|
|
|
assertNotNull(result);
|
|
assertEquals("CLOSED", result.get("status"));
|
|
}
|
|
|
|
@Test
|
|
void getPublicJob_DRAFT_shouldStillThrow() {
|
|
when(jobRepository.findById(3L)).thenReturn(Optional.of(draftJob));
|
|
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> jobService.getPublicJob(3L));
|
|
assertEquals(ErrorCode.JOB_NOT_FOUND.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
// ── 20. companyId 筛选 ──────────────────────────────────────
|
|
|
|
@Test
|
|
void search_companyId_filter_shouldReturn() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1));
|
|
|
|
Page<JobSearchItem> result = jobService.searchJobs(
|
|
null, null, null, null, null, 1L, null, 0, 20);
|
|
|
|
assertEquals(1, result.getTotalElements());
|
|
assertEquals(1L, result.getContent().get(0).getCompany().getId());
|
|
}
|
|
|
|
// ── 21. 分页稳定性 ──────────────────────────────────────────
|
|
|
|
@Test
|
|
void search_pagination_stability_shouldReturnSameOrder() {
|
|
when(jobRepository.findAll(any(Specification.class), any(Pageable.class)))
|
|
.thenReturn(new PageImpl<>(List.of(pubJob2, pubJob1)));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of(company1, company2));
|
|
|
|
Page<JobSearchItem> result1 = jobService.searchJobs(
|
|
null, null, null, null, null, null, "latest", 0, 20);
|
|
Page<JobSearchItem> result2 = jobService.searchJobs(
|
|
null, null, null, null, null, null, "latest", 0, 20);
|
|
|
|
assertEquals(result1.getContent().get(0).getId(), result2.getContent().get(0).getId());
|
|
assertEquals(result1.getContent().get(1).getId(), result2.getContent().get(1).getId());
|
|
}
|
|
}
|