From afaa899c19fcf6dfd9626517db0357770b768072 Mon Sep 17 00:00:00 2001
From: Vickey <891607480@qq.com>
Date: Wed, 13 May 2026 16:01:22 +0800
Subject: [PATCH] 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.
---
.../impl/TenantBindServiceImplTest.java | 151 +++++++++++
.../service/impl/TenantServiceImplTest.java | 234 ++++++++++++++++++
.../impl/UserLoginServiceImplTest.java | 181 ++++++++++++++
.../impl/UserPasswordServiceImplTest.java | 179 ++++++++++++++
4 files changed, 745 insertions(+)
create mode 100644 dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantBindServiceImplTest.java
create mode 100644 dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantServiceImplTest.java
create mode 100644 dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserLoginServiceImplTest.java
create mode 100644 dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserPasswordServiceImplTest.java
diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantBindServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantBindServiceImplTest.java
new file mode 100644
index 000000000..c2b9afd79
--- /dev/null
+++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantBindServiceImplTest.java
@@ -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 .
+ */
+
+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);
+ }
+}
diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantServiceImplTest.java
new file mode 100644
index 000000000..868ed9764
--- /dev/null
+++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/TenantServiceImplTest.java
@@ -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 .
+ */
+
+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 doPage = new Page<>(1, 10);
+ when(tenantManager.page(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(doPage);
+ Page boPage = new Page<>(1, 10);
+ when(tenantBuilder.buildBOPageByDOPage(doPage)).thenReturn(boPage);
+
+ Page 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 doPage = new Page<>(2, 50);
+ when(tenantManager.page(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(doPage);
+ Page 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);
+ }
+}
diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserLoginServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserLoginServiceImplTest.java
new file mode 100644
index 000000000..cdd2e97db
--- /dev/null
+++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserLoginServiceImplTest.java
@@ -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 .
+ */
+
+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);
+ }
+}
diff --git a/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserPasswordServiceImplTest.java b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserPasswordServiceImplTest.java
new file mode 100644
index 000000000..29d0ba254
--- /dev/null
+++ b/dc3-common/dc3-common-auth/src/test/java/io/github/pnoker/common/auth/service/impl/UserPasswordServiceImplTest.java
@@ -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 .
+ */
+
+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 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 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);
+ }
+}