test(auth): cover security-critical service impls with mockito

Add Mockito-based unit tests for the four ServiceImpl classes that the
TokenServiceImpl pipeline transitively trusts. Other ServiceImpls in
dc3-common-auth follow the same MyBatis-Plus CRUD shape and will be
covered as Testcontainers PG slice tests in a later stage where the
real schema can validate the lambda query wrappers end-to-end.

- TenantServiceImplTest (16): save / remove / update / selectById /
  selectByCode / selectByPage. Covers DuplicateException on save,
  NotFoundException on remove and update, AddException / DeleteException
  / UpdateException when the manager returns false, the same-row
  duplicate check during update, and Pages defaulting / preservation.
- UserLoginServiceImplTest (12): the standard CRUD trio plus the four
  selectByLoginName branches (blank + throw / blank + silent /
  not-found + throw / happy path) and the three checkLoginNameValid
  outcomes (unknown / disabled / enabled).
- UserPasswordServiceImplTest (9): the CRUD trio plus the password
  hashing assertion via DecodeUtil.md5 (save and update both rehash
  before persistence). Reset-password is asserted to set
  AlgorithmConstant.DEFAULT_PASSWORD before delegating to update.
- TenantBindServiceImplTest (8): CRUD trio plus the
  selectByTenantIdAndUserId branches and the null-id short circuit on
  listUserIdsByTenantId. The happy path of listUserIdsByTenantId
  requires mybatis-plus lambda metadata not available without a real
  ORM context and is deferred to the slice test stage.

Verified locally: 45 new cases green; dc3-common-auth now ships 69
unit tests across biz + service tiers.
This commit is contained in:
Vickey
2026-05-13 16:01:22 +08:00
parent fb99b84aa0
commit afaa899c19
4 changed files with 745 additions and 0 deletions
@@ -0,0 +1,151 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.auth.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.github.pnoker.common.auth.dal.TenantBindManager;
import io.github.pnoker.common.auth.entity.bo.TenantBindBO;
import io.github.pnoker.common.auth.entity.builder.TenantBindBuilder;
import io.github.pnoker.common.auth.entity.model.TenantBindDO;
import io.github.pnoker.common.exception.AddException;
import io.github.pnoker.common.exception.DeleteException;
import io.github.pnoker.common.exception.DuplicateException;
import io.github.pnoker.common.exception.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TenantBindServiceImplTest {
@Mock
private TenantBindBuilder tenantBindBuilder;
@Mock
private TenantBindManager tenantBindManager;
@InjectMocks
private TenantBindServiceImpl service;
private TenantBindBO bo;
private TenantBindDO doRow;
@BeforeEach
void setUp() throws Exception {
bo = new TenantBindBO();
setField(bo, "id", 5L);
setField(bo, "tenantId", 1L);
setField(bo, "userId", 7L);
doRow = new TenantBindDO();
doRow.setId(5L);
doRow.setTenantId(1L);
doRow.setUserId(7L);
}
@Test
void saveSucceedsWhenNoDuplicate() {
when(tenantBindManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBindBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantBindManager.save(doRow)).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.save(bo));
}
@Test
void saveRejectsDuplicateBinding() {
when(tenantBindManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(DuplicateException.class);
verify(tenantBindManager, never()).save(any());
}
@Test
void saveThrowsAddExceptionWhenManagerReturnsFalse() {
when(tenantBindManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBindBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantBindManager.save(doRow)).thenReturn(false);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(AddException.class);
}
@Test
void removeRejectsUnknownId() {
when(tenantBindManager.getById(5L)).thenReturn(null);
assertThatThrownBy(() -> service.remove(5L)).isInstanceOf(NotFoundException.class);
}
@Test
void removeThrowsDeleteExceptionWhenManagerReturnsFalse() {
when(tenantBindManager.getById(5L)).thenReturn(doRow);
when(tenantBindManager.removeById(5L)).thenReturn(false);
assertThatThrownBy(() -> service.remove(5L)).isInstanceOf(DeleteException.class);
}
@Test
void selectByTenantIdAndUserIdReturnsBoFromDal() {
when(tenantBindManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(tenantBindBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.selectByTenantIdAndUserId(1L, 7L)).isSameAs(bo);
}
@Test
void selectByTenantIdAndUserIdReturnsNullWhenMissing() {
when(tenantBindManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBindBuilder.buildBOByDO(null)).thenReturn(null);
assertThat(service.selectByTenantIdAndUserId(1L, 7L)).isNull();
}
@Test
void listUserIdsByTenantIdReturnsEmptyForNull() {
assertThat(service.listUserIdsByTenantId(null)).isEmpty();
verify(tenantBindManager, never()).listObjs(any(LambdaQueryWrapper.class), any());
}
// The happy-path of listUserIdsByTenantId requires mybatis-plus lambda metadata
// which is unavailable in pure Mockito; that branch is exercised by the
// Testcontainers PG slice test introduced in a later stage.
private static void setField(Object target, String name, Object value) throws Exception {
Field field = findField(target.getClass(), name);
field.setAccessible(true);
field.set(target, value);
}
private static Field findField(Class<?> type, String name) throws NoSuchFieldException {
Class<?> current = type;
while (current != null) {
try {
return current.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}
}
@@ -0,0 +1,234 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.auth.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.pnoker.common.auth.dal.TenantManager;
import io.github.pnoker.common.auth.entity.bo.TenantBO;
import io.github.pnoker.common.auth.entity.builder.TenantBuilder;
import io.github.pnoker.common.auth.entity.model.TenantDO;
import io.github.pnoker.common.auth.entity.query.TenantQuery;
import io.github.pnoker.common.entity.common.Pages;
import io.github.pnoker.common.exception.AddException;
import io.github.pnoker.common.exception.DeleteException;
import io.github.pnoker.common.exception.DuplicateException;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.exception.UpdateException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TenantServiceImplTest {
@Mock
private TenantBuilder tenantBuilder;
@Mock
private TenantManager tenantManager;
@InjectMocks
private TenantServiceImpl service;
private TenantBO bo;
private TenantDO doRow;
@BeforeEach
void setUp() throws Exception {
bo = new TenantBO();
setField(bo, "id", 1L);
bo.setTenantName("Acme");
bo.setTenantCode("acme");
doRow = new TenantDO();
doRow.setId(1L);
doRow.setTenantName("Acme");
doRow.setTenantCode("acme");
}
@Test
void saveSucceedsWhenManagerAccepts() {
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantManager.save(doRow)).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.save(bo));
verify(tenantManager).save(doRow);
}
@Test
void saveRejectsDuplicate() {
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
assertThatThrownBy(() -> service.save(bo))
.isInstanceOf(DuplicateException.class)
.hasMessageContaining("Tenant has been duplicated");
verify(tenantManager, never()).save(any());
}
@Test
void saveThrowsAddExceptionWhenManagerReturnsFalse() {
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantManager.save(doRow)).thenReturn(false);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(AddException.class);
}
@Test
void removeSucceedsForExistingId() {
when(tenantManager.getById(1L)).thenReturn(doRow);
when(tenantManager.removeById(1L)).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.remove(1L));
verify(tenantManager).removeById(1L);
}
@Test
void removeRejectsUnknownId() {
when(tenantManager.getById(99L)).thenReturn(null);
assertThatThrownBy(() -> service.remove(99L))
.isInstanceOf(NotFoundException.class);
}
@Test
void removeThrowsDeleteExceptionWhenManagerReturnsFalse() {
when(tenantManager.getById(1L)).thenReturn(doRow);
when(tenantManager.removeById(1L)).thenReturn(false);
assertThatThrownBy(() -> service.remove(1L)).isInstanceOf(DeleteException.class);
}
@Test
void updateRejectsUnknownId() {
when(tenantManager.getById(1L)).thenReturn(null);
assertThatThrownBy(() -> service.update(bo)).isInstanceOf(NotFoundException.class);
}
@Test
void updateRejectsDuplicateAcrossDifferentRow() {
when(tenantManager.getById(1L)).thenReturn(doRow);
TenantDO other = new TenantDO();
other.setId(2L);
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(other);
assertThatThrownBy(() -> service.update(bo)).isInstanceOf(DuplicateException.class);
}
@Test
void updateAcceptsSameRowOnDuplicateCheck() {
when(tenantManager.getById(1L)).thenReturn(doRow);
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(tenantBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantManager.updateById(doRow)).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.update(bo));
}
@Test
void updateThrowsUpdateExceptionWhenManagerReturnsFalse() {
when(tenantManager.getById(1L)).thenReturn(doRow);
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(tenantBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(tenantManager.updateById(doRow)).thenReturn(false);
assertThatThrownBy(() -> service.update(bo)).isInstanceOf(UpdateException.class);
}
@Test
void selectByIdReturnsBoForKnownId() {
when(tenantManager.getById(1L)).thenReturn(doRow);
when(tenantBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.selectById(1L)).isSameAs(bo);
}
@Test
void selectByIdRejectsUnknownId() {
when(tenantManager.getById(99L)).thenReturn(null);
assertThatThrownBy(() -> service.selectById(99L)).isInstanceOf(NotFoundException.class);
}
@Test
void selectByCodeFiltersOnEnabledTenants() {
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(tenantBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.selectByCode("acme")).isSameAs(bo);
}
@Test
void selectByCodeReturnsNullWhenNoMatch() {
when(tenantManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(tenantBuilder.buildBOByDO(null)).thenReturn(null);
assertThat(service.selectByCode("missing")).isNull();
}
@Test
void selectByPageInjectsDefaultPagesWhenMissing() {
TenantQuery query = new TenantQuery();
Page<TenantDO> doPage = new Page<>(1, 10);
when(tenantManager.page(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(doPage);
Page<TenantBO> boPage = new Page<>(1, 10);
when(tenantBuilder.buildBOPageByDOPage(doPage)).thenReturn(boPage);
Page<TenantBO> result = service.selectByPage(query);
assertThat(query.getPage()).isNotNull();
assertThat(result).isSameAs(boPage);
}
@Test
void selectByPagePreservesProvidedPagination() {
TenantQuery query = new TenantQuery();
Pages pages = new Pages();
pages.setCurrent(2);
pages.setSize(50);
query.setPage(pages);
Page<TenantDO> doPage = new Page<>(2, 50);
when(tenantManager.page(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(doPage);
Page<TenantBO> boPage = new Page<>(2, 50);
when(tenantBuilder.buildBOPageByDOPage(doPage)).thenReturn(boPage);
assertThat(service.selectByPage(query)).isSameAs(boPage);
assertThat(query.getPage()).isSameAs(pages);
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = findField(target.getClass(), name);
field.setAccessible(true);
field.set(target, value);
}
private static Field findField(Class<?> type, String name) throws NoSuchFieldException {
Class<?> current = type;
while (current != null) {
try {
return current.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}
}
@@ -0,0 +1,181 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.auth.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.github.pnoker.common.auth.dal.UserLoginManager;
import io.github.pnoker.common.auth.entity.bo.UserLoginBO;
import io.github.pnoker.common.auth.entity.builder.UserLoginBuilder;
import io.github.pnoker.common.auth.entity.model.UserLoginDO;
import io.github.pnoker.common.auth.service.TenantBindService;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.exception.AddException;
import io.github.pnoker.common.exception.DeleteException;
import io.github.pnoker.common.exception.DuplicateException;
import io.github.pnoker.common.exception.EmptyException;
import io.github.pnoker.common.exception.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UserLoginServiceImplTest {
@Mock
private UserLoginBuilder userLoginBuilder;
@Mock
private UserLoginManager userLoginManager;
@Mock
private TenantBindService tenantBindService;
@InjectMocks
private UserLoginServiceImpl service;
private UserLoginBO bo;
private UserLoginDO doRow;
@BeforeEach
void setUp() throws Exception {
bo = new UserLoginBO();
setField(bo, "id", 7L);
setField(bo, "userId", 1L);
setField(bo, "loginName", "alice");
doRow = new UserLoginDO();
doRow.setId(7L);
doRow.setUserId(1L);
doRow.setLoginName("alice");
}
@Test
void saveSucceedsWhenNoDuplicate() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(userLoginBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userLoginManager.save(doRow)).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.save(bo));
}
@Test
void saveRejectsDuplicateLoginName() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(DuplicateException.class);
verify(userLoginManager, never()).save(any());
}
@Test
void saveThrowsAddExceptionWhenManagerReturnsFalse() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(userLoginBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userLoginManager.save(doRow)).thenReturn(false);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(AddException.class);
}
@Test
void removeRejectsUnknownId() {
when(userLoginManager.getById(7L)).thenReturn(null);
assertThatThrownBy(() -> service.remove(7L)).isInstanceOf(NotFoundException.class);
}
@Test
void removeThrowsDeleteExceptionWhenManagerReturnsFalse() {
when(userLoginManager.getById(7L)).thenReturn(doRow);
when(userLoginManager.removeById(7L)).thenReturn(false);
assertThatThrownBy(() -> service.remove(7L)).isInstanceOf(DeleteException.class);
}
@Test
void selectByLoginNameThrowsEmptyExceptionForBlankWhenAsked() {
assertThatThrownBy(() -> service.selectByLoginName("", true))
.isInstanceOf(EmptyException.class);
}
@Test
void selectByLoginNameReturnsNullForBlankWhenSilent() {
assertThat(service.selectByLoginName(null, false)).isNull();
assertThat(service.selectByLoginName("", false)).isNull();
}
@Test
void selectByLoginNameThrowsNotFoundWhenAskedAndMissing() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
assertThatThrownBy(() -> service.selectByLoginName("missing", true))
.isInstanceOf(NotFoundException.class);
}
@Test
void selectByLoginNameReturnsBoForExistingEnabledLogin() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userLoginBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.selectByLoginName("alice", false)).isSameAs(bo);
}
@Test
void checkLoginNameValidReturnsFalseForUnknownLogin() {
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
assertThat(service.checkLoginNameValid("ghost")).isFalse();
}
@Test
void checkLoginNameValidReturnsTrueForEnabledLogin() throws Exception {
setField(bo, "enableFlag", EnableFlagEnum.ENABLE);
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userLoginBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.checkLoginNameValid("alice")).isTrue();
}
@Test
void checkLoginNameValidReturnsFalseForDisabledLogin() throws Exception {
setField(bo, "enableFlag", EnableFlagEnum.DISABLE);
when(userLoginManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userLoginBuilder.buildBOByDO(doRow)).thenReturn(bo);
assertThat(service.checkLoginNameValid("alice")).isFalse();
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = findField(target.getClass(), name);
field.setAccessible(true);
field.set(target, value);
}
private static Field findField(Class<?> type, String name) throws NoSuchFieldException {
Class<?> current = type;
while (current != null) {
try {
return current.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}
}
@@ -0,0 +1,179 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.auth.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.github.pnoker.common.auth.dal.UserPasswordManager;
import io.github.pnoker.common.auth.entity.bo.UserPasswordBO;
import io.github.pnoker.common.auth.entity.builder.UserPasswordBuilder;
import io.github.pnoker.common.auth.entity.model.UserPasswordDO;
import io.github.pnoker.common.constant.common.AlgorithmConstant;
import io.github.pnoker.common.exception.AddException;
import io.github.pnoker.common.exception.DeleteException;
import io.github.pnoker.common.exception.DuplicateException;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.exception.UpdateException;
import io.github.pnoker.common.utils.DecodeUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UserPasswordServiceImplTest {
@Mock
private UserPasswordBuilder userPasswordBuilder;
@Mock
private UserPasswordManager userPasswordManager;
@InjectMocks
private UserPasswordServiceImpl service;
private UserPasswordBO bo;
private UserPasswordDO doRow;
@BeforeEach
void setUp() throws Exception {
bo = new UserPasswordBO();
setField(bo, "id", 9L);
setField(bo, "loginPassword", "secret");
doRow = new UserPasswordDO();
doRow.setId(9L);
doRow.setLoginPassword("secret");
}
@Test
void saveHashesPasswordBeforePersisting() {
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(userPasswordBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userPasswordManager.save(doRow)).thenReturn(true);
service.save(bo);
ArgumentCaptor<UserPasswordDO> captor = ArgumentCaptor.forClass(UserPasswordDO.class);
verify(userPasswordManager).save(captor.capture());
assertThat(captor.getValue().getLoginPassword())
.isEqualTo(DecodeUtil.md5("secret"));
}
@Test
void saveRejectsDuplicate() {
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(DuplicateException.class);
}
@Test
void saveThrowsAddExceptionWhenManagerReturnsFalse() {
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(userPasswordBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userPasswordManager.save(any(UserPasswordDO.class))).thenReturn(false);
assertThatThrownBy(() -> service.save(bo)).isInstanceOf(AddException.class);
}
@Test
void removeRejectsUnknownId() {
when(userPasswordManager.getById(9L)).thenReturn(null);
assertThatThrownBy(() -> service.remove(9L)).isInstanceOf(NotFoundException.class);
}
@Test
void removeThrowsDeleteExceptionWhenManagerReturnsFalse() {
when(userPasswordManager.getById(9L)).thenReturn(doRow);
when(userPasswordManager.removeById(9L)).thenReturn(false);
assertThatThrownBy(() -> service.remove(9L)).isInstanceOf(DeleteException.class);
}
@Test
void updateRehashesPassword() {
when(userPasswordManager.getById(9L)).thenReturn(doRow);
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userPasswordBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userPasswordManager.updateById(any(UserPasswordDO.class))).thenReturn(true);
service.update(bo);
ArgumentCaptor<UserPasswordDO> captor = ArgumentCaptor.forClass(UserPasswordDO.class);
verify(userPasswordManager).updateById(captor.capture());
assertThat(captor.getValue().getLoginPassword())
.isEqualTo(DecodeUtil.md5("secret"));
}
@Test
void updateThrowsUpdateExceptionWhenManagerReturnsFalse() {
when(userPasswordManager.getById(9L)).thenReturn(doRow);
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userPasswordBuilder.buildDOByBO(bo)).thenReturn(doRow);
when(userPasswordManager.updateById(any(UserPasswordDO.class))).thenReturn(false);
assertThatThrownBy(() -> service.update(bo)).isInstanceOf(UpdateException.class);
}
@Test
void resetPasswordReplacesValueWithDefaultBeforeUpdate() throws Exception {
UserPasswordBO restored = new UserPasswordBO();
setField(restored, "id", 9L);
setField(restored, "loginPassword", "old");
when(userPasswordManager.getById(9L)).thenReturn(doRow, doRow);
when(userPasswordBuilder.buildBOByDO(doRow)).thenReturn(restored);
when(userPasswordManager.getOne(any(LambdaQueryWrapper.class))).thenReturn(doRow);
when(userPasswordBuilder.buildDOByBO(restored)).thenReturn(doRow);
when(userPasswordManager.updateById(any(UserPasswordDO.class))).thenReturn(true);
assertThatNoException().isThrownBy(() -> service.restPassword(9L));
assertThat(restored.getLoginPassword()).isEqualTo(AlgorithmConstant.DEFAULT_PASSWORD);
}
@Test
void resetPasswordIsNoopWhenRecordMissing() {
when(userPasswordManager.getById(9L)).thenReturn(null);
assertThatThrownBy(() -> service.restPassword(9L)).isInstanceOf(NotFoundException.class);
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = findField(target.getClass(), name);
field.setAccessible(true);
field.set(target, value);
}
private static Field findField(Class<?> type, String name) throws NoSuchFieldException {
Class<?> current = type;
while (current != null) {
try {
return current.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(name);
}
}