forked from baskbuddy/zhangshun
14 changed files with 679 additions and 15 deletions
@ -0,0 +1,30 @@ |
|||||
|
package com.mydreamjob.job; |
||||
|
|
||||
|
import jakarta.persistence.criteria.Predicate; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Locale; |
||||
|
import org.springframework.data.jpa.domain.Specification; |
||||
|
|
||||
|
/** 企业私有岗位列表查询;租户条件始终是必选条件。 */ |
||||
|
public final class CompanyJobManagementSpecification { |
||||
|
private CompanyJobManagementSpecification() {} |
||||
|
|
||||
|
public static Specification<Job> forCompany(long companyId, String status, String keyword, String city) { |
||||
|
return (root, query, cb) -> { |
||||
|
List<Predicate> predicates = new ArrayList<>(); |
||||
|
predicates.add(cb.equal(root.get("companyId"), companyId)); |
||||
|
if (status != null) predicates.add(cb.equal(root.get("status"), status)); |
||||
|
if (keyword != null) { |
||||
|
// LOCATE treats % and _ literally and is supported by both H2 and MySQL.
|
||||
|
predicates.add(cb.greaterThan(cb.locate( |
||||
|
cb.lower(root.get("title")), keyword.toLowerCase(Locale.ROOT)), 0)); |
||||
|
} |
||||
|
if (city != null) { |
||||
|
predicates.add(cb.equal(cb.lower(root.get("city")), city.toLowerCase(Locale.ROOT))); |
||||
|
} |
||||
|
return cb.and(predicates.toArray(Predicate[]::new)); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
package com.mydreamjob.job.dto; |
||||
|
|
||||
|
import com.mydreamjob.job.Job; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import java.time.LocalDateTime; |
||||
|
|
||||
|
/** 企业自身岗位管理列表项,不包含招聘统计、候选人或内部审核数据。 */ |
||||
|
@Schema(name = "CompanyManagedJobItem", description = "企业自身岗位管理安全摘要") |
||||
|
public record CompanyManagedJobItem( |
||||
|
@Schema(description = "岗位 ID") Long id, |
||||
|
@Schema(description = "所属企业 ID") Long companyId, |
||||
|
@Schema(description = "岗位名称") String title, |
||||
|
@Schema(description = "岗位描述") String description, |
||||
|
@Schema(description = "任职要求") String requirements, |
||||
|
@Schema(description = "工作城市") String city, |
||||
|
@Schema(description = "用工类型") String employmentType, |
||||
|
@Schema(description = "最低月薪,单位元") Integer salaryMin, |
||||
|
@Schema(description = "最高月薪,单位元") Integer salaryMax, |
||||
|
@Schema(description = "岗位状态:DRAFT、PUBLISHED、CLOSED") String status, |
||||
|
@Schema(description = "发布时间") LocalDateTime publishedAt, |
||||
|
@Schema(description = "创建时间") LocalDateTime createdAt, |
||||
|
@Schema(description = "更新时间") LocalDateTime updatedAt) { |
||||
|
|
||||
|
public static CompanyManagedJobItem from(Job job) { |
||||
|
return new CompanyManagedJobItem(job.getId(), job.getCompanyId(), job.getTitle(), |
||||
|
job.getDescription(), job.getRequirements(), job.getCity(), job.getEmploymentType(), |
||||
|
job.getSalaryMin(), job.getSalaryMax(), job.getStatus(), job.getPublishedAt(), |
||||
|
job.getCreatedAt(), job.getUpdatedAt()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,235 @@ |
|||||
|
package com.mydreamjob.company; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.mydreamjob.audit.AuditContext; |
||||
|
import com.mydreamjob.auth.AuthSessionService; |
||||
|
import com.mydreamjob.auth.User; |
||||
|
import com.mydreamjob.auth.UserRepository; |
||||
|
import com.mydreamjob.job.Job; |
||||
|
import com.mydreamjob.job.JobRepository; |
||||
|
import java.time.Instant; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
import org.junit.jupiter.api.AfterEach; |
||||
|
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.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.test.context.ActiveProfiles; |
||||
|
import org.springframework.test.context.TestPropertySource; |
||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
/** 企业岗位管理列表:租户隔离、状态/关键词/城市过滤、分页及只读行为。 */ |
||||
|
@SpringBootTest |
||||
|
@AutoConfigureMockMvc |
||||
|
@ActiveProfiles("integration-test") |
||||
|
@TestPropertySource(properties = "spring.datasource.url=jdbc:h2:mem:company_job_management;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") |
||||
|
class CompanyJobManagementIntegrationTest { |
||||
|
private static final AtomicInteger SEQUENCE = new AtomicInteger(); |
||||
|
|
||||
|
@MockBean private RedisTemplate<String, Object> redis; |
||||
|
@Autowired private MockMvc mvc; |
||||
|
@Autowired private JdbcTemplate jdbc; |
||||
|
@Autowired private UserRepository users; |
||||
|
@Autowired private CompanyRepository companies; |
||||
|
@Autowired private JobRepository jobs; |
||||
|
@Autowired private AuthSessionService sessions; |
||||
|
@Autowired private ObjectMapper mapper; |
||||
|
|
||||
|
private long companyId; |
||||
|
private long otherCompanyId; |
||||
|
private String companyToken; |
||||
|
private String otherCompanyToken; |
||||
|
private String studentToken; |
||||
|
private String adminToken; |
||||
|
private long draftId; |
||||
|
private long publishedId; |
||||
|
private long closedId; |
||||
|
|
||||
|
@BeforeEach |
||||
|
void setUp() { |
||||
|
Identity owner = identity(User.Role.COMPANY); |
||||
|
Identity otherOwner = identity(User.Role.COMPANY); |
||||
|
Identity student = identity(User.Role.STUDENT); |
||||
|
Identity admin = identity(User.Role.ADMIN); |
||||
|
companyToken = owner.token(); |
||||
|
otherCompanyToken = otherOwner.token(); |
||||
|
studentToken = student.token(); |
||||
|
adminToken = admin.token(); |
||||
|
companyId = companies.saveAndFlush(Company.builder().ownerUserId(owner.id()) |
||||
|
.name("岗位列表测试企业A").verified(false).build()).getId(); |
||||
|
otherCompanyId = companies.saveAndFlush(Company.builder().ownerUserId(otherOwner.id()) |
||||
|
.name("岗位列表测试企业B").verified(true).build()).getId(); |
||||
|
draftId = job(companyId, "Java%工程师", "南京", "DRAFT"); |
||||
|
publishedId = job(companyId, "Python工程师", "北京", "PUBLISHED"); |
||||
|
closedId = job(companyId, "Java工程师", "南京", "CLOSED"); |
||||
|
job(otherCompanyId, "Java外部岗位", "南京", "PUBLISHED"); |
||||
|
jdbc.update("update jobs set created_at=? where company_id in (?, ?)", |
||||
|
LocalDateTime.of(2026, 10, 10, 12, 0), companyId, otherCompanyId); |
||||
|
} |
||||
|
|
||||
|
@AfterEach |
||||
|
void tearDown() { |
||||
|
jdbc.update("delete from jobs where company_id in (?, ?)", companyId, otherCompanyId); |
||||
|
jdbc.update("delete from companies where id in (?, ?)", companyId, otherCompanyId); |
||||
|
jdbc.update("delete from auth_tokens where user_id in (select id from users where phone like '1990099%')"); |
||||
|
jdbc.update("delete from user_roles where user_id in (select id from users where phone like '1990099%')"); |
||||
|
jdbc.update("delete from users where phone like '1990099%'"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void companyCanListAllOwnStatusesWithTypedPageAndStablePagination() throws Exception { |
||||
|
String before = jobSnapshot(); |
||||
|
long auditsBefore = count("audit_logs"); |
||||
|
long notificationsBefore = count("notifications"); |
||||
|
long messagesBefore = count("messages"); |
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.code").value(200)) |
||||
|
.andExpect(jsonPath("$.data.total").value(3)) |
||||
|
.andExpect(jsonPath("$.data.page").value(0)) |
||||
|
.andExpect(jsonPath("$.data.size").value(20)) |
||||
|
.andExpect(jsonPath("$.data.totalPages").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items.length()").value(3)); |
||||
|
JsonNode first = mapper.readTree(mvc.perform(get("/companies/me/jobs?page=0&size=1") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray()).path("data"); |
||||
|
JsonNode second = mapper.readTree(mvc.perform(get("/companies/me/jobs?page=1&size=1") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray()).path("data"); |
||||
|
assertThat(first.path("total").asLong()).isEqualTo(3); |
||||
|
assertThat(second.path("total").asLong()).isEqualTo(3); |
||||
|
assertThat(first.path("items").get(0).path("id").asLong()) |
||||
|
.isNotEqualTo(second.path("items").get(0).path("id").asLong()); |
||||
|
assertThat(first.path("items").get(0).path("id").asLong()) |
||||
|
.isEqualTo(Math.max(draftId, Math.max(publishedId, closedId))); |
||||
|
assertThat(jobSnapshot()).isEqualTo(before); |
||||
|
assertThat(count("audit_logs")).isEqualTo(auditsBefore); |
||||
|
assertThat(count("notifications")).isEqualTo(notificationsBefore); |
||||
|
assertThat(count("messages")).isEqualTo(messagesBefore); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void statusKeywordCityFiltersAreAppliedTogetherAndWildcardIsLiteral() throws Exception { |
||||
|
mvc.perform(get("/companies/me/jobs").param("status", "DRAFT") |
||||
|
.param("keyword", "%").param("city", "南京") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.data.total").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(draftId)) |
||||
|
.andExpect(jsonPath("$.data.items[0].status").value("DRAFT")) |
||||
|
.andExpect(jsonPath("$.data.items[0].salaryMin").value(12000)); |
||||
|
mvc.perform(get("/companies/me/jobs?status=REJECTED") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isBadRequest()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void tenantScopeAppliesToContentAndCountAndAnonymousOrNonCompanyRemainDenied() throws Exception { |
||||
|
mvc.perform(get("/companies/me/jobs?status=PUBLISHED&companyId=" + otherCompanyId) |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.data.total").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(publishedId)) |
||||
|
.andExpect(jsonPath("$.data.items[0].companyId").value(companyId)); |
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(otherCompanyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.total").value(1)); |
||||
|
mvc.perform(get("/companies/me/jobs")).andExpect(status().isUnauthorized()); |
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(studentToken))) |
||||
|
.andExpect(status().isForbidden()); |
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(adminToken))) |
||||
|
.andExpect(status().isForbidden()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void rejectsInvalidPagingAndDoesNotExposeAnotherTenant() throws Exception { |
||||
|
mvc.perform(get("/companies/me/jobs?page=-1").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isBadRequest()); |
||||
|
mvc.perform(get("/companies/me/jobs?size=101").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isBadRequest()); |
||||
|
mvc.perform(get("/companies/me/jobs?size=0").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isBadRequest()); |
||||
|
String body = mvc.perform(get("/companies/me/jobs?size=1&page=99") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.items.length()").value(0)) |
||||
|
.andExpect(jsonPath("$.data.total").value(3)).andReturn().getResponse().getContentAsString(); |
||||
|
assertThat(body).doesNotContain("Java外部岗位").doesNotContain("1990099"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void closedJobsRemainClosedAndPublicListDoesNotExposeDraftOrClosed() throws Exception { |
||||
|
mvc.perform(get("/companies/me/jobs?status=CLOSED").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.total").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(closedId)); |
||||
|
mvc.perform(get("/jobs?companyId=" + companyId)) |
||||
|
.andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.data.items[?(@.status == 'DRAFT')]").doesNotExist()) |
||||
|
.andExpect(jsonPath("$.data.items[?(@.status == 'CLOSED')]").doesNotExist()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void openApiDocumentsThePrivateManagementRouteAndTypedResponse() throws Exception { |
||||
|
JsonNode openApi = mapper.readTree(mvc.perform(get("/v3/api-docs")) |
||||
|
.andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray()); |
||||
|
JsonNode operation = openApi.path("paths").path("/companies/me/jobs").path("get"); |
||||
|
assertThat(operation.path("operationId").asText()).isEqualTo("getMyJobs"); |
||||
|
assertThat(operation.path("security").toString()).contains("bearerAuth"); |
||||
|
assertThat(operation.path("parameters").toString()).contains("status", "keyword", "city", "page", "size"); |
||||
|
assertThat(operation.path("responses").path("200").path("content").path("*/*") |
||||
|
.path("schema").path("$ref").asText()).contains("ApiResponsePageResponseCompanyManagedJobItem"); |
||||
|
JsonNode itemProperties = openApi.path("components").path("schemas") |
||||
|
.path("CompanyManagedJobItem").path("properties"); |
||||
|
assertThat(itemProperties.has("status")).isTrue(); |
||||
|
assertThat(itemProperties.has("salaryMin")).isTrue(); |
||||
|
assertThat(itemProperties.has("salaryMax")).isTrue(); |
||||
|
assertThat(itemProperties.has("companyId")).isTrue(); |
||||
|
} |
||||
|
|
||||
|
private Identity identity(User.Role role) { |
||||
|
int sequence = SEQUENCE.incrementAndGet(); |
||||
|
User user = users.saveAndFlush(User.builder() |
||||
|
.phone("1990099" + String.format("%04d", sequence)) |
||||
|
.role(role).roles(new HashSet<>(Set.of(role))).active(true).verified(true).build()); |
||||
|
var token = sessions.createSession(user, Instant.now(), new AuditContext()).accessToken(); |
||||
|
return new Identity(user.getId(), token); |
||||
|
} |
||||
|
|
||||
|
private long job(long ownerCompanyId, String title, String city, String status) { |
||||
|
return jobs.saveAndFlush(Job.builder().companyId(ownerCompanyId).title(title) |
||||
|
.description("岗位描述").requirements("岗位要求").city(city).employmentType("FULL_TIME") |
||||
|
.salaryMin(12000).salaryMax(18000).status(status).build()).getId(); |
||||
|
} |
||||
|
|
||||
|
private String jobSnapshot() { |
||||
|
return jdbc.query("select id,company_id,title,description,requirements,city,employment_type,salary_min,salary_max,status,published_at,created_at,updated_at from jobs order by id", |
||||
|
rs -> { |
||||
|
StringBuilder snapshot = new StringBuilder(); |
||||
|
while (rs.next()) snapshot.append(rs.getString(1)).append('|').append(rs.getString(2)) |
||||
|
.append('|').append(rs.getString(3)).append('|').append(rs.getString(4)) |
||||
|
.append('|').append(rs.getString(5)).append('|').append(rs.getString(6)) |
||||
|
.append('|').append(rs.getString(7)).append('|').append(rs.getString(8)) |
||||
|
.append('|').append(rs.getString(9)).append('|').append(rs.getString(10)) |
||||
|
.append('|').append(rs.getString(11)).append('|').append(rs.getString(12)) |
||||
|
.append('|').append(rs.getString(13)).append('\n'); |
||||
|
return snapshot.toString(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private long count(String table) { |
||||
|
return jdbc.queryForObject("select count(*) from " + table, Long.class); |
||||
|
} |
||||
|
|
||||
|
private String bearer(String token) { return "Bearer " + token; } |
||||
|
private record Identity(long id, String token) {} |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
package com.mydreamjob.integration; |
||||
|
|
||||
|
import com.mydreamjob.audit.AuditContext; |
||||
|
import com.mydreamjob.auth.AuthSessionService; |
||||
|
import com.mydreamjob.auth.User; |
||||
|
import com.mydreamjob.auth.UserRepository; |
||||
|
import com.mydreamjob.company.Company; |
||||
|
import com.mydreamjob.company.CompanyRepository; |
||||
|
import com.mydreamjob.job.Job; |
||||
|
import com.mydreamjob.job.JobRepository; |
||||
|
import java.sql.Timestamp; |
||||
|
import java.time.Instant; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
import org.junit.jupiter.api.AfterEach; |
||||
|
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.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.test.context.ActiveProfiles; |
||||
|
import org.springframework.test.context.DynamicPropertyRegistry; |
||||
|
import org.springframework.test.context.DynamicPropertySource; |
||||
|
import org.springframework.test.web.servlet.MockMvc; |
||||
|
import org.testcontainers.containers.MySQLContainer; |
||||
|
import org.testcontainers.junit.jupiter.Container; |
||||
|
import org.testcontainers.junit.jupiter.Testcontainers; |
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
/** 企业私有岗位管理接口在真实 MySQL 上的租户、分页、筛选和只读验收。 */ |
||||
|
@Testcontainers(disabledWithoutDocker = false) |
||||
|
@SpringBootTest |
||||
|
@AutoConfigureMockMvc |
||||
|
@ActiveProfiles("mysql-it") |
||||
|
class CompanyJobManagementMySqlTest { |
||||
|
@Container |
||||
|
static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.4") |
||||
|
.withDatabaseName("company_job_management_test") |
||||
|
.withUsername("company_job_test") |
||||
|
.withPassword("company-job-test-only"); |
||||
|
|
||||
|
@DynamicPropertySource |
||||
|
static void database(DynamicPropertyRegistry registry) { |
||||
|
registry.add("spring.datasource.url", MYSQL::getJdbcUrl); |
||||
|
registry.add("spring.datasource.username", MYSQL::getUsername); |
||||
|
registry.add("spring.datasource.password", MYSQL::getPassword); |
||||
|
} |
||||
|
|
||||
|
private static final AtomicInteger SEQUENCE = new AtomicInteger(); |
||||
|
@MockBean private RedisTemplate<String, Object> redis; |
||||
|
@Autowired private MockMvc mvc; |
||||
|
@Autowired private JdbcTemplate jdbc; |
||||
|
@Autowired private UserRepository users; |
||||
|
@Autowired private CompanyRepository companies; |
||||
|
@Autowired private JobRepository jobs; |
||||
|
@Autowired private AuthSessionService sessions; |
||||
|
|
||||
|
private final List<Long> userIds = new ArrayList<>(); |
||||
|
private final List<Long> companyIds = new ArrayList<>(); |
||||
|
private long companyId; |
||||
|
private long otherCompanyId; |
||||
|
private long draftId; |
||||
|
private long publishedId; |
||||
|
private long closedId; |
||||
|
private String companyToken; |
||||
|
private String studentToken; |
||||
|
|
||||
|
@BeforeEach |
||||
|
void fixtures() { |
||||
|
assertThat(jdbc.queryForObject("select version()", String.class)).startsWith("8."); |
||||
|
Identity company = identity(User.Role.COMPANY); |
||||
|
Identity otherCompany = identity(User.Role.COMPANY); |
||||
|
Identity student = identity(User.Role.STUDENT); |
||||
|
companyToken = company.token(); |
||||
|
studentToken = student.token(); |
||||
|
companyId = company(company.userId(), false); |
||||
|
otherCompanyId = company(otherCompany.userId(), true); |
||||
|
draftId = job(companyId, "Java%工程师", "南京", "DRAFT"); |
||||
|
publishedId = job(companyId, "Python工程师", "北京", "PUBLISHED"); |
||||
|
closedId = job(companyId, "Java工程师", "南京", "CLOSED"); |
||||
|
job(otherCompanyId, "Java其他企业岗位", "南京", "PUBLISHED"); |
||||
|
jdbc.update("update jobs set created_at=? where company_id in (?, ?)", |
||||
|
Timestamp.valueOf(LocalDateTime.of(2026, 10, 10, 12, 0)), companyId, otherCompanyId); |
||||
|
} |
||||
|
|
||||
|
@AfterEach |
||||
|
void cleanup() { |
||||
|
for (long id : companyIds) jdbc.update("delete from jobs where company_id=?", id); |
||||
|
for (long id : companyIds) { |
||||
|
jdbc.update("delete from company_verifications where company_id=?", id); |
||||
|
jdbc.update("delete from companies where id=?", id); |
||||
|
} |
||||
|
for (long id : userIds) jdbc.update("delete from auth_tokens where user_id=?", id); |
||||
|
for (long id : userIds) jdbc.update("delete from user_roles where user_id=?", id); |
||||
|
for (long id : userIds) jdbc.update("delete from users where id=?", id); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void realMysqlListsAllOwnStatusesAndFiltersWithoutWritesOrCrossTenantLeakage() throws Exception { |
||||
|
String before = snapshot(companyId); |
||||
|
long auditsBefore = count("audit_logs"); |
||||
|
long notificationsBefore = count("notifications"); |
||||
|
long messagesBefore = count("messages"); |
||||
|
|
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.total").value(3)) |
||||
|
.andExpect(jsonPath("$.data.items.length()").value(3)); |
||||
|
mvc.perform(get("/companies/me/jobs").param("status", "DRAFT") |
||||
|
.param("keyword", "%").param("city", "南京") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.total").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(draftId)); |
||||
|
mvc.perform(get("/companies/me/jobs").param("status", "CLOSED") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.items[0].id").value(closedId)); |
||||
|
mvc.perform(get("/companies/me/jobs").param("status", "PUBLISHED") |
||||
|
.param("companyId", Long.toString(otherCompanyId)) |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.total").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(publishedId)) |
||||
|
.andExpect(jsonPath("$.data.items[0].companyId").value(companyId)); |
||||
|
mvc.perform(get("/companies/me/jobs").param("page", "0").param("size", "1") |
||||
|
.header("Authorization", bearer(companyToken))) |
||||
|
.andExpect(status().isOk()).andExpect(jsonPath("$.data.items.length()").value(1)) |
||||
|
.andExpect(jsonPath("$.data.items[0].id").value(Math.max(draftId, Math.max(publishedId, closedId)))); |
||||
|
mvc.perform(get("/companies/me/jobs").header("Authorization", bearer(studentToken))) |
||||
|
.andExpect(status().isForbidden()); |
||||
|
mvc.perform(get("/companies/me/jobs")).andExpect(status().isUnauthorized()); |
||||
|
mvc.perform(get("/companies/" + companyId + "/jobs")) |
||||
|
.andExpect(status().isOk()) |
||||
|
.andExpect(jsonPath("$.data.items[?(@.id == " + draftId + ")]").doesNotExist()) |
||||
|
.andExpect(jsonPath("$.data.items[?(@.id == " + closedId + ")]").doesNotExist()); |
||||
|
|
||||
|
assertThat(snapshot(companyId)).isEqualTo(before); |
||||
|
assertThat(count("audit_logs")).isEqualTo(auditsBefore); |
||||
|
assertThat(count("notifications")).isEqualTo(notificationsBefore); |
||||
|
assertThat(count("messages")).isEqualTo(messagesBefore); |
||||
|
} |
||||
|
|
||||
|
private Identity identity(User.Role role) { |
||||
|
int sequence = SEQUENCE.incrementAndGet(); |
||||
|
User user = users.saveAndFlush(User.builder() |
||||
|
.phone("1990098" + String.format("%04d", sequence)) |
||||
|
.role(role).roles(new HashSet<>(Set.of(role))).active(true).verified(true).build()); |
||||
|
userIds.add(user.getId()); |
||||
|
String token = sessions.createSession(user, Instant.now(), new AuditContext()).accessToken(); |
||||
|
return new Identity(user.getId(), token); |
||||
|
} |
||||
|
|
||||
|
private long company(long ownerUserId, boolean verified) { |
||||
|
Company value = companies.saveAndFlush(Company.builder().ownerUserId(ownerUserId) |
||||
|
.name("临时岗位管理企业").verified(verified).build()); |
||||
|
companyIds.add(value.getId()); |
||||
|
return value.getId(); |
||||
|
} |
||||
|
|
||||
|
private long job(long ownerCompanyId, String title, String city, String status) { |
||||
|
return jobs.saveAndFlush(Job.builder().companyId(ownerCompanyId).title(title) |
||||
|
.description("临时岗位描述").requirements("临时岗位要求").city(city) |
||||
|
.employmentType("FULL_TIME").salaryMin(12000).salaryMax(18000).status(status).build()).getId(); |
||||
|
} |
||||
|
|
||||
|
private long count(String table) { |
||||
|
return jdbc.queryForObject("select count(*) from " + table, Long.class); |
||||
|
} |
||||
|
|
||||
|
private String snapshot(long ownerCompanyId) { |
||||
|
return jdbc.query("select id,company_id,title,description,requirements,city,employment_type,salary_min,salary_max,status,published_at,created_at,updated_at from jobs where company_id=? order by id", |
||||
|
rs -> { |
||||
|
StringBuilder result = new StringBuilder(); |
||||
|
while (rs.next()) for (int column = 1; column <= 13; column++) |
||||
|
result.append(rs.getString(column)).append(column == 13 ? '\n' : '|'); |
||||
|
return result.toString(); |
||||
|
}, ownerCompanyId); |
||||
|
} |
||||
|
|
||||
|
private static String bearer(String token) { return "Bearer " + token; } |
||||
|
private record Identity(long userId, String token) {} |
||||
|
} |
||||
Loading…
Reference in new issue