|
|
@@ -0,0 +1,313 @@
|
|
|
+package com.etotem.cfc.controller;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.HealthReport;
|
|
|
+import com.etotem.cfc.service.HealthReportService;
|
|
|
+import com.etotem.cfc.service.ReportMemberPermissionService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.Mockito.doThrow;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * E09 越权修复契约单测:健康报告 删除/编辑/下载 三个端点的归属/权限校验。
|
|
|
+ *
|
|
|
+ * 契约(对应 HealthReportController 的 deleteReport / editReport / downloadReport):
|
|
|
+ * - userId 未注入 → Result.error("用户未登录")
|
|
|
+ * - reportId 不存在 → Result.error("报告不存在"),不触发底层写/删操作
|
|
|
+ * - 上传者本人(report.userId == userId 且 subjectId 为空)→ 允许
|
|
|
+ * - 家庭管理员(report.familyId 匹配且 isFamilyAdmin=true 且 subjectId 为空)→ 允许
|
|
|
+ * - 被授权成员(subjectId 非空且 canViewMemberReports=true)→ 允许
|
|
|
+ * - 其他登录用户(非本人、非管理员、未被授权)→ Result.error("无权..."),不触发底层操作
|
|
|
+ * - familyId 与 report.familyId 均为空 → 拒绝(edit/download;delete 由成员级判定兜底仅限本人)
|
|
|
+ * - deleteReport:请求方家庭与报告家庭不一致 → 403(SC-P-24-P 跨家庭横向越权)
|
|
|
+ * - deleteReport:报告已关联成长方案(service 抛 IllegalStateException)→ 409 透传,不吞异常
|
|
|
+ *
|
|
|
+ * 测试不依赖数据库/外部服务:healthReportService / reportMemberPermissionService 全 mock;
|
|
|
+ * 不启 Spring 上下文,沿用 HealthPlanDeleteControllerTest 的直接调用模式。
|
|
|
+ */
|
|
|
+class HealthReportControllerDeleteAuthzTest {
|
|
|
+
|
|
|
+ private HealthReportController controller;
|
|
|
+ private HealthReportService healthReportService;
|
|
|
+ private ReportMemberPermissionService reportMemberPermissionService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ controller = new HealthReportController();
|
|
|
+ healthReportService = mock(HealthReportService.class);
|
|
|
+ reportMemberPermissionService = mock(ReportMemberPermissionService.class);
|
|
|
+ inject("healthReportService", healthReportService);
|
|
|
+ inject("reportMemberPermissionService", reportMemberPermissionService);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void inject(String name, Object value) throws Exception {
|
|
|
+ Field f = controller.getClass().getDeclaredField(name);
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(controller, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> params(Object reportId) {
|
|
|
+ Map<String, Object> p = new HashMap<>();
|
|
|
+ p.put("reportId", reportId);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private HealthReport report(Long reportId, Long ownerUserId, Long familyId) {
|
|
|
+ HealthReport r = new HealthReport();
|
|
|
+ r.setId(reportId);
|
|
|
+ r.setUserId(ownerUserId);
|
|
|
+ r.setFamilyId(familyId);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 未登录_返回用户未登录不触发删除() {
|
|
|
+ Result<?> r = controller.deleteReport(params(1L), null, null);
|
|
|
+ assertEquals("用户未登录", r.getMessage());
|
|
|
+ verify(healthReportService, never()).deleteReport(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 报告不存在_返回报告不存在不触发删除() {
|
|
|
+ when(healthReportService.getReportById(99L)).thenReturn(null);
|
|
|
+ Result<?> r = controller.deleteReport(params(99L), 1L, null);
|
|
|
+ assertEquals("报告不存在", r.getMessage());
|
|
|
+ verify(healthReportService, never()).deleteReport(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 上传者本人_可删除自己报告() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ Result<?> r = controller.deleteReport(params(100L), 10L, null);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals("已删除", r.getData());
|
|
|
+ verify(healthReportService).deleteReport(100L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 家庭管理员_可删除家庭内报告() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(20L, 7L)).thenReturn(true);
|
|
|
+ Result<?> r = controller.deleteReport(params(100L), 20L, null);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals("已删除", r.getData());
|
|
|
+ verify(healthReportService).deleteReport(100L);
|
|
|
+ verify(reportMemberPermissionService).isFamilyAdmin(20L, 7L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 非本人非管理员_拒绝删除() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(30L, 7L)).thenReturn(false);
|
|
|
+ Result<?> r = controller.deleteReport(params(100L), 30L, null);
|
|
|
+ assertEquals("无权删除该报告", r.getMessage());
|
|
|
+ assertNotEquals(200, r.getCode());
|
|
|
+ verify(healthReportService, never()).deleteReport(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 跨家庭_拒绝删除_403() {
|
|
|
+ // 本人 userId=10 也无权删他家庭(7)的报告:请求方家庭 8 ≠ 报告家庭 7,先于成员级判定拦截
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ Result<?> r = controller.deleteReport(params(100L), 10L, 8L);
|
|
|
+ assertEquals("无权删除该报告", r.getMessage());
|
|
|
+ assertEquals(403, r.getCode());
|
|
|
+ verify(healthReportService, never()).deleteReport(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 报告已关联成长方案_拒绝删除_409不吞异常() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ doThrow(new IllegalStateException("报告已关联成长方案,请先解除关联")).when(healthReportService).deleteReport(100L);
|
|
|
+ Result<?> r = controller.deleteReport(params(100L), 10L, 7L);
|
|
|
+ assertEquals("报告已关联成长方案,请先解除关联", r.getMessage());
|
|
|
+ assertEquals(409, r.getCode());
|
|
|
+ // 守卫异常被捕获转 409,但 deleteReport 确已调用(不是被静默吞掉)
|
|
|
+ verify(healthReportService).deleteReport(100L);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============ editReport:越权编辑 ============ */
|
|
|
+
|
|
|
+ private HealthReport reportWithSubject(Long reportId, Long ownerUserId, Long familyId, Long subjectId) {
|
|
|
+ HealthReport r = report(reportId, ownerUserId, familyId);
|
|
|
+ r.setSubjectId(subjectId);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> editParams(Object reportId, Object subjectId) {
|
|
|
+ Map<String, Object> p = params(reportId);
|
|
|
+ p.put("subjectId", subjectId);
|
|
|
+ p.put("payload", new HashMap<String, Object>());
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_未登录_返回用户未登录() {
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), null, null);
|
|
|
+ assertEquals("用户未登录", r.getMessage());
|
|
|
+ verify(healthReportService, never()).updateReportFromPayload(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_报告不存在_返回报告不存在() {
|
|
|
+ when(healthReportService.getReportById(99L)).thenReturn(null);
|
|
|
+ Result<?> r = controller.editReport(editParams(99L, null), 1L, null);
|
|
|
+ assertEquals("报告不存在", r.getMessage());
|
|
|
+ verify(healthReportService, never()).updateReportFromPayload(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_本人可编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, 7L, null));
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 10L, 7L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals("已保存", r.getData());
|
|
|
+ verify(healthReportService).updateReportFromPayload(eq(100L), any(), any(), eq(10L));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_管理员可编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, 7L, null));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(20L, 7L)).thenReturn(true);
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 20L, 7L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals("已保存", r.getData());
|
|
|
+ verify(healthReportService).updateReportFromPayload(eq(100L), any(), any(), eq(20L));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_被授权成员可编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, 7L, 50L));
|
|
|
+ when(reportMemberPermissionService.canViewMemberReports(30L, 50L, 7L)).thenReturn(true);
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 30L, 7L);
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals("已保存", r.getData());
|
|
|
+ verify(healthReportService).updateReportFromPayload(eq(100L), any(), any(), eq(30L));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_非本人非管理员_拒绝编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, 7L, null));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(30L, 7L)).thenReturn(false);
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 30L, 7L);
|
|
|
+ assertEquals("无权编辑该报告", r.getMessage());
|
|
|
+ verify(healthReportService, never()).updateReportFromPayload(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_授权成员但不可见_拒绝编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, 7L, 50L));
|
|
|
+ when(reportMemberPermissionService.canViewMemberReports(30L, 50L, 7L)).thenReturn(false);
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 30L, 7L);
|
|
|
+ assertEquals("无权编辑该报告", r.getMessage());
|
|
|
+ verify(healthReportService, never()).updateReportFromPayload(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 编辑_家庭缺失_拒绝编辑() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(reportWithSubject(100L, 10L, null, null));
|
|
|
+ Result<?> r = controller.editReport(editParams(100L, null), 10L, null);
|
|
|
+ assertEquals("无权编辑该报告", r.getMessage());
|
|
|
+ verify(healthReportService, never()).updateReportFromPayload(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============ downloadReport:越权下载 ============ */
|
|
|
+
|
|
|
+ private static final String FILE_URL = "https://cdn.example.com/report.pdf";
|
|
|
+
|
|
|
+ private HealthReport downloadReportData(Long reportId, Long ownerUserId, Long familyId, Long subjectId) {
|
|
|
+ HealthReport r = report(reportId, ownerUserId, familyId);
|
|
|
+ r.setSubjectId(subjectId);
|
|
|
+ r.setFileUrl(FILE_URL);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private HttpServletRequest mockRequest() {
|
|
|
+ return mock(HttpServletRequest.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_未登录_返回用户未登录() {
|
|
|
+ Result<?> r = controller.downloadReport(params(100L), null, null, mockRequest());
|
|
|
+ assertEquals("用户未登录", r.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_报告不存在_返回报告不存在() {
|
|
|
+ when(healthReportService.getReportById(99L)).thenReturn(null);
|
|
|
+ Result<?> r = controller.downloadReport(params(99L), 1L, null, mockRequest());
|
|
|
+ assertEquals("报告不存在", r.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_本人可下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, 7L, null));
|
|
|
+ Result<Map<String, Object>> r = controller.downloadReport(params(100L), 10L, 7L, mockRequest());
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(FILE_URL, r.getData().get("fileUrl"));
|
|
|
+ assertEquals("report.pdf", r.getData().get("fileName"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_管理员可下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, 7L, null));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(20L, 7L)).thenReturn(true);
|
|
|
+ Result<Map<String, Object>> r = controller.downloadReport(params(100L), 20L, 7L, mockRequest());
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(FILE_URL, r.getData().get("fileUrl"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_被授权成员可下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, 7L, 50L));
|
|
|
+ when(reportMemberPermissionService.canViewMemberReports(30L, 50L, 7L)).thenReturn(true);
|
|
|
+ Result<Map<String, Object>> r = controller.downloadReport(params(100L), 30L, 7L, mockRequest());
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertEquals(FILE_URL, r.getData().get("fileUrl"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_非授权_拒绝下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, 7L, null));
|
|
|
+ when(reportMemberPermissionService.isFamilyAdmin(30L, 7L)).thenReturn(false);
|
|
|
+ Result<?> r = controller.downloadReport(params(100L), 30L, 7L, mockRequest());
|
|
|
+ assertEquals("无权下载该报告", r.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_授权成员但不可见_拒绝下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, 7L, 50L));
|
|
|
+ when(reportMemberPermissionService.canViewMemberReports(30L, 50L, 7L)).thenReturn(false);
|
|
|
+ Result<?> r = controller.downloadReport(params(100L), 30L, 7L, mockRequest());
|
|
|
+ assertEquals("无权下载该报告", r.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_家庭缺失_拒绝下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(downloadReportData(100L, 10L, null, null));
|
|
|
+ Result<?> r = controller.downloadReport(params(100L), 10L, null, mockRequest());
|
|
|
+ assertEquals("无权下载该报告", r.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 下载_无文件_返回无文件可下载() {
|
|
|
+ when(healthReportService.getReportById(100L)).thenReturn(report(100L, 10L, 7L));
|
|
|
+ Result<?> r = controller.downloadReport(params(100L), 10L, 7L, mockRequest());
|
|
|
+ assertEquals("该报告无文件可下载", r.getMessage());
|
|
|
+ }
|
|
|
+}
|