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.
227 lines
9.7 KiB
227 lines
9.7 KiB
package com.mydreamjob.onboarding;
|
|
|
|
import com.mydreamjob.auth.JwtTokenProvider;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
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.http.MediaType;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
import org.springframework.test.context.TestPropertySource;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
|
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.request.MockMvcRequestBuilders.post;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ActiveProfiles("integration-test")
|
|
@TestPropertySource(properties =
|
|
"spring.datasource.url=jdbc:h2:mem:onboarding_privacy;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
|
|
class OnboardingPrivacyTest {
|
|
|
|
private static final AtomicLong PHONE_SEQ = new AtomicLong(973_000_000L);
|
|
|
|
@Autowired private MockMvc mockMvc;
|
|
@Autowired private JwtTokenProvider jwtTokenProvider;
|
|
@Autowired private UserRepository userRepository;
|
|
@Autowired private CompanyRepository companyRepository;
|
|
@Autowired private OnboardingChecklistItemRepository checklistItemRepository;
|
|
@Autowired private StudentOnboardingProgressRepository progressRepository;
|
|
@Autowired private CompanyKnowledgeRepository knowledgeRepository;
|
|
|
|
@MockBean private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
@BeforeEach
|
|
void clearData() {
|
|
progressRepository.deleteAll();
|
|
knowledgeRepository.deleteAll();
|
|
checklistItemRepository.deleteAll();
|
|
companyRepository.deleteAll();
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
@Test
|
|
void companyChecklistResponseDoesNotLeakPersistenceOrIdempotencyFields() throws Exception {
|
|
Long companyUser = createUser(User.Role.COMPANY);
|
|
createCompany(companyUser, "隐私企业");
|
|
|
|
String body = mockMvc.perform(post("/onboarding/company/checklist-items")
|
|
.header("Authorization", bearer(companyUser))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("""
|
|
{
|
|
"title": "隐私测试清单项",
|
|
"description": "仅返回契约字段",
|
|
"category": "ADMIN",
|
|
"sortOrder": 100,
|
|
"required": false,
|
|
"idempotencyKey": "private-key"
|
|
}
|
|
"""))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
|
|
assertThat(body).contains("itemId", "source", "itemKey", "title", "category");
|
|
assertSensitiveFieldsAbsent(body);
|
|
assertThat(body).doesNotContain("\"idempotencyKey\":", "\"requestHash\":",
|
|
"\"scopeId\":", "\"scopeType\":", "\"companyId\":");
|
|
}
|
|
|
|
@Test
|
|
void studentChecklistResponseDoesNotLeakStudentOrPersistenceIdentifiers() throws Exception {
|
|
Long companyUser = createUser(User.Role.COMPANY);
|
|
Long companyId = createCompany(companyUser, "学生隐私企业");
|
|
Long student = createUser(User.Role.STUDENT);
|
|
OnboardingChecklistItem item = checklistItemRepository.save(OnboardingChecklistItem.builder()
|
|
.scopeType("STANDARD")
|
|
.scopeId(0L)
|
|
.companyId(null)
|
|
.itemKey("privacy-standard")
|
|
.title("隐私通用项")
|
|
.description("说明")
|
|
.category("DOCUMENT")
|
|
.sortOrder(10)
|
|
.required(true)
|
|
.active(true)
|
|
.build());
|
|
progressRepository.save(StudentOnboardingProgress.builder()
|
|
.studentUserId(student)
|
|
.itemId(item.getId())
|
|
.completed(true)
|
|
.completedAt(LocalDateTime.now())
|
|
.build());
|
|
|
|
String body = mockMvc.perform(get("/onboarding/companies/{companyId}/checklist", companyId)
|
|
.header("Authorization", bearer(student)))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
|
|
assertThat(body).contains("companyId", "companyName", "items", "progressVersion");
|
|
assertSensitiveFieldsAbsent(body);
|
|
assertThat(body).doesNotContain("\"studentUserId\":", "\"passwordHash\":",
|
|
"\"requestHash\":", "\"idempotencyKey\":");
|
|
}
|
|
|
|
@Test
|
|
void studentKnowledgeOverviewDoesNotLeakOwnerOrModerationInternals() throws Exception {
|
|
Long companyUser = createUser(User.Role.COMPANY);
|
|
Long companyId = createCompany(companyUser, "知识隐私企业");
|
|
Long student = createUser(User.Role.STUDENT);
|
|
knowledgeRepository.save(CompanyKnowledge.builder()
|
|
.companyId(companyId)
|
|
.culture("文化")
|
|
.workPolicies("制度")
|
|
.onboardingGuide("指南")
|
|
.build());
|
|
|
|
String body = mockMvc.perform(get("/onboarding/companies/{companyId}/knowledge", companyId)
|
|
.header("Authorization", bearer(student)))
|
|
.andExpect(status().isOk())
|
|
.andReturn().getResponse().getContentAsString();
|
|
|
|
assertThat(body).contains("knowledgeState", "company", "reviewSummary");
|
|
assertSensitiveFieldsAbsent(body);
|
|
assertThat(body).doesNotContain("\"companyId\":", "\"ownerUserId\":",
|
|
"\"moderationReason\":", "\"reviewHash\":");
|
|
}
|
|
|
|
@Test
|
|
void crossCompanyErrorDoesNotEchoTargetContentOrHashes() throws Exception {
|
|
Long ownerA = createUser(User.Role.COMPANY);
|
|
Long companyA = createCompany(ownerA, "内容企业A");
|
|
Long ownerB = createUser(User.Role.COMPANY);
|
|
createCompany(ownerB, "内容企业B");
|
|
OnboardingChecklistItem item = checklistItemRepository.save(OnboardingChecklistItem.builder()
|
|
.scopeType("COMPANY")
|
|
.scopeId(companyA)
|
|
.companyId(companyA)
|
|
.itemKey("private-target-item")
|
|
.title("不可泄露的目标标题")
|
|
.description("不可泄露的说明")
|
|
.category("ADMIN")
|
|
.sortOrder(1)
|
|
.required(false)
|
|
.active(true)
|
|
.idempotencyKey("private-target-key")
|
|
.requestHash("b".repeat(64))
|
|
.build());
|
|
|
|
String body = mockMvc.perform(put("/onboarding/company/checklist-items/{itemId}", item.getId())
|
|
.header("Authorization", bearer(ownerB))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("""
|
|
{
|
|
"title": "越权请求",
|
|
"category": "ADMIN",
|
|
"sortOrder": 1,
|
|
"required": false,
|
|
"active": true,
|
|
"version": 0
|
|
}
|
|
"""))
|
|
.andExpect(status().isNotFound())
|
|
.andReturn().getResponse().getContentAsString();
|
|
|
|
assertThat(body).doesNotContain("不可泄露的目标标题", "不可泄露的说明",
|
|
"private-target-key", "b".repeat(64), "private-target-item");
|
|
assertSensitiveFieldsAbsent(body);
|
|
}
|
|
|
|
private void assertSensitiveFieldsAbsent(String body) {
|
|
assertThat(body).doesNotContain(
|
|
"ownerUserId",
|
|
"studentUserId",
|
|
"requestHash",
|
|
"idempotencyKey",
|
|
"moderationReason",
|
|
"passwordHash",
|
|
"ipAddress",
|
|
"userAgent",
|
|
"\"token\":",
|
|
"\"accessToken\":");
|
|
}
|
|
|
|
private Long createCompany(Long ownerUserId, String name) {
|
|
return companyRepository.save(Company.builder()
|
|
.ownerUserId(ownerUserId)
|
|
.name(name)
|
|
.description("公司描述")
|
|
.industry("软件")
|
|
.website("https://example.com")
|
|
.address("上海")
|
|
.verified(true)
|
|
.build()).getId();
|
|
}
|
|
|
|
private Long createUser(User.Role role) {
|
|
return userRepository.save(User.builder()
|
|
.phone(String.valueOf(PHONE_SEQ.getAndIncrement()))
|
|
.nickname(role.name())
|
|
.role(role)
|
|
.roles(Set.of(role))
|
|
.active(true)
|
|
.verified(true)
|
|
.build()).getId();
|
|
}
|
|
|
|
private String bearer(Long userId) {
|
|
return "Bearer " + jwtTokenProvider.generateAccessToken(
|
|
userId, String.valueOf(userId), Map.of("roles", "STUDENT"));
|
|
}
|
|
}
|