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.
194 lines
8.2 KiB
194 lines
8.2 KiB
package com.mydreamjob.deletion;
|
|
|
|
import com.mydreamjob.auth.JwtTokenProvider;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
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.http.MediaType;
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
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.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
|
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.jsonPath;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
/**
|
|
* 注销守卫拦截器集成测试。
|
|
*
|
|
* <p>覆盖:PENDING 阻断 POST/PUT/PATCH/DELETE(9302),GET/白名单放行,
|
|
* NONE 放行,COMPLETED 普通业务接口返回 9303,注销接口白名单放行。
|
|
*/
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ActiveProfiles("integration-test")
|
|
class DeletionGuardInterceptorTest {
|
|
|
|
@Autowired private MockMvc mockMvc;
|
|
@Autowired private JwtTokenProvider jwtTokenProvider;
|
|
@Autowired private UserRepository userRepository;
|
|
@Autowired private AccountDeletionRepository deletionRepository;
|
|
|
|
@MockBean private org.springframework.data.redis.core.RedisTemplate<String, Object> redisTemplate;
|
|
|
|
private static final AtomicLong PHONE = new AtomicLong(932_000_000L);
|
|
|
|
@BeforeEach
|
|
@SuppressWarnings("unchecked")
|
|
void setUp() {
|
|
org.springframework.data.redis.core.ValueOperations<String, Object> ops =
|
|
mock(org.springframework.data.redis.core.ValueOperations.class);
|
|
when(redisTemplate.opsForValue()).thenReturn(ops);
|
|
when(redisTemplate.hasKey(anyString())).thenReturn(false);
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
// 清理共享 H2 库(integrationdb)中的注销记录,避免残留 PENDING/COMPLETED
|
|
// 污染后续其他集成测试类(其使用固定 userId 触发注销守卫)。
|
|
deletionRepository.deleteAll();
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
private long createStudent() {
|
|
User u = userRepository.save(User.builder()
|
|
.phone(String.valueOf(PHONE.getAndIncrement()))
|
|
.roles(Set.of(User.Role.STUDENT))
|
|
.role(User.Role.STUDENT)
|
|
.active(true)
|
|
.build());
|
|
return u.getId();
|
|
}
|
|
|
|
private String token(long uid) {
|
|
return com.mydreamjob.auth.support.TestAuthTokens.bearer(uid, "STUDENT");
|
|
}
|
|
|
|
private void setStatus(long uid, DeletionStatus status, LocalDateTime effectiveAt) {
|
|
deletionRepository.save(AccountDeletion.builder()
|
|
.userId(uid).status(status).effectiveAt(effectiveAt).version(0L).build());
|
|
}
|
|
|
|
@Test
|
|
void pending_blocksWrites_returns9302() throws Exception {
|
|
long uid = createStudent();
|
|
setStatus(uid, DeletionStatus.PENDING, LocalDateTime.now().plusDays(30));
|
|
|
|
mockMvc.perform(put("/profile/me/privacy")
|
|
.header("Authorization", token(uid))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("{\"resumeVisibility\":\"PRIVATE\",\"contactVisible\":false}"))
|
|
.andExpect(status().isConflict())
|
|
.andExpect(jsonPath("$.code").value(9302));
|
|
|
|
mockMvc.perform(put("/profile/me")
|
|
.header("Authorization", token(uid))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("{}"))
|
|
.andExpect(status().isConflict())
|
|
.andExpect(jsonPath("$.code").value(9302));
|
|
|
|
mockMvc.perform(post("/applications")
|
|
.header("Authorization", token(uid)))
|
|
.andExpect(status().isConflict())
|
|
.andExpect(jsonPath("$.code").value(9302));
|
|
}
|
|
|
|
@Test
|
|
void pending_allowsGet_andDeletionWhitelist() throws Exception {
|
|
long uid = createStudent();
|
|
setStatus(uid, DeletionStatus.PENDING, LocalDateTime.now().plusDays(30));
|
|
|
|
mockMvc.perform(get("/profile/me/deletion").header("Authorization", token(uid)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
|
|
|
// GET 默认放行
|
|
mockMvc.perform(get("/profile/me/privacy").header("Authorization", token(uid)))
|
|
.andExpect(status().isOk());
|
|
}
|
|
|
|
@Test
|
|
void noneUser_isNotBlocked() throws Exception {
|
|
long uid = createStudent();
|
|
|
|
mockMvc.perform(put("/profile/me/privacy")
|
|
.header("Authorization", token(uid))
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("{\"resumeVisibility\":\"PRIVATE\",\"contactVisible\":false}"))
|
|
.andExpect(status().isOk());
|
|
|
|
mockMvc.perform(get("/profile/me/privacy").header("Authorization", token(uid)))
|
|
.andExpect(status().isOk());
|
|
}
|
|
|
|
@Test
|
|
void completed_blocksOrdinaryBusiness_with9303() throws Exception {
|
|
long uid = createStudent();
|
|
setStatus(uid, DeletionStatus.COMPLETED, null);
|
|
|
|
// 普通业务 GET 同样被 9303 拒绝(旧 JWT 即便未过期也失效)
|
|
mockMvc.perform(get("/profile/me/privacy").header("Authorization", token(uid)))
|
|
.andExpect(status().isConflict())
|
|
.andExpect(jsonPath("$.code").value(9303));
|
|
|
|
mockMvc.perform(get("/profile/me/deletion").header("Authorization", token(uid)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data.status").value("COMPLETED"));
|
|
|
|
mockMvc.perform(post("/profile/me/deletion").header("Authorization", token(uid)))
|
|
.andExpect(status().isConflict())
|
|
.andExpect(jsonPath("$.code").value(9301));
|
|
}
|
|
|
|
@Test
|
|
void unauthenticated_isNotBlockedByInterceptor() throws Exception {
|
|
mockMvc.perform(get("/profile/me/privacy"))
|
|
.andExpect(status().isUnauthorized());
|
|
}
|
|
|
|
/**
|
|
* R01-A 回归:本拦截器注册在 {@code /**} 上,先于所有 Controller 执行,
|
|
* 因此非法主体绝不能在拦截器层逃逸为 500。
|
|
*
|
|
* <p>修复前 {@code Long.valueOf(auth.getName())} 对 Long 溢出主体(
|
|
* {@code "99999999999999999999"})抛出 {@code NumberFormatException},
|
|
* 在到达任何 Controller 之前就返回全站 HTTP 500。</p>
|
|
*/
|
|
@Test
|
|
void illegalOrOverflowingPrincipal_returns401InsteadOf500() throws Exception {
|
|
for (String principal : new String[]{
|
|
"99999999999999999999", "٤٢", "+42", "-1", "0", " ", "not-a-number"}) {
|
|
mockMvc.perform(get("/profile/me/privacy")
|
|
.with(authentication(UsernamePasswordAuthenticationToken.authenticated(
|
|
principal, null, java.util.List.of()))))
|
|
.andExpect(status().isUnauthorized());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void options_isAllowed() throws Exception {
|
|
long uid = createStudent();
|
|
setStatus(uid, DeletionStatus.PENDING, LocalDateTime.now().plusDays(30));
|
|
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
|
.options("/profile/me/privacy").header("Authorization", token(uid)))
|
|
.andExpect(status().isOk());
|
|
}
|
|
}
|