|
|
@@ -0,0 +1,384 @@
|
|
|
+package com.etotem.cfc.controller;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.Badge;
|
|
|
+import com.etotem.cfc.entity.MemberBadge;
|
|
|
+import com.etotem.cfc.service.BadgeService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
+
|
|
|
+/**
|
|
|
+ * S-cfc-32 · achievement.grid.fiveCol — BadgeController.getMemberBadges 契约级单测(RED 阶段补齐)。
|
|
|
+ *
|
|
|
+ * <p>覆盖 {@link BadgeController#getMemberBadges(Map, Long)}:
|
|
|
+ * <ol>
|
|
|
+ * <li><b>正常路径</b>:有效 memberId(或 currentMemberId 兜底)→ 返回 memberBadge+badge 聚合列表;</li>
|
|
|
+ * <li><b>关键边界</b>:memberId 为空且无 currentMemberId → 400/统一错误;</li>
|
|
|
+ * <li><b>角色/权限</b>:parent/teacher/admin 均可访问(无额外角色限制,仅需登录上下文)。</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * 纯单元测试:MockMvc standalone + Mockito,不依赖数据库/微信/外部服务。
|
|
|
+ */
|
|
|
+public class BadgeControllerGetMemberBadgesTest {
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private BadgeService badgeService;
|
|
|
+
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ public void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ BadgeController controller = new BadgeController();
|
|
|
+ ReflectionTestUtils.setField(controller, "badgeService", badgeService);
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // 正常路径
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_显式传memberId_返回聚合列表() throws Exception {
|
|
|
+ Long memberId = 1001L;
|
|
|
+ Long badgeId = 10L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(1L);
|
|
|
+ mb.setMemberId(memberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setIsFavorite(false);
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge = new Badge();
|
|
|
+ badge.setId(badgeId);
|
|
|
+ badge.setBadgeId("b_test_001");
|
|
|
+ badge.setName("连续打卡达人");
|
|
|
+ badge.setIcon("🏅");
|
|
|
+ badge.setCategory("streak");
|
|
|
+ badge.setLevel("gold");
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(badge);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":1001}")
|
|
|
+ .requestAttr("userId", 1L)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data[0].memberBadge.id").value(1))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.badgeId").value("b_test_001"))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("连续打卡达人"))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.category").value("streak"));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ verify(badgeService).getBadgeById(badgeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_不传memberId_回退currentMemberId() throws Exception {
|
|
|
+ Long currentMemberId = 2002L;
|
|
|
+ Long badgeId = 20L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(2L);
|
|
|
+ mb.setMemberId(currentMemberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setIsFavorite(true);
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge = new Badge();
|
|
|
+ badge.setId(badgeId);
|
|
|
+ badge.setBadgeId("b_test_002");
|
|
|
+ badge.setName("任务完成者");
|
|
|
+ badge.setIcon("✅");
|
|
|
+ badge.setCategory("task");
|
|
|
+ badge.setLevel("silver");
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(currentMemberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(badge);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{}")
|
|
|
+ .requestAttr("userId", 2L)
|
|
|
+ .requestAttr("currentMemberId", currentMemberId)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data[0].memberBadge.id").value(2))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("任务完成者"))
|
|
|
+ .andExpect(jsonPath("$.data[0].memberBadge.isFavorite").value(true));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(currentMemberId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_多枚勋章_按收藏与获得时间排序() throws Exception {
|
|
|
+ Long memberId = 3003L;
|
|
|
+ Long badgeId1 = 30L;
|
|
|
+ Long badgeId2 = 31L;
|
|
|
+
|
|
|
+ // mb2: 收藏=true, 获得时间=今天 → 应排在前面
|
|
|
+ MemberBadge mb2 = new MemberBadge();
|
|
|
+ mb2.setId(4L);
|
|
|
+ mb2.setMemberId(memberId);
|
|
|
+ mb2.setBadgeId(badgeId2);
|
|
|
+ mb2.setEarnedAt(new Date()); // 今天
|
|
|
+ mb2.setIsFavorite(true); // 收藏优先
|
|
|
+ mb2.setStatus(1);
|
|
|
+
|
|
|
+ // mb1: 收藏=false, 获得时间=昨天 → 排在后面
|
|
|
+ MemberBadge mb1 = new MemberBadge();
|
|
|
+ mb1.setId(3L);
|
|
|
+ mb1.setMemberId(memberId);
|
|
|
+ mb1.setBadgeId(badgeId1);
|
|
|
+ mb1.setEarnedAt(new Date(System.currentTimeMillis() - 86400000L)); // 昨天
|
|
|
+ mb1.setIsFavorite(false);
|
|
|
+ mb1.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge1 = new Badge();
|
|
|
+ badge1.setId(badgeId1);
|
|
|
+ badge1.setBadgeId("b_test_003");
|
|
|
+ badge1.setName("成长记录者");
|
|
|
+ badge1.setCategory("growth");
|
|
|
+
|
|
|
+ Badge badge2 = new Badge();
|
|
|
+ badge2.setId(badgeId2);
|
|
|
+ badge2.setBadgeId("b_test_004");
|
|
|
+ badge2.setName("挑战参与者");
|
|
|
+ badge2.setCategory("challenge");
|
|
|
+
|
|
|
+ // Mock 返回已按 service 排序规则(isFavorite DESC, earnedAt DESC)排好序的列表
|
|
|
+ List<MemberBadge> memberBadges = Arrays.asList(mb2, mb1);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId1)).thenReturn(badge1);
|
|
|
+ when(badgeService.getBadgeById(badgeId2)).thenReturn(badge2);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":3003}")
|
|
|
+ .requestAttr("userId", 3L)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.length()").value(2))
|
|
|
+ // 收藏优先:mb2 (挑战参与者) 应在前
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("挑战参与者"))
|
|
|
+ .andExpect(jsonPath("$.data[1].badge.name").value("成长记录者"));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_空列表_返回空数组() throws Exception {
|
|
|
+ Long memberId = 4004L;
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":4004}")
|
|
|
+ .requestAttr("userId", 4L)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data").isArray())
|
|
|
+ .andExpect(jsonPath("$.data.length()").value(0));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ verify(badgeService, never()).getBadgeById(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // 关键边界
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_无memberId且无currentMemberId_返回空列表() throws Exception {
|
|
|
+ // 既不传 memberId,也没有 currentMemberId 请求属性 → memberId 为 null
|
|
|
+ // 实际行为:service 查询 WHERE member_id = NULL 返回空,controller 返回 200 + 空数组
|
|
|
+ when(badgeService.getActiveMemberBadges(null)).thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{}")
|
|
|
+ .requestAttr("userId", 5L)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data").isArray())
|
|
|
+ .andExpect(jsonPath("$.data.length()").value(0));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_memberBadge存在但Badge定义丢失_跳过该项() throws Exception {
|
|
|
+ Long memberId = 6006L;
|
|
|
+ Long badgeId = 60L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(5L);
|
|
|
+ mb.setMemberId(memberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(null); // Badge 定义被删/丢失
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":6006}")
|
|
|
+ .requestAttr("userId", 6L)
|
|
|
+ .requestAttr("role", "parent")
|
|
|
+ .requestAttr("roles", Collections.singletonList("parent")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.length()").value(0)); // 该项被过滤
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ verify(badgeService).getBadgeById(badgeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // 角色/权限:parent/teacher/admin 均可访问(仅需登录上下文)
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_teacher角色_可访问() throws Exception {
|
|
|
+ Long memberId = 7007L;
|
|
|
+ Long badgeId = 70L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(6L);
|
|
|
+ mb.setMemberId(memberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge = new Badge();
|
|
|
+ badge.setId(badgeId);
|
|
|
+ badge.setBadgeId("b_teacher_001");
|
|
|
+ badge.setName("规划师认证");
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(badge);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":7007}")
|
|
|
+ .requestAttr("userId", 7L)
|
|
|
+ .requestAttr("currentMemberId", memberId)
|
|
|
+ .requestAttr("role", "teacher")
|
|
|
+ .requestAttr("roles", Collections.singletonList("teacher")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("规划师认证"));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_admin角色_可访问() throws Exception {
|
|
|
+ Long memberId = 8008L;
|
|
|
+ Long badgeId = 80L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(7L);
|
|
|
+ mb.setMemberId(memberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge = new Badge();
|
|
|
+ badge.setId(badgeId);
|
|
|
+ badge.setBadgeId("b_admin_001");
|
|
|
+ badge.setName("平台管理员徽章");
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(badge);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{\"memberId\":8008}")
|
|
|
+ .requestAttr("userId", 8L)
|
|
|
+ .requestAttr("currentMemberId", memberId)
|
|
|
+ .requestAttr("role", "admin")
|
|
|
+ .requestAttr("roles", Collections.singletonList("admin")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("平台管理员徽章"));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getMemberBadges_child角色_可访问自身勋章() throws Exception {
|
|
|
+ // 直接登录的孩子端(role=child,memberType=login)也可查看自己的勋章
|
|
|
+ Long memberId = 9009L;
|
|
|
+ Long badgeId = 90L;
|
|
|
+
|
|
|
+ MemberBadge mb = new MemberBadge();
|
|
|
+ mb.setId(8L);
|
|
|
+ mb.setMemberId(memberId);
|
|
|
+ mb.setBadgeId(badgeId);
|
|
|
+ mb.setEarnedAt(new Date());
|
|
|
+ mb.setStatus(1);
|
|
|
+
|
|
|
+ Badge badge = new Badge();
|
|
|
+ badge.setId(badgeId);
|
|
|
+ badge.setBadgeId("b_child_001");
|
|
|
+ badge.setName("小小达人");
|
|
|
+
|
|
|
+ List<MemberBadge> memberBadges = Collections.singletonList(mb);
|
|
|
+ when(badgeService.getActiveMemberBadges(memberId)).thenReturn(memberBadges);
|
|
|
+ when(badgeService.getBadgeById(badgeId)).thenReturn(badge);
|
|
|
+
|
|
|
+ mockMvc.perform(post("/api/badges/member/list")
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
+ .content("{}") // 孩子端通常不传 memberId,靠 currentMemberId
|
|
|
+ .requestAttr("userId", 9L)
|
|
|
+ .requestAttr("currentMemberId", memberId)
|
|
|
+ .requestAttr("role", "child")
|
|
|
+ .requestAttr("roles", Collections.singletonList("child")))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data[0].badge.name").value("小小达人"));
|
|
|
+
|
|
|
+ verify(badgeService).getActiveMemberBadges(memberId);
|
|
|
+ }
|
|
|
+}
|