package com.ruoyi.app.user; import com.ruoyi.app.user.dto.MerchantSubaccountCreateRequest; import com.ruoyi.app.user.dto.StoreAccountsView; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.PosStore; import com.ruoyi.system.mapper.PosStoreMapper; import com.ruoyi.system.service.IInfoUserService; import com.ruoyi.system.service.IMerchantSubaccountStoreService; import com.ruoyi.system.service.MerchantStoreAccessService; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MerchantSubaccountApplicationServiceTest { @Test void newSubaccountUsesTelPhoneAsTheBusinessPhoneField() { IInfoUserService infoUserService = mock(IInfoUserService.class); IMerchantSubaccountStoreService relationService = mock(IMerchantSubaccountStoreService.class); MerchantStoreAccessService accessService = mock(MerchantStoreAccessService.class); PosStoreMapper posStoreMapper = mock(PosStoreMapper.class); MerchantTokenSessionService tokenSessionService = mock(MerchantTokenSessionService.class); BusinessPhoneService businessPhoneService = mock(BusinessPhoneService.class); MerchantSubaccountApplicationService service = new MerchantSubaccountApplicationService( infoUserService, relationService, accessService, posStoreMapper, tokenSessionService, businessPhoneService); when(accessService.getAccessibleStoreIds(10L)).thenReturn(Set.of(7L)); when(infoUserService.getinfouserName("0912345678")).thenReturn(null); doAnswer(invocation -> { InfoUser user = invocation.getArgument(0); user.setUserId(20L); return 1; }).when(infoUserService).insertInfoUser(any(InfoUser.class)); when(relationService.selectStoreIdsBySubaccountUserId(20L)).thenReturn(List.of()); MerchantSubaccountCreateRequest request = new MerchantSubaccountCreateRequest(); request.setPhone("0912345678"); request.setUserName("store_staff"); request.setName("分店账号"); request.setPassword("password"); request.setStoreIds(List.of(7L)); var created = service.create(10L, request); assertEquals("store_staff", created.getUserName()); ArgumentCaptor captor = ArgumentCaptor.forClass(InfoUser.class); verify(infoUserService).insertInfoUser(captor.capture()); assertEquals("store_staff", captor.getValue().getUserName()); assertEquals("0912345678", captor.getValue().getTelPhone()); assertNull(captor.getValue().getPhone()); verify(businessPhoneService).ensureUnique("0912345678", null); } @Test void listStoreAccountsRejectsMissingOrDeletedStore() { PosStoreMapper posStoreMapper = mock(PosStoreMapper.class); MerchantSubaccountApplicationService service = newService(posStoreMapper); when(posStoreMapper.selectPosStoreById(7L)).thenReturn(null); PosStore deleted = new PosStore(); deleted.setDelFlag("1"); when(posStoreMapper.selectPosStoreById(8L)).thenReturn(deleted); try (MockedStatic messages = mockStatic(MessageUtils.class)) { messages.when(() -> MessageUtils.message(anyString())) .thenAnswer(invocation -> invocation.getArgument(0)); ServiceException missing = assertThrows(ServiceException.class, () -> service.listStoreAccounts(7L)); assertEquals("merchant.store.not.found", missing.getMessage()); ServiceException deletedStore = assertThrows(ServiceException.class, () -> service.listStoreAccounts(8L)); assertEquals("merchant.store.not.found", deletedStore.getMessage()); } } @Test void listStoreAccountsReturnsOwnerAndEligibleSubaccountsWithContactFields() { IInfoUserService infoUserService = mock(IInfoUserService.class); IMerchantSubaccountStoreService relationService = mock(IMerchantSubaccountStoreService.class); PosStoreMapper posStoreMapper = mock(PosStoreMapper.class); MerchantSubaccountApplicationService service = newService(infoUserService, relationService, posStoreMapper); PosStore store = new PosStore(); store.setDelFlag("0"); store.setUserId(10L); when(posStoreMapper.selectPosStoreById(7L)).thenReturn(store); when(infoUserService.selectInfoUserByUserId(10L)).thenReturn(contactUser(10L, "1", "0")); when(relationService.selectEligibleSubaccountsByStoreId(7L)) .thenReturn(List.of(contactUser(20L, "5", "0"))); StoreAccountsView view = service.listStoreAccounts(7L); assertEquals(10L, view.getOwner().getUserId()); assertEquals("1", view.getOwner().getUserType()); assertEquals("nick-10", view.getOwner().getNickName()); assertEquals("tel-10", view.getOwner().getPhone()); assertEquals("avatar-10", view.getOwner().getAvatar()); assertEquals("imKey-10", view.getOwner().getImApiKey()); assertEquals("900010", view.getOwner().getImUserId()); assertEquals("cid-10", view.getOwner().getCid()); assertEquals("ios", view.getOwner().getCidType()); assertEquals("device-10", view.getOwner().getDeviceToken()); assertEquals("voip-10", view.getOwner().getVoIPToken()); assertEquals(1, view.getSubaccounts().size()); assertEquals(20L, view.getSubaccounts().get(0).getUserId()); assertEquals("5", view.getSubaccounts().get(0).getUserType()); assertEquals("nick-20", view.getSubaccounts().get(0).getNickName()); } @Test void listStoreAccountsOmitsUnavailableOwner() { IInfoUserService infoUserService = mock(IInfoUserService.class); IMerchantSubaccountStoreService relationService = mock(IMerchantSubaccountStoreService.class); PosStoreMapper posStoreMapper = mock(PosStoreMapper.class); MerchantSubaccountApplicationService service = newService(infoUserService, relationService, posStoreMapper); PosStore store = new PosStore(); store.setDelFlag("0"); store.setUserId(10L); when(posStoreMapper.selectPosStoreById(7L)).thenReturn(store); when(infoUserService.selectInfoUserByUserId(10L)).thenReturn(contactUser(10L, "1", "1")); when(relationService.selectEligibleSubaccountsByStoreId(7L)).thenReturn(List.of()); StoreAccountsView view = service.listStoreAccounts(7L); assertNull(view.getOwner()); assertEquals(List.of(), view.getSubaccounts()); } private MerchantSubaccountApplicationService newService(PosStoreMapper posStoreMapper) { return newService(mock(IInfoUserService.class), mock(IMerchantSubaccountStoreService.class), posStoreMapper); } private MerchantSubaccountApplicationService newService(IInfoUserService infoUserService, IMerchantSubaccountStoreService relationService, PosStoreMapper posStoreMapper) { return new MerchantSubaccountApplicationService(infoUserService, relationService, mock(MerchantStoreAccessService.class), posStoreMapper, mock(MerchantTokenSessionService.class), mock(BusinessPhoneService.class)); } private InfoUser contactUser(Long userId, String userType, String status) { InfoUser user = new InfoUser(); user.setUserId(userId); user.setUserType(userType); user.setStatus(status); user.setDelFlag("0"); user.setNickName("nick-" + userId); user.setTelPhone("tel-" + userId); user.setAvatar("avatar-" + userId); user.setImApiKey("imKey-" + userId); user.setImUserId(900000L + userId); user.setCid("cid-" + userId); user.setCidType("ios"); user.setDeviceToken("device-" + userId); user.setVoIPToken("voip-" + userId); return user; } @Test void missingUsernameDoesNotFallBackToPhone() { var fixture = new UsernameFixture(); fixture.request.setUserName(" "); try (MockedStatic messages = mockStatic(MessageUtils.class)) { messages.when(() -> MessageUtils.message(anyString())).thenAnswer(i -> i.getArgument(0)); assertEquals("merchant.subaccount.username.required", assertThrows(ServiceException.class, () -> fixture.service.create(10L, fixture.request)).getMessage()); } org.mockito.Mockito.verify(fixture.users, org.mockito.Mockito.never()).insertInfoUser(any()); } @Test void usernameCannotCollideWithAnotherAccount() { var fixture = new UsernameFixture(); InfoUser other = new InfoUser(); other.setUserId(99L); when(fixture.users.getinfouserName("staff")).thenReturn(other); try (MockedStatic messages = mockStatic(MessageUtils.class)) { messages.when(() -> MessageUtils.message(anyString())).thenAnswer(i -> i.getArgument(0)); assertEquals("merchant.subaccount.username.exists", assertThrows(ServiceException.class, () -> fixture.service.create(10L, fixture.request)).getMessage()); } } @Test void renameExistingSubaccountPreservesContactAndRevokesSessions() { var fixture = new UsernameFixture(); var request = new com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest(); request.setUserName(" new_staff "); request.setStoreIds(List.of(7L)); var view = fixture.service.update(10L, 20L, request); assertEquals("new_staff", view.getUserName()); assertEquals("0912345678", view.getPhone()); ArgumentCaptor update = ArgumentCaptor.forClass(InfoUser.class); verify(fixture.users).updateInfoUser(update.capture()); assertEquals("new_staff", update.getValue().getUserName()); assertNull(update.getValue().getTelPhone()); verify(fixture.sessions).revokeAll(20L); } @Test void unchangedUsernameCanBelongToSelfWithoutRevokingSessions() { var fixture = new UsernameFixture(); when(fixture.users.getinfouserName("0912345678")).thenReturn(fixture.target); var request = new com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest(); request.setUserName("0912345678"); request.setStoreIds(List.of(7L)); assertEquals("0912345678", fixture.service.update(10L, 20L, request).getUserName()); org.mockito.Mockito.verify(fixture.sessions, org.mockito.Mockito.never()).revokeAll(20L); } private static class UsernameFixture { final IInfoUserService users = mock(IInfoUserService.class); final MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class); final InfoUser target = new InfoUser(); final MerchantSubaccountCreateRequest request = new MerchantSubaccountCreateRequest(); final MerchantSubaccountApplicationService service; UsernameFixture() { var access = mock(MerchantStoreAccessService.class); var relations = mock(IMerchantSubaccountStoreService.class); service = new MerchantSubaccountApplicationService(users, relations, access, mock(PosStoreMapper.class), sessions, mock(BusinessPhoneService.class)); when(access.getAccessibleStoreIds(10L)).thenReturn(Set.of(7L)); target.setUserId(20L); target.setUserType("5"); target.setDelFlag("0"); target.setMerchantOwnerId(10L); target.setUserName("0912345678"); target.setTelPhone("0912345678"); when(users.selectInfoUserByUserId(20L)).thenReturn(target); when(users.updateInfoUser(any())).thenReturn(1); request.setUserName("staff"); request.setPhone("0912345678"); request.setPassword("encrypted-password"); request.setStoreIds(List.of(7L)); } } }