diff --git a/backend/src/main/java/com/xly/test4/module/usr/service/UserService.java b/backend/src/main/java/com/xly/test4/module/usr/service/UserService.java new file mode 100644 index 0000000..73e548a --- /dev/null +++ b/backend/src/main/java/com/xly/test4/module/usr/service/UserService.java @@ -0,0 +1,9 @@ +package com.xly.test4.module.usr.service; + +import com.xly.test4.module.usr.dto.UserCreateDTO; +import com.xly.test4.module.usr.vo.UserCreateVO; + +public interface UserService { + + UserCreateVO createUser(UserCreateDTO dto); +} diff --git a/backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java b/backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..f0d5e93 --- /dev/null +++ b/backend/src/main/java/com/xly/test4/module/usr/service/impl/UserServiceImpl.java @@ -0,0 +1,53 @@ +package com.xly.test4.module.usr.service.impl; + +import com.xly.test4.common.security.CurrentUser; +import com.xly.test4.common.security.CurrentUserContext; +import com.xly.test4.module.usr.converter.UserConverter; +import com.xly.test4.module.usr.dto.UserCreateDTO; +import com.xly.test4.module.usr.entity.User; +import com.xly.test4.module.usr.mapper.UserMapper; +import com.xly.test4.module.usr.service.UserService; +import com.xly.test4.module.usr.vo.UserCreateVO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserServiceImpl implements UserService { + + private final UserMapper userMapper; + private final UserConverter userConverter; + private final PasswordEncoder passwordEncoder; + private final String defaultPassword; + + public UserServiceImpl(UserMapper userMapper, + UserConverter userConverter, + PasswordEncoder passwordEncoder, + @Value("${app.security.default-password}") String defaultPassword) { + this.userMapper = userMapper; + this.userConverter = userConverter; + this.passwordEncoder = passwordEncoder; + this.defaultPassword = defaultPassword; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public UserCreateVO createUser(UserCreateDTO dto) { + CurrentUser current = CurrentUserContext.current(); + + User user = userConverter.toEntity(dto); + user.setSBrandsId(current.getBrandsId()); + user.setSSubsidiaryId(current.getSubsidiaryId()); + user.setSCreatedBy(current.getUserName()); + user.setIIsDisabled(0); + user.setILoginFailCount(0); + + String rawPassword = dto.getPassword() != null ? dto.getPassword() : defaultPassword; + user.setSPasswordHash(passwordEncoder.encode(rawPassword)); + + userMapper.insert(user); + + return userConverter.toVO(user); + } +} diff --git a/backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java b/backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java new file mode 100644 index 0000000..b8ff942 --- /dev/null +++ b/backend/src/test/java/com/xly/test4/module/usr/service/impl/UserServiceImplTest.java @@ -0,0 +1,116 @@ +package com.xly.test4.module.usr.service.impl; + +import com.xly.test4.common.security.CurrentUser; +import com.xly.test4.common.security.CurrentUserContext; +import com.xly.test4.module.usr.converter.UserConverter; +import com.xly.test4.module.usr.dto.UserCreateDTO; +import com.xly.test4.module.usr.entity.User; +import com.xly.test4.module.usr.mapper.UserMapper; +import com.xly.test4.module.usr.vo.UserCreateVO; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +class UserServiceImplTest { + + private UserMapper userMapper; + private UserConverter userConverter; + private PasswordEncoder passwordEncoder; + private UserServiceImpl service; + private MockedStatic ctxMock; + + private static final CurrentUser ADMIN_CONTEXT = CurrentUser.builder() + .userId(1) + .userName("admin") + .brandsId("BR-DEFAULT") + .subsidiaryId("SUB-DEFAULT") + .authorities(List.of("usr:user:create")) + .build(); + + @BeforeEach + void setUp() { + userMapper = mock(UserMapper.class); + userConverter = mock(UserConverter.class); + passwordEncoder = new BCryptPasswordEncoder(); + service = new UserServiceImpl(userMapper, userConverter, passwordEncoder, "666666"); + ctxMock = mockStatic(CurrentUserContext.class); + ctxMock.when(CurrentUserContext::current).thenReturn(ADMIN_CONTEXT); + } + + @AfterEach + void tearDown() { + ctxMock.close(); + } + + private UserCreateDTO baseDTO() { + UserCreateDTO dto = new UserCreateDTO(); + dto.setUserCode("U-NEW-001"); + dto.setUserName("newuser"); + dto.setUserType("NORMAL"); + dto.setLanguage("zh-CN"); + dto.setCanEditDoc(false); + return dto; + } + + private void stubConverterReturnsEmptyUser() { + when(userConverter.toEntity(any(UserCreateDTO.class))).thenAnswer(inv -> new User()); + when(userConverter.toVO(any(User.class))).thenAnswer(inv -> { + User u = inv.getArgument(0); + return UserCreateVO.builder().userId(u.getIIncrement()).userCode(u.getSUserCode()).build(); + }); + when(userMapper.insert(any(User.class))).thenAnswer(inv -> { + User u = inv.getArgument(0); + u.setIIncrement(42); + return 1; + }); + } + + @Test + void createUser_minimalDTO_writesSecurityContextFieldsAndHashesPassword() { + stubConverterReturnsEmptyUser(); + UserCreateDTO dto = baseDTO(); + dto.setPassword("MyPass123"); + + UserCreateVO vo = service.createUser(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + org.mockito.Mockito.verify(userMapper).insert(captor.capture()); + User written = captor.getValue(); + + assertThat(written.getSCreatedBy()).isEqualTo("admin"); + assertThat(written.getSBrandsId()).isEqualTo("BR-DEFAULT"); + assertThat(written.getSSubsidiaryId()).isEqualTo("SUB-DEFAULT"); + assertThat(written.getIIsDisabled()).isZero(); + assertThat(written.getILoginFailCount()).isZero(); + assertThat(written.getSPasswordHash()).startsWith("$2"); + assertThat(written.getSPasswordHash()).hasSize(60); + assertThat(passwordEncoder.matches("MyPass123", written.getSPasswordHash())).isTrue(); + + assertThat(vo.getUserId()).isEqualTo(42); + } + + @Test + void createUser_noPasswordInDTO_usesDefaultPassword666666() { + stubConverterReturnsEmptyUser(); + UserCreateDTO dto = baseDTO(); + dto.setPassword(null); + + service.createUser(dto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + org.mockito.Mockito.verify(userMapper).insert(captor.capture()); + assertThat(passwordEncoder.matches("666666", captor.getValue().getSPasswordHash())).isTrue(); + } +}