218 lines
9.3 KiB
Java
218 lines
9.3 KiB
Java
package com.baiyue.auth.service;
|
|
|
|
import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest;
|
|
import com.baiyue.auth.dto.AccountManagementDtos.ForcedPasswordChangeRequest;
|
|
import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest;
|
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest;
|
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest;
|
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest;
|
|
import com.baiyue.auth.entity.AuthAccount;
|
|
import com.baiyue.auth.repository.AuthAccountRepository;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|
|
|
import jakarta.annotation.Resource;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.Mockito.*;
|
|
import java.time.LocalDateTime;
|
|
import java.util.Set;
|
|
import java.util.List;
|
|
|
|
@DisplayName("账号管理服务")
|
|
@SpringJUnitConfig(classes = {AccountManagementService.class, AccountManagementServiceTest.TestConfig.class})
|
|
class AccountManagementServiceTest {
|
|
@MockBean private AuthAccountRepository repository;
|
|
@MockBean private TokenSessionStore sessions;
|
|
@Resource private PasswordEncoder encoder;
|
|
@Resource private AccountManagementService service;
|
|
private AuthAccount account;
|
|
|
|
@BeforeEach void setUp() {
|
|
account = new AuthAccount(); account.setId(10L); account.setPasswordHash(encoder.encode("OldPass1"));
|
|
account.setTokenVersion(2); account.setAccountStatus("ACTIVE");
|
|
when(repository.findById(10L)).thenReturn(account);
|
|
}
|
|
|
|
@Test void changePasswordValidatesCurrentPasswordAndRevokesAllSessions() {
|
|
service.changePassword(10L, new ChangePasswordRequest("OldPass1", "NewPass2"));
|
|
assertTrue(encoder.matches("NewPass2", account.getPasswordHash()));
|
|
assertEquals(3, account.getTokenVersion());
|
|
verify(repository).save(account);
|
|
verify(sessions).revokeAll(10L);
|
|
}
|
|
|
|
@Test void changePasswordDoesNotPersistWhenCurrentPasswordIsInvalid() {
|
|
assertThrows(BizException.class,
|
|
() -> service.changePassword(10L, new ChangePasswordRequest("WrongPass1", "NewPass2")));
|
|
verify(repository, never()).save(any());
|
|
verify(sessions, never()).revokeAll(any());
|
|
}
|
|
|
|
@Test void forcedPasswordChangeRequiresResetFlagAndClearsIt() {
|
|
account.setPasswordChangeRequired(true);
|
|
|
|
service.forcedChangePassword(10L, new ForcedPasswordChangeRequest("NewPass2"));
|
|
|
|
assertTrue(encoder.matches("NewPass2", account.getPasswordHash()));
|
|
assertFalse(account.getPasswordChangeRequired());
|
|
verify(repository).save(account);
|
|
verify(sessions).revokeAll(10L);
|
|
}
|
|
|
|
@Test void normalPasswordChangeIsRejectedUntilForcedChangeIsCompleted() {
|
|
account.setPasswordChangeRequired(true);
|
|
|
|
assertThrows(BizException.class,
|
|
() -> service.changePassword(10L, new ChangePasswordRequest("OldPass1", "NewPass2")));
|
|
verify(repository, never()).save(any());
|
|
}
|
|
|
|
@Test void disableAccountRevokesAllSessions() {
|
|
service.updateStatus(10L, new UpdateAccountStatusRequest("DISABLED"));
|
|
assertEquals("DISABLED", account.getAccountStatus());
|
|
verify(sessions).revokeAll(10L);
|
|
}
|
|
|
|
@Test void createPlatformAccountPersistsPlatformAppCodeAndPasswordHashOnly() {
|
|
when(repository.findById(20L)).thenReturn(null);
|
|
when(repository.existsByUsername("platform.user")).thenReturn(false);
|
|
|
|
var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1",
|
|
"平台用户", 0L, 30L, "PLATFORM"));
|
|
|
|
assertEquals("platform.user", result.username());
|
|
var captor = org.mockito.ArgumentCaptor.forClass(AuthAccount.class);
|
|
verify(repository).insert(captor.capture());
|
|
assertEquals("PLATFORM", captor.getValue().getDefaultAppCode());
|
|
assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash()));
|
|
assertNotEquals("Password1", captor.getValue().getPasswordHash());
|
|
}
|
|
|
|
@Test void usernameAvailabilityUsesTheAuthAccountRepositoryAsTheFactSource() {
|
|
when(repository.existsByUsername("13800138000")).thenReturn(true);
|
|
|
|
assertFalse(service.usernameAvailable("13800138000"));
|
|
assertTrue(service.usernameAvailable("13900139000"));
|
|
}
|
|
|
|
@Test void createDisabledAccountAndSummaryExposeLastLoginAndScope() {
|
|
when(repository.findById(20L)).thenReturn(null);
|
|
when(repository.existsByUsername("merchant.user")).thenReturn(false);
|
|
|
|
var created = service.create(new CreateAccountRequest(20L, "merchant.user", "Password1",
|
|
"企业用户", 10001L, 30L, "MERCHANT", "DISABLED"));
|
|
var captor = org.mockito.ArgumentCaptor.forClass(AuthAccount.class);
|
|
verify(repository).insert(captor.capture());
|
|
AuthAccount stored = captor.getValue();
|
|
stored.setLastLoginAt(LocalDateTime.of(2026, 6, 1, 10, 30));
|
|
when(repository.findById(20L)).thenReturn(stored);
|
|
|
|
var summary = service.summary(20L);
|
|
assertEquals("DISABLED", created.status());
|
|
assertEquals(10001L, summary.tenantId());
|
|
assertEquals(30L, summary.membershipId());
|
|
assertEquals(LocalDateTime.of(2026, 6, 1, 10, 30), summary.lastLoginAt());
|
|
}
|
|
|
|
@Test void updateScopeChangesTenantAndRevokesExistingSessions() {
|
|
account.setDefaultTenantId(10001L); account.setOperatorTenantId(10001L);
|
|
account.setDefaultMembershipId(30L); account.setDefaultAppCode("MERCHANT");
|
|
|
|
service.updateScope(10L, new UpdateAccountScopeRequest(10002L, 30L, "MERCHANT"));
|
|
|
|
assertEquals(10002L, account.getDefaultTenantId());
|
|
assertEquals(10002L, account.getOperatorTenantId());
|
|
verify(repository).save(account);
|
|
verify(sessions).revokeAll(10L);
|
|
}
|
|
|
|
@Test void createAccountRetriesIdempotentlyForSameAccountAndOwner() {
|
|
account.setUsername("platform.user"); account.setDisplayName("平台用户");
|
|
account.setDefaultTenantId(0L); account.setOperatorTenantId(0L);
|
|
account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
|
|
|
|
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
|
|
"平台用户", 0L, 30L, "PLATFORM"));
|
|
|
|
assertEquals(10L, result.accountId());
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@Test void synchronizeDisplayNameRevokesExistingSessions() {
|
|
account.setDisplayName("旧名称");
|
|
|
|
service.updateProfile(10L, new UpdateAccountProfileRequest("新名称"));
|
|
|
|
assertEquals("新名称", account.getDisplayName());
|
|
verify(repository).save(account);
|
|
verify(sessions).revokeAll(10L);
|
|
}
|
|
|
|
@Test void rbacChangeRevokesAccessAndRefreshSessionsAndBumpsTokenVersions() {
|
|
AuthAccount second = new AuthAccount();
|
|
second.setId(11L); second.setTokenVersion(7); second.setAccountStatus("ACTIVE");
|
|
when(repository.findById(11L)).thenReturn(second);
|
|
|
|
service.revokeSessions(Set.of(10L, 11L));
|
|
|
|
assertEquals(3, account.getTokenVersion());
|
|
assertEquals(8, second.getTokenVersion());
|
|
verify(repository).save(account);
|
|
verify(repository).save(second);
|
|
verify(sessions).revokeAll(10L);
|
|
verify(sessions).revokeAll(11L);
|
|
}
|
|
|
|
@Test void archiveTenantDisablesEveryMerchantAccountAndRevokesSessions() {
|
|
AuthAccount owner = merchantAccount(20L, 10001L, "ACTIVE", 3);
|
|
AuthAccount member = merchantAccount(21L, 10001L, "DISABLED", null);
|
|
when(repository.findMerchantByTenant(10001L)).thenReturn(List.of(owner, member));
|
|
|
|
service.archiveTenant(10001L);
|
|
|
|
assertEquals("DISABLED", owner.getAccountStatus());
|
|
assertEquals(4, owner.getTokenVersion());
|
|
assertEquals("DISABLED", member.getAccountStatus());
|
|
assertEquals(1, member.getTokenVersion());
|
|
verify(repository).save(owner);
|
|
verify(repository).save(member);
|
|
verify(sessions).revokeAll(20L);
|
|
verify(sessions).revokeAll(21L);
|
|
}
|
|
|
|
@Test void archiveTenantIsSuccessfulWhenCustomerHasNoAccounts() {
|
|
when(repository.findMerchantByTenant(10001L)).thenReturn(List.of());
|
|
|
|
service.archiveTenant(10001L);
|
|
|
|
verify(repository, never()).save(any());
|
|
verify(sessions, never()).revokeAll(any());
|
|
}
|
|
|
|
private AuthAccount merchantAccount(long id, long tenantId, String status, Integer tokenVersion) {
|
|
AuthAccount value = new AuthAccount();
|
|
value.setId(id);
|
|
value.setDefaultTenantId(tenantId);
|
|
value.setDefaultAppCode("MERCHANT");
|
|
value.setAccountStatus(status);
|
|
value.setTokenVersion(tokenVersion);
|
|
return value;
|
|
}
|
|
@Configuration
|
|
static class TestConfig {
|
|
@Bean
|
|
PasswordEncoder passwordEncoder() {
|
|
return new BCryptPasswordEncoder(4);
|
|
}
|
|
}
|
|
}
|