|
|
@@ -0,0 +1,149 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.guide.TrainingPlanController;
|
|
|
+import com.etotem.cfc.entity.TrainingPlan;
|
|
|
+import com.etotem.cfc.entity.TrainingPlanItem;
|
|
|
+import com.etotem.cfc.service.TrainingPlanService;
|
|
|
+import org.junit.jupiter.api.AfterEach;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
|
|
+import org.springframework.context.annotation.Configuration;
|
|
|
+import org.springframework.security.access.AccessDeniedException;
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
|
|
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
+
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * B-9:guide.trainingPlan.getPlanDetail / getPlanList 的「规划师角色约束」落地验证。
|
|
|
+ *
|
|
|
+ * 修复前:两个 handler 不读取 role,任何已认证角色(含匿名 userId=-1)均可调用。
|
|
|
+ * 修复后:handler 上声明 @PreAuthorize("hasAnyRole('teacher','admin')"),
|
|
|
+ * 由 {@code SecurityConfig#@EnableGlobalMethodSecurity} 建立的方法级安全代理拦截;
|
|
|
+ * 无权限抛 AccessDeniedException,生产环境由 GlobalExceptionHandler 转 Result.error(403, "无权限")。
|
|
|
+ *
|
|
|
+ * 本测试用最小 Spring 上下文(仅方法安全 + 控制器 + mock service)直接验证代理行为,
|
|
|
+ * 不启动 Web 容器 / DB。
|
|
|
+ */
|
|
|
+class TrainingPlanControllerRoleGuardTest {
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ @EnableGlobalMethodSecurity(prePostEnabled = true)
|
|
|
+ static class MethodSecurityTestConfig {
|
|
|
+ }
|
|
|
+
|
|
|
+ private AnnotationConfigApplicationContext context;
|
|
|
+ private TrainingPlanController controller;
|
|
|
+ private TrainingPlanService trainingPlanService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ // 手动注册 mock service:绕过 BeanPostProcessor,避免 Mockito 子类被 @Resource 二次注入
|
|
|
+ TrainingPlanService mockService = mock(TrainingPlanService.class);
|
|
|
+ context = new AnnotationConfigApplicationContext();
|
|
|
+ context.getBeanFactory().registerSingleton("trainingPlanService", mockService);
|
|
|
+ context.register(MethodSecurityTestConfig.class, TrainingPlanController.class);
|
|
|
+ context.refresh();
|
|
|
+ controller = context.getBean(TrainingPlanController.class);
|
|
|
+ trainingPlanService = mockService;
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterEach
|
|
|
+ void tearDown() {
|
|
|
+ SecurityContextHolder.clearContext();
|
|
|
+ context.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void loginAs(String role) {
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(
|
|
|
+ new UsernamePasswordAuthenticationToken(7L, null,
|
|
|
+ Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> body(String key, Object value) {
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put(key, value);
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ private TrainingPlan plan(long id) {
|
|
|
+ TrainingPlan plan = new TrainingPlan();
|
|
|
+ plan.setId(id);
|
|
|
+ plan.setGuideId(7L);
|
|
|
+ plan.setTitle("暑期认知训练");
|
|
|
+ plan.setStatus("published");
|
|
|
+ return plan;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== detail ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanDetail_非规划师角色_拒绝访问() {
|
|
|
+ loginAs("user");
|
|
|
+ when(trainingPlanService.getPlanById(9L)).thenReturn(plan(9L));
|
|
|
+
|
|
|
+ assertThrows(AccessDeniedException.class,
|
|
|
+ () -> controller.getPlanDetail(body("planId", 9)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanDetail_匿名角色_拒绝访问() {
|
|
|
+ loginAs("anonymous");
|
|
|
+
|
|
|
+ assertThrows(AccessDeniedException.class,
|
|
|
+ () -> controller.getPlanDetail(body("planId", 9)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanDetail_规划师角色_放行() {
|
|
|
+ loginAs("teacher");
|
|
|
+ when(trainingPlanService.getPlanById(9L)).thenReturn(plan(9L));
|
|
|
+ when(trainingPlanService.getPlanItems(9L)).thenReturn(Collections.<TrainingPlanItem>emptyList());
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.getPlanDetail(body("planId", 9));
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanDetail_管理员角色_放行() {
|
|
|
+ loginAs("admin");
|
|
|
+ when(trainingPlanService.getPlanById(9L)).thenReturn(plan(9L));
|
|
|
+ when(trainingPlanService.getPlanItems(9L)).thenReturn(Collections.<TrainingPlanItem>emptyList());
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.getPlanDetail(body("planId", 9));
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== list ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanList_非规划师角色_拒绝访问() {
|
|
|
+ loginAs("user");
|
|
|
+
|
|
|
+ assertThrows(AccessDeniedException.class,
|
|
|
+ () -> controller.getPlanList(7L, new HashMap<>(), null));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPlanList_规划师角色_放行() {
|
|
|
+ loginAs("teacher");
|
|
|
+ when(trainingPlanService.getPlansByGuide(7L)).thenReturn(Collections.<TrainingPlan>emptyList());
|
|
|
+
|
|
|
+ Result<java.util.List<TrainingPlan>> result = controller.getPlanList(7L, new HashMap<>(), null);
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ }
|
|
|
+}
|