|
|
@@ -0,0 +1,357 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.family.FamilyMembersController;
|
|
|
+import com.etotem.cfc.dto.SwitchMemberVO;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.FamilyMemberService;
|
|
|
+import com.etotem.cfc.service.SystemConfigService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+import org.springframework.security.access.AccessDeniedException;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.Calendar;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+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 family.childContext.switch}(SC-P-73 多孩子切换孩子上下文,TDD-RED 阶段)。
|
|
|
+ *
|
|
|
+ * <p><b>溯源·需求</b>:{@code tests/user-scenarios/应用场景总览.md} SC-P-73(业务域 J)——
|
|
|
+ * 多孩子家庭切换:选孩子 → 任务/报告/方案上下文切换;边界:仅本家庭孩子可选;当前孩子态隔离。
|
|
|
+ * 派生用例 {@code tests/test-cases/parent.md} TC-P-73(正常)/ TC-P-73-E(异常)/ TC-P-73-P(权限)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>溯源·设计</b>:{@code docs/superpowers/api/API_REFERENCE.md} §4.2 / §4.61.1 / §4.63.1——
|
|
|
+ * 载体 {@code POST /api/family/member/switch}(FamilyMembersController.switchToMember →
|
|
|
+ * FamilyMemberService.switchToMember)→ 响应 {@code SwitchMemberVO{memberId, source,
|
|
|
+ * effectiveRole, nickname, isSwitchable}}。
|
|
|
+ * 与能力键 {@code family.parent.switchRole}(S-cfc-11,P-03 家长↔孩子角色切换)区分:
|
|
|
+ * 本键 = P-73 <b>孩子间上下文切换</b>(多孩子家庭中切换当前孩子态)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>实现现状与 RED 锚点</b>:端点 {@code /switch} 已存在并落地了「家长 → 不可登录孩子」的
|
|
|
+ * 单次视角切换(P-03 语义),但 P-73 的<b>孩子上下文分类/隔离</b>默认值未落地——
|
|
|
+ * {@code computeEffectiveRole} 在被切换的不可登录孩子(managed child,{@code userId=null})
|
|
|
+ * <b>既无 birthday 又无 generation</b> 时默认返回 {@code "parent"}(FamilyMemberService.java:1392),
|
|
|
+ * 导致「选孩子」后返回的 {@code effectiveRole="parent"}、前端据此把当前孩子上下文误判为家长上下文,
|
|
|
+ * 违反 SC-P-73「选孩子 → 任务/报告/方案按孩子上下文隔离」契约(RED 用例)。
|
|
|
+ * 其余(跨家庭拒绝 / 自切拒绝 / 可登录成员拒绝 / 孩子端切家长拒绝 / 端点 401 / 缺 memberId)当前已落地,
|
|
|
+ * 作为 GREEN 锁定用例,防 GREEN 阶段误伤已落地行为。</p>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito / 反射,无 Spring 上下文 / 无 DB / 无微信 / 无外部服务。
|
|
|
+ * 风格对齐同批 RED 契约测试(FamilyRemoveMemberContractTest / FamilyJoinValidationRulesContractTest)——
|
|
|
+ * 结果契约断言 + RED/GREEN 混编锁定。</p>
|
|
|
+ */
|
|
|
+@DisplayName("family.childContext.switch 契约(FamilyMembersController /switch → FamilyMemberService.switchToMember)")
|
|
|
+class FamilyChildContextSwitchContractTest {
|
|
|
+
|
|
|
+ /** 被测试家庭 */
|
|
|
+ private static final Long FAMILY_ID = 100L;
|
|
|
+ /** 当前登录家长 userId */
|
|
|
+ private static final Long PARENT_USER_ID = 1L;
|
|
|
+ /** 不能登录孩子成员(managed child,family_members.userId = null)A */
|
|
|
+ private static final Long CHILD_A_MEMBER_ID = 11L;
|
|
|
+ /** 不能登录孩子成员(managed child,family_members.userId = null)B —— 切换目标 */
|
|
|
+ private static final Long CHILD_B_MEMBER_ID = 12L;
|
|
|
+ /** 可登录成员关联 userId(有 openid) */
|
|
|
+ private static final Long LOGIN_MEMBER_USER_ID = 200L;
|
|
|
+ /** 非本家庭 memberId */
|
|
|
+ private static final Long OTHER_FAMILY_MEMBER_ID = 999L;
|
|
|
+ /** 孩子年龄阈值(测试桩) */
|
|
|
+ private static final int CHILD_AGE_THRESHOLD = 18;
|
|
|
+
|
|
|
+ // ---------------- Service 层被测对象及依赖 ----------------
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private UserMapper userMapper;
|
|
|
+ @Mock
|
|
|
+ private SystemConfigService systemConfigService;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private FamilyMemberService familyMemberService;
|
|
|
+
|
|
|
+ // ---------------- Controller 层被测对象 ----------------
|
|
|
+ private FamilyMembersController controller;
|
|
|
+ /** Controller 层注入的 mock 服务(verify 用) */
|
|
|
+ private FamilyMemberService controllerService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ when(systemConfigService.getChildAgeThreshold()).thenReturn(CHILD_AGE_THRESHOLD);
|
|
|
+
|
|
|
+ // Controller 层:反射注入 mock 的 FamilyMemberService(贴合既有 unit 风格)
|
|
|
+ controller = new FamilyMembersController();
|
|
|
+ controllerService = mock(FamilyMemberService.class);
|
|
|
+ setField(controller, "familyMemberService", controllerService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // Controller 契约层(POST /api/family/member/switch)
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("Controller 正常流:memberId 传递到服务层,返回 SwitchMemberVO")
|
|
|
+ void controller_正常切换_透传服务层VO() {
|
|
|
+ SwitchMemberVO vo = childContextVo(CHILD_B_MEMBER_ID, "child");
|
|
|
+ when(controllerService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID)).thenReturn(vo);
|
|
|
+
|
|
|
+ Result<SwitchMemberVO> r = controller.switchToMember(body(CHILD_B_MEMBER_ID), PARENT_USER_ID);
|
|
|
+
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
+ assertNotNull(r.getData());
|
|
|
+ assertEquals(CHILD_B_MEMBER_ID, r.getData().getMemberId());
|
|
|
+ verify(controllerService).switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("Controller 异常流:未登录(userId=null)→ 401,不触达服务层")
|
|
|
+ void controller_未登录_userId为null_401() {
|
|
|
+ Result<SwitchMemberVO> r = controller.switchToMember(body(CHILD_B_MEMBER_ID), null);
|
|
|
+ assertEquals(401, r.getCode());
|
|
|
+ assertEquals("请先登录", r.getMessage());
|
|
|
+ verify(controllerService, never()).switchToMember(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("Controller 异常流:缺 memberId → 400「缺少成员ID」,不触达服务层")
|
|
|
+ void controller_缺少memberId_400() {
|
|
|
+ Result<SwitchMemberVO> r = controller.switchToMember(Collections.emptyMap(), PARENT_USER_ID);
|
|
|
+ assertEquals(400, r.getCode());
|
|
|
+ assertEquals("缺少成员ID", r.getMessage());
|
|
|
+ verify(controllerService, never()).switchToMember(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // Service 正常流(SC-P-73 / TC-P-73:选孩子 → 孩子上下文切换)
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:多孩子家庭,家长切换到本家庭不可登录孩子B → 返回孩子B上下文VO")
|
|
|
+ void service_多孩子切换到孩子B_返回孩子B上下文VO() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ FamilyMember childB = managedChild(CHILD_B_MEMBER_ID, ageYearsAgo(10), "小宝");
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(childB);
|
|
|
+
|
|
|
+ SwitchMemberVO vo = familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID);
|
|
|
+
|
|
|
+ assertNotNull(vo);
|
|
|
+ assertEquals(CHILD_B_MEMBER_ID, vo.getMemberId());
|
|
|
+ assertEquals("family_member", vo.getSource());
|
|
|
+ assertEquals("child", vo.getEffectiveRole());
|
|
|
+ assertEquals("小宝", vo.getNickname());
|
|
|
+ assertTrue(Boolean.TRUE.equals(vo.getIsSwitchable()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:目标孩子有 birthday 且未成年 → effectiveRole=child")
|
|
|
+ void service_目标孩子有生日未成年_effectiveRole为child() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ FamilyMember childB = managedChild(CHILD_B_MEMBER_ID, ageYearsAgo(10), null);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(childB);
|
|
|
+
|
|
|
+ SwitchMemberVO vo = familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID);
|
|
|
+
|
|
|
+ assertEquals("child", vo.getEffectiveRole());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:目标孩子无 birthday 但 generation 为负 → effectiveRole=child")
|
|
|
+ void service_目标孩子无生日generation为负_effectiveRole为child() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ FamilyMember childB = managedChild(CHILD_B_MEMBER_ID, null, null);
|
|
|
+ childB.setGeneration(-1);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(childB);
|
|
|
+
|
|
|
+ SwitchMemberVO vo = familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID);
|
|
|
+
|
|
|
+ assertEquals("child", vo.getEffectiveRole());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * <b>RED 用例</b>:被切换的不可登录孩子(managed child,{@code userId=null})既无 birthday
|
|
|
+ * 又无 generation 时,切换返回的 {@code effectiveRole} 必须仍归为孩子上下文({@code child}),
|
|
|
+ * 以保证 SC-P-73「选孩子 → 任务/报告/方案按孩子上下文隔离」。
|
|
|
+ *
|
|
|
+ * <p><b>当前实现</b>:{@code computeEffectiveRole} 无 birthday、无 generation 时默认返回
|
|
|
+ * {@code "parent"}(FamilyMemberService.java:1392)——把 managed child 误判为家长上下文,
|
|
|
+ * 前端据此把当前孩子态置为家长,孩子上下文隔离失效。<b>预期失败(红)</b>;GREEN 阶段实现
|
|
|
+ * 「userId=null 的不可登录成员 = 孩子上下文」的默认分类后转绿。</p>
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ @DisplayName("RED-正常流-4:managed child 无生日无 generation,切换仍应归为孩子上下文(child)")
|
|
|
+ void service_managedChild无生日无generation_仍应归为孩子上下文() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ // userId=null 的不可登录孩子,未填 birthday / generation(多孩子家庭常见数据边界)
|
|
|
+ FamilyMember childB = managedChild(CHILD_B_MEMBER_ID, null, null);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(childB);
|
|
|
+
|
|
|
+ SwitchMemberVO vo = familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID);
|
|
|
+
|
|
|
+ assertNotNull(vo, "切换应成功返回孩子上下文VO");
|
|
|
+ assertEquals("child", vo.getEffectiveRole(),
|
|
|
+ "[family.childContext.switch] managed child(userId=null,无 birthday 无 generation)"
|
|
|
+ + "切换后 effectiveRole 应为 child(孩子上下文),实际=" + vo.getEffectiveRole()
|
|
|
+ + " —— SC-P-73 当前孩子态隔离契约未落地:computeEffectiveRole 无年龄数据时默认 parent,"
|
|
|
+ + "未实现孩子上下文默认分类");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // Service 异常流(TC-P-73-E:非本家庭不可选;切换保持各孩子上下文隔离不串)
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-1:非本家庭 memberId → AccessDenied(不泄露他人家庭信息)")
|
|
|
+ void service_非本家庭memberId_拒绝() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ // 跨家庭:其他家庭的成员
|
|
|
+ FamilyMember otherFamilyChild = managedChild(OTHER_FAMILY_MEMBER_ID, ageYearsAgo(8), null);
|
|
|
+ otherFamilyChild.setFamilyId(888L);
|
|
|
+ when(familyMemberMapper.selectById(OTHER_FAMILY_MEMBER_ID)).thenReturn(otherFamilyChild);
|
|
|
+
|
|
|
+ AccessDeniedException ex = assertThrows(AccessDeniedException.class,
|
|
|
+ () -> familyMemberService.switchToMember(PARENT_USER_ID, OTHER_FAMILY_MEMBER_ID));
|
|
|
+ assertEquals("成员不存在或不属于您的家庭", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-2:切换到自己 → IllegalArgumentException「不能切换到自己」")
|
|
|
+ void service_切换到自己_拒绝() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ // 家庭主账号成员(userId = 本人),走 family_members 段自切判定
|
|
|
+ FamilyMember selfMember = managedChild(CHILD_B_MEMBER_ID, ageYearsAgo(35), null);
|
|
|
+ selfMember.setUserId(PARENT_USER_ID);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(selfMember);
|
|
|
+
|
|
|
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
|
|
+ () -> familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID));
|
|
|
+ assertEquals("不能切换到自己", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-3:目标为可登录成员(有 openid)→ 拒绝「不能切换为TA的身份」")
|
|
|
+ void service_目标是可登录成员_拒绝() {
|
|
|
+ stubParent(PARENT_USER_ID);
|
|
|
+ // 可登录成员:userId 关联 User 有 openid
|
|
|
+ FamilyMember loginMember = managedChild(CHILD_B_MEMBER_ID, ageYearsAgo(12), null);
|
|
|
+ loginMember.setUserId(LOGIN_MEMBER_USER_ID);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(loginMember);
|
|
|
+ User memberUser = new User();
|
|
|
+ memberUser.setId(LOGIN_MEMBER_USER_ID);
|
|
|
+ memberUser.setOpenid("wx_openid_" + LOGIN_MEMBER_USER_ID);
|
|
|
+ when(userMapper.selectById(LOGIN_MEMBER_USER_ID)).thenReturn(memberUser);
|
|
|
+
|
|
|
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
|
|
+ () -> familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID));
|
|
|
+ assertEquals("该成员可通过微信登录,不能切换为TA的身份", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("异常流-4:用户未加入家庭 → IllegalArgumentException「用户未加入家庭」")
|
|
|
+ void service_用户未加入家庭_拒绝() {
|
|
|
+ User noFamily = new User();
|
|
|
+ noFamily.setId(PARENT_USER_ID);
|
|
|
+ noFamily.setFamilyId(null);
|
|
|
+ when(userMapper.selectById(PARENT_USER_ID)).thenReturn(noFamily);
|
|
|
+
|
|
|
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
|
|
+ () -> familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID));
|
|
|
+ assertEquals("用户未加入家庭", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // Service 权限流(TC-P-73-P:双角色铁律——孩子端不能切回家长)
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流:直接登录孩子端(role=child)切换到家长 effectiveRole → AccessDenied")
|
|
|
+ void service_孩子端切换到家长视角_拒绝() {
|
|
|
+ // 操作者 = 直接登录的孩子(role=child,有 familyId)
|
|
|
+ User childOperator = new User();
|
|
|
+ childOperator.setId(PARENT_USER_ID);
|
|
|
+ childOperator.setFamilyId(FAMILY_ID);
|
|
|
+ childOperator.setRole("child");
|
|
|
+ when(userMapper.selectById(PARENT_USER_ID)).thenReturn(childOperator);
|
|
|
+ // 目标成员 effectiveRole=parent(成年生日),触发「孩子端不能切回家长」铁律
|
|
|
+ FamilyMember adultMember = managedChild(CHILD_B_MEMBER_ID, ageYearsAgo(30), null);
|
|
|
+ when(familyMemberMapper.selectById(CHILD_B_MEMBER_ID)).thenReturn(adultMember);
|
|
|
+
|
|
|
+ AccessDeniedException ex = assertThrows(AccessDeniedException.class,
|
|
|
+ () -> familyMemberService.switchToMember(PARENT_USER_ID, CHILD_B_MEMBER_ID));
|
|
|
+ assertEquals("家庭成员账号不能切换到本人视角", ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ====================================================================
|
|
|
+ // 辅助构造
|
|
|
+ // ====================================================================
|
|
|
+
|
|
|
+ /** 构造家长 User(role=parent,已加入 FAMILY_ID 家庭) */
|
|
|
+ private void stubParent(Long userId) {
|
|
|
+ User parent = new User();
|
|
|
+ parent.setId(userId);
|
|
|
+ parent.setFamilyId(FAMILY_ID);
|
|
|
+ parent.setRole("parent");
|
|
|
+ when(userMapper.selectById(userId)).thenReturn(parent);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造不可登录孩子成员(managed child,userId=null,归属 FAMILY_ID) */
|
|
|
+ private FamilyMember managedChild(Long memberId, Date birthday, String nickname) {
|
|
|
+ FamilyMember fm = new FamilyMember();
|
|
|
+ fm.setId(memberId);
|
|
|
+ fm.setFamilyId(FAMILY_ID);
|
|
|
+ fm.setUserId(null);
|
|
|
+ fm.setBirthday(birthday);
|
|
|
+ fm.setNickname(nickname);
|
|
|
+ return fm;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构造孩子上下文 SwitchMemberVO(契约响应形态) */
|
|
|
+ private SwitchMemberVO childContextVo(Long memberId, String effectiveRole) {
|
|
|
+ SwitchMemberVO vo = new SwitchMemberVO();
|
|
|
+ vo.setMemberId(memberId);
|
|
|
+ vo.setSource("family_member");
|
|
|
+ vo.setEffectiveRole(effectiveRole);
|
|
|
+ vo.setNickname("小宝");
|
|
|
+ vo.setIsSwitchable(true);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> body(Long memberId) {
|
|
|
+ Map<String, Object> b = new HashMap<>();
|
|
|
+ b.put("memberId", memberId);
|
|
|
+ return b;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 距今 N 年的生日 */
|
|
|
+ private Date ageYearsAgo(int years) {
|
|
|
+ Calendar c = Calendar.getInstance();
|
|
|
+ c.add(Calendar.YEAR, -years);
|
|
|
+ return c.getTime();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void setField(Object target, String name, Object value) throws Exception {
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(target, value);
|
|
|
+ }
|
|
|
+}
|