|
|
@@ -0,0 +1,384 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.controller.assessment.AssessmentAppointmentController;
|
|
|
+import com.etotem.cfc.entity.AssessmentAppointment;
|
|
|
+import com.etotem.cfc.mapper.AssessmentAppointmentMapper;
|
|
|
+import com.etotem.cfc.service.AssessmentAppointmentService;
|
|
|
+import com.etotem.cfc.service.EnergyService;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:能力键 {@code teacher.appointment.guard} —— 规划师测评预约的状态守卫与未到闭环。
|
|
|
+ *
|
|
|
+ * <p><b>需求溯源</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>《应用场景总览》SC-T-09「测评预约确认(状态守卫)」、SC-T-10「测评预约完成/未到」:
|
|
|
+ * 确认(待确认→已确认)/完成(已确认→已完成)/标记未到(→未到)均须遵守状态机合法流转。</li>
|
|
|
+ * <li>《2026-11-07-planner-zone-contract.md》§5 转测试侧建议第 3 条:
|
|
|
+ * 「SC-T-09 预约状态守卫——confirm 仅 status=0(待确认)可流转,重复 confirm 拒绝;
|
|
|
+ * complete/cancel 状态机边界」。</li>
|
|
|
+ * <li>API 参考 §4.57:{@code AssessmentAppointmentController}
|
|
|
+ * {@code /api/assessment/appointment/*}(create/confirm/{id}/cancel/{id}/my-list/guide-list/assessor-list)
|
|
|
+ * + {@code DanAssessmentController} {@code /appointment/complete}。
|
|
|
+ * 状态值:0=待确认 1=已确认 2=已完成 3=已取消 4=未到。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>契约缺口(G-1)</b>:{@code AssessmentAppointmentController.getStatusText} 已定义
|
|
|
+ * {@code status=4}(未到),但 Controller 仅提供 create/confirm/cancel/complete/my-list/guide-list/
|
|
|
+ * assessor-list,<b>没有把预约置为 status=4 的独立入口</b>;规划师「完成/未到」闭环缺少「未到」分支
|
|
|
+ * (契约文档 §4 G-1 已登记)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>被校验的契约行为</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>confirm:仅 {@code status=0}(待确认)可流转到 1;重复确认、已完成/未到/已取消状态一律拒绝,
|
|
|
+ * 且拒绝时不写库。</li>
|
|
|
+ * <li>complete:仅 {@code status=1}(已确认)可流转到 2;待确认/已完成/未到状态一律拒绝,
|
|
|
+ * 且拒绝时不写库、不重复发放完成能量。</li>
|
|
|
+ * <li>cancel:仅未到终态前(待确认 0 / 已确认 1)可流转到 3;已完成 2 / 未到 4 等终态一律拒绝,
|
|
|
+ * 且拒绝时不写库。</li>
|
|
|
+ * <li>G-1:规划师可将预约标记为「未到」——服务层应提供 {@code noShowAppointment(...)},
|
|
|
+ * Controller 应登记 {@code @PostMapping(...no-show...)} 端点(契约文档 §4 G-1 建议
|
|
|
+ * {@code POST /api/assessment/appointment/no-show/{id}})。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito / 反射,无 Spring 上下文 / 无 DB / 无外部服务。
|
|
|
+ * 复用 {@link AssessmentPlanContractTestBase#inject} 反射注入。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期(实现未落地)</b>:当前 {@code AssessmentAppointmentService} 的
|
|
|
+ * {@code confirmAppointment} / {@code completeAppointment} / {@code cancelAppointment}
|
|
|
+ * 均<strong>无前置 status 守卫</strong>(直接改状态并写库),且无「未到」方法/端点。
|
|
|
+ * 因此全部非法跳变用例与 G-1 探测用例在编译通过的前提下运行失败(红)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>GREEN 升级指引</b>:实现者给三个方法加入 status 前置判定(不满足直接 {@code return false}
|
|
|
+ * 且不写库/不发能量),并新增 {@code noShowAppointment(id, teacherId)} + 对应 Controller 映射后,
|
|
|
+ * 本类全部用例自动转绿;正常流「绿色锚」用例自始至终通过,用于保证守卫不会误伤合法流转。</p>
|
|
|
+ */
|
|
|
+class TeacherAppointmentGuardContractTest extends AssessmentPlanContractTestBase {
|
|
|
+
|
|
|
+ private static final long APPOINTMENT_ID = 101L;
|
|
|
+ private static final long ASSESSOR_ID = 5L;
|
|
|
+ private static final long GUIDE_ID = 7L;
|
|
|
+ private static final long MEMBER_ID = 10L;
|
|
|
+
|
|
|
+ // ============================== SC-T-09 确认状态守卫 ==============================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 状态守卫:重复确认(已确认 status=1)应被拒绝且不写库")
|
|
|
+ void confirm_duplicateReconfirmation_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(1, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(APPOINTMENT_ID, ASSESSOR_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] confirm 重复确认(当前 status=1 已确认)应被状态守卫拒绝,"
|
|
|
+ + "但实现未校验前置状态直接改状态写库(SC-T-09 §5.3「重复 confirm 拒绝」)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 状态守卫:已完成(status=2)不可回退确认")
|
|
|
+ void confirm_afterCompletion_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(2, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(APPOINTMENT_ID, ASSESSOR_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 已完成(status=2)的预约不可再走 confirm 回退状态机,"
|
|
|
+ + "当前实现未拦截(SC-T-09 状态守卫)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 状态守卫:未到(status=4)不可进入确认")
|
|
|
+ void confirm_afterNoShow_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(4, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(APPOINTMENT_ID, ASSESSOR_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 被标记『未到』(status=4)的预约不可重新确认,"
|
|
|
+ + "当前实现未拦截(SC-T-09 状态守卫)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-09 正常流(绿色锚):待确认 status=0 → 已确认 status=1")
|
|
|
+ void confirm_pending_toConfirmed_allowed() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(0, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.confirmAppointment(APPOINTMENT_ID, ASSESSOR_ID);
|
|
|
+
|
|
|
+ assertTrue(ok, "待确认 status=0 应可正常确认流转");
|
|
|
+ assertEquals(1, a.getStatus(), "确认后状态应为 1(已确认)");
|
|
|
+ verify(mapper).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================== SC-T-10 完成状态守卫 ==============================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 状态守卫:待确认(status=0)不得提前完成,且不发完成能量")
|
|
|
+ void complete_beforeConfirmation_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ EnergyService energySvc = mock(EnergyService.class);
|
|
|
+ inject(svc, "energyService", energySvc);
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(0, null, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.completeAppointment(APPOINTMENT_ID, GUIDE_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 预约尚未确认(status=0)时不得提前标记完成,"
|
|
|
+ + "状态机合法流转为 0→1→2;当前实现直接置 status=2(SC-T-10)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ verify(energySvc, never()).awardEnergy(any(), any(), any(), any(), any(), isNull());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 状态守卫:已完成(status=2)不可重复完成,避免重复发能量")
|
|
|
+ void complete_alreadyCompleted_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ EnergyService energySvc = mock(EnergyService.class);
|
|
|
+ inject(svc, "energyService", energySvc);
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(2, null, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.completeAppointment(APPOINTMENT_ID, GUIDE_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 已完成(status=2)的预约重复 complete 应被守卫拒绝,"
|
|
|
+ + "当前实现未拦截并会二次发放完成能量(SC-T-10 幂等边界)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ verify(energySvc, never()).awardEnergy(any(), any(), any(), any(), any(), isNull());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 状态守卫:未到(status=4)不可转为完成")
|
|
|
+ void complete_afterNoShow_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ EnergyService energySvc = mock(EnergyService.class);
|
|
|
+ inject(svc, "energyService", energySvc);
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(4, null, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.completeAppointment(APPOINTMENT_ID, GUIDE_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 已标记『未到』(status=4)的预约不可再走完成流程,"
|
|
|
+ + "当前实现未拦截(SC-T-10 状态机合法流转)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ verify(energySvc, never()).awardEnergy(any(), any(), any(), any(), any(), isNull());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 正常流(绿色锚):已确认 status=1 → 已完成 status=2")
|
|
|
+ void complete_confirmed_toCompleted_allowed() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ EnergyService energySvc = mock(EnergyService.class);
|
|
|
+ inject(svc, "energyService", energySvc);
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(1, null, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.completeAppointment(APPOINTMENT_ID, GUIDE_ID);
|
|
|
+
|
|
|
+ assertTrue(ok, "已确认 status=1 应可正常完成流转");
|
|
|
+ assertEquals(2, a.getStatus(), "完成后状态应为 2(已完成)");
|
|
|
+ verify(mapper).updateById(any());
|
|
|
+ verify(energySvc).awardEnergy(eq(MEMBER_ID), eq("appointment"), eq(APPOINTMENT_ID),
|
|
|
+ eq(30), any(), isNull());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================== SC-P-38 / SC-T-10 取消状态守卫 ==============================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 状态守卫:已完成(status=2)不可回退取消")
|
|
|
+ void cancel_afterCompleted_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(2, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.cancelAppointment(APPOINTMENT_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 已完成(status=2)的预约不可再取消,"
|
|
|
+ + "当前实现无状态守卫直接置 status=3(SC-T-10 合法流转)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 状态守卫:未到(status=4)不可再取消")
|
|
|
+ void cancel_afterNoShow_rejected() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(4, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.cancelAppointment(APPOINTMENT_ID);
|
|
|
+
|
|
|
+ assertFalse(ok,
|
|
|
+ "[teacher.appointment.guard] 已标记『未到』(status=4)的预约不可再取消,"
|
|
|
+ + "当前实现无状态守卫直接置 status=3(SC-T-10 合法流转)。");
|
|
|
+ verify(mapper, never()).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 正常流(绿色锚):待确认 status=0 → 已取消 status=3")
|
|
|
+ void cancel_pending_toCancelled_allowed() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(0, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.cancelAppointment(APPOINTMENT_ID);
|
|
|
+
|
|
|
+ assertTrue(ok, "待确认 status=0 应可正常取消");
|
|
|
+ assertEquals(3, a.getStatus(), "取消后状态应为 3(已取消)");
|
|
|
+ verify(mapper).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-T-10 正常流(绿色锚):已确认 status=1 → 已取消 status=3")
|
|
|
+ void cancel_confirmed_toCancelled_allowed() throws Exception {
|
|
|
+ AssessmentAppointmentMapper mapper = mock(AssessmentAppointmentMapper.class);
|
|
|
+ AssessmentAppointmentService svc = new AssessmentAppointmentService();
|
|
|
+ inject(svc, "assessmentAppointmentMapper", mapper);
|
|
|
+ inject(svc, "energyService", mock(EnergyService.class));
|
|
|
+
|
|
|
+ AssessmentAppointment a = buildAppointment(1, ASSESSOR_ID, GUIDE_ID);
|
|
|
+ when(mapper.selectById(APPOINTMENT_ID)).thenReturn(a);
|
|
|
+ when(mapper.updateById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean ok = svc.cancelAppointment(APPOINTMENT_ID);
|
|
|
+
|
|
|
+ assertTrue(ok, "已确认 status=1 应可正常取消");
|
|
|
+ assertEquals(3, a.getStatus(), "取消后状态应为 3(已取消)");
|
|
|
+ verify(mapper).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================== 缺口 G-1:未到(no-show)入口 ==============================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("缺口 G-1:AssessmentAppointmentService 应提供 noShowAppointment 方法(置 status=4)")
|
|
|
+ void g1_service_hasNoShowMethod() {
|
|
|
+ boolean found = false;
|
|
|
+ for (Method m : AssessmentAppointmentService.class.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals("noShowAppointment")) {
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(found,
|
|
|
+ "[teacher.appointment.guard-G1] AssessmentAppointmentService 缺少 noShowAppointment(...) 方法:"
|
|
|
+ + "契约缺口 G-1 —— status=4(未到)竟无独立设置入口,规划师「完成/未到」闭环断裂"
|
|
|
+ + "(契约文档 §4 G-1;API_REFERENCE §4.57 SC-T-10)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("缺口 G-1:AssessmentAppointmentController 应登记 POST .../no-show/{id} 端点")
|
|
|
+ void g1_controller_hasNoShowEndpoint() {
|
|
|
+ boolean found = false;
|
|
|
+ for (Method m : AssessmentAppointmentController.class.getDeclaredMethods()) {
|
|
|
+ PostMapping pm = m.getAnnotation(PostMapping.class);
|
|
|
+ if (pm == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (String path : pm.value()) {
|
|
|
+ if (path != null && path.contains("no-show")) {
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (found) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(found,
|
|
|
+ "[teacher.appointment.guard-G1] AssessmentAppointmentController 未登记『未到』POST 端点:"
|
|
|
+ + "契约缺口 G-1 —— 规划师无法将预约置为 status=4(未到),完成/未到闭环缺失"
|
|
|
+ + "(契约文档 §4 G-1 建议 POST /api/assessment/appointment/no-show/{id})。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================== 辅助构造 ==============================
|
|
|
+
|
|
|
+ private AssessmentAppointment buildAppointment(int status, Long assessorId, Long guideId) {
|
|
|
+ AssessmentAppointment a = new AssessmentAppointment();
|
|
|
+ a.setId(APPOINTMENT_ID);
|
|
|
+ a.setStatus(status);
|
|
|
+ a.setMemberId(MEMBER_ID);
|
|
|
+ a.setAssessorId(assessorId);
|
|
|
+ a.setGuideId(guideId);
|
|
|
+ a.setTeacherId(guideId != null ? guideId : assessorId);
|
|
|
+ return a;
|
|
|
+ }
|
|
|
+}
|