264 lines
12 KiB
Java
264 lines
12 KiB
Java
package com.mzh.library.service;
|
|
|
|
import com.mzh.library.dao.SystemLogDao;
|
|
import com.mzh.library.dao.UserAccountDao;
|
|
import com.mzh.library.entity.AuthenticatedUser;
|
|
import com.mzh.library.entity.Permission;
|
|
import com.mzh.library.entity.Role;
|
|
import com.mzh.library.entity.SystemLog;
|
|
import com.mzh.library.entity.SystemLogSearchCriteria;
|
|
import com.mzh.library.entity.User;
|
|
import com.mzh.library.entity.UserSearchCriteria;
|
|
import com.mzh.library.exception.DaoException;
|
|
import com.mzh.library.service.impl.UserAccountServiceImpl;
|
|
import com.mzh.library.util.PasswordHasher;
|
|
|
|
import java.sql.Connection;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.EnumSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
|
|
public final class UserAccountServiceCheck {
|
|
private static final String DENIED_MESSAGE = "您无权管理用户。";
|
|
private static final String UNAVAILABLE_MESSAGE =
|
|
"用户管理服务暂时不可用,请稍后重试。";
|
|
|
|
private UserAccountServiceCheck() {
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
Logger.getLogger(UserAccountServiceImpl.class.getName()).setLevel(Level.OFF);
|
|
|
|
InMemoryUserAccountDao userDao = new InMemoryUserAccountDao();
|
|
InMemorySystemLogDao logDao = new InMemorySystemLogDao();
|
|
User adminAccount = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, true, "admin-password");
|
|
User staffAccount = user(2L, "staff", "Library Staff", Role.LIBRARIAN, true, "staff-password");
|
|
userDao.put(adminAccount);
|
|
userDao.put(staffAccount);
|
|
|
|
UserAccountService service = new UserAccountServiceImpl(userDao, logDao, new PermissionPolicy(),
|
|
new UserAccountServiceImpl.DirectTransactionExecutor());
|
|
AuthenticatedUser admin = actor(1L, Role.ADMINISTRATOR);
|
|
AuthenticatedUser reader = actor(3L, Role.READER);
|
|
|
|
requireMessage(service.searchUsers(reader, new UserSearchCriteria()), DENIED_MESSAGE);
|
|
|
|
User duplicate = user(0L, " admin ", "Duplicate Admin", Role.ADMINISTRATOR, true, "unused");
|
|
ServiceResult<Long> duplicateResult = service.createUser(admin, duplicate, "new-password", "127.0.0.1");
|
|
require(!duplicateResult.isSuccessful(), "duplicate username should be rejected");
|
|
require(duplicateResult.getErrors().containsKey("username"), "duplicate username should target username");
|
|
|
|
User invalid = new User();
|
|
ServiceResult<Long> invalidResult = service.createUser(admin, invalid, "", "127.0.0.1");
|
|
require(!invalidResult.isSuccessful(), "invalid user should be rejected");
|
|
require(invalidResult.getErrors().containsKey("username"), "missing username should be reported");
|
|
require(invalidResult.getErrors().containsKey("displayName"), "missing display name should be reported");
|
|
require(invalidResult.getErrors().containsKey("password"), "missing password should be reported");
|
|
|
|
User newReader = new User();
|
|
newReader.setUsername(" new.reader ");
|
|
newReader.setDisplayName("New Reader");
|
|
newReader.setRole(Role.READER);
|
|
newReader.setActive(true);
|
|
ServiceResult<Long> created = service.createUser(admin, newReader, "reader-password", "127.0.0.1");
|
|
require(created.isSuccessful(), "administrator should create user accounts");
|
|
User storedReader = userDao.findById(created.getData()).orElseThrow(AssertionError::new);
|
|
require("new.reader".equals(storedReader.getUsername()), "username should be trimmed like login");
|
|
require(!"reader-password".equals(storedReader.getPasswordHash()), "password should not be stored in plain text");
|
|
require(PasswordHasher.verify("reader-password", storedReader.getPasswordHash()), "stored password should verify");
|
|
require(logDao.logs.size() == 1, "user creation should write one system log");
|
|
require("user.create".equals(logDao.logs.get(0).getOperationType()), "creation log should use user.create");
|
|
|
|
User selfRoleChange = user(1L, "admin", "System Administrator", Role.LIBRARIAN, true, "ignored");
|
|
ServiceResult<Void> selfRoleResult = service.updateUser(admin, selfRoleChange, "", "127.0.0.1");
|
|
require(!selfRoleResult.isSuccessful(), "current administrator role change should be blocked");
|
|
require(selfRoleResult.getErrors().containsKey("role"), "self role change should target role");
|
|
|
|
User selfDeactivate = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, false, "ignored");
|
|
ServiceResult<Void> selfDeactivateResult = service.updateUser(admin, selfDeactivate, "", "127.0.0.1");
|
|
require(!selfDeactivateResult.isSuccessful(), "current administrator deactivation should be blocked");
|
|
require(selfDeactivateResult.getErrors().containsKey("active"), "self deactivation should target active state");
|
|
|
|
String originalStaffHash = staffAccount.getPasswordHash();
|
|
User updatedStaff = user(2L, "staff", "Lead Librarian", Role.LIBRARIAN, true, "ignored");
|
|
ServiceResult<Void> updated = service.updateUser(admin, updatedStaff, "", "127.0.0.1");
|
|
require(updated.isSuccessful(), "administrator should update user accounts");
|
|
require(originalStaffHash.equals(userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
|
|
"blank update password should preserve existing hash");
|
|
|
|
ServiceResult<Void> reset = service.updateUser(admin, updatedStaff, "replacement-password", "127.0.0.1");
|
|
require(reset.isSuccessful(), "administrator should reset passwords");
|
|
require(PasswordHasher.verify("replacement-password",
|
|
userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
|
|
"replacement password should be hashed");
|
|
|
|
ServiceResult<Void> deactivated = service.deactivateUser(admin, 2L, "127.0.0.1");
|
|
require(deactivated.isSuccessful(), "administrator should deactivate other accounts");
|
|
require(!userDao.findById(2L).orElseThrow(AssertionError::new).isActive(),
|
|
"deactivate action should mark account inactive");
|
|
require(logDao.logs.stream().anyMatch(log -> "user.deactivate".equals(log.getOperationType())),
|
|
"deactivate should write a system log");
|
|
|
|
UserAccountService failingService = new UserAccountServiceImpl(new FailingUserAccountDao(), logDao,
|
|
new PermissionPolicy(), new UserAccountServiceImpl.DirectTransactionExecutor());
|
|
requireMessage(failingService.searchUsers(admin, new UserSearchCriteria()), UNAVAILABLE_MESSAGE);
|
|
}
|
|
|
|
private static User user(long id, String username, String displayName, Role role, boolean active, String password) {
|
|
User user = new User();
|
|
user.setId(id);
|
|
user.setUsername(username);
|
|
user.setDisplayName(displayName);
|
|
user.setRole(role);
|
|
user.setActive(active);
|
|
user.setPasswordHash(PasswordHasher.hash(password));
|
|
return user;
|
|
}
|
|
|
|
private static AuthenticatedUser actor(long id, Role role) {
|
|
return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
|
|
role == Role.ADMINISTRATOR
|
|
? EnumSet.allOf(Permission.class)
|
|
: EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS));
|
|
}
|
|
|
|
private static void requireMessage(ServiceResult<?> result, String message) {
|
|
require(!result.isSuccessful(), "result should be a failure");
|
|
require(message.equals(result.getMessage()), "expected message: " + message);
|
|
}
|
|
|
|
private static void require(boolean condition, String message) {
|
|
if (!condition) {
|
|
throw new AssertionError(message);
|
|
}
|
|
}
|
|
|
|
private static final class InMemoryUserAccountDao implements UserAccountDao {
|
|
private final Map<Long, User> users = new LinkedHashMap<>();
|
|
private long nextId = 10L;
|
|
|
|
private void put(User user) {
|
|
users.put(user.getId(), copy(user));
|
|
}
|
|
|
|
@Override
|
|
public List<User> search(UserSearchCriteria criteria) {
|
|
return users.values().stream()
|
|
.filter(user -> criteria.getRoleCode().isEmpty()
|
|
|| user.getRole().getCode().equals(criteria.getRoleCode()))
|
|
.filter(user -> criteria.getActiveValue() == null
|
|
|| user.isActive() == criteria.getActiveValue())
|
|
.map(this::copy)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
@Override
|
|
public Optional<User> findById(long id) {
|
|
return Optional.ofNullable(users.get(id)).map(this::copy);
|
|
}
|
|
|
|
@Override
|
|
public Optional<User> findByUsername(String username) {
|
|
return users.values().stream()
|
|
.filter(user -> user.getUsername().equals(username))
|
|
.findFirst()
|
|
.map(this::copy);
|
|
}
|
|
|
|
@Override
|
|
public long create(Connection connection, User user) {
|
|
long id = nextId++;
|
|
User stored = copy(user);
|
|
stored.setId(id);
|
|
users.put(id, stored);
|
|
return id;
|
|
}
|
|
|
|
@Override
|
|
public boolean update(Connection connection, User user, boolean updatePassword) {
|
|
User existing = users.get(user.getId());
|
|
if (existing == null) {
|
|
return false;
|
|
}
|
|
existing.setDisplayName(user.getDisplayName());
|
|
existing.setRole(user.getRole());
|
|
existing.setActive(user.isActive());
|
|
if (updatePassword) {
|
|
existing.setPasswordHash(user.getPasswordHash());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private User copy(User source) {
|
|
User copy = new User();
|
|
copy.setId(source.getId());
|
|
copy.setUsername(source.getUsername());
|
|
copy.setDisplayName(source.getDisplayName());
|
|
copy.setRole(source.getRole());
|
|
copy.setActive(source.isActive());
|
|
copy.setPasswordHash(source.getPasswordHash());
|
|
copy.setCreatedAt(source.getCreatedAt());
|
|
copy.setUpdatedAt(source.getUpdatedAt());
|
|
return copy;
|
|
}
|
|
}
|
|
|
|
private static final class InMemorySystemLogDao implements SystemLogDao {
|
|
private final List<SystemLog> logs = new ArrayList<>();
|
|
|
|
@Override
|
|
public List<SystemLog> search(SystemLogSearchCriteria criteria) {
|
|
return new ArrayList<>(logs);
|
|
}
|
|
|
|
@Override
|
|
public List<String> findOperationTypes() {
|
|
return logs.stream()
|
|
.map(SystemLog::getOperationType)
|
|
.distinct()
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
@Override
|
|
public long create(Connection connection, SystemLog log) {
|
|
log.setId(logs.size() + 1L);
|
|
logs.add(log);
|
|
return log.getId();
|
|
}
|
|
}
|
|
|
|
private static final class FailingUserAccountDao implements UserAccountDao {
|
|
@Override
|
|
public List<User> search(UserSearchCriteria criteria) {
|
|
throw new DaoException("Simulated user search failure", null);
|
|
}
|
|
|
|
@Override
|
|
public Optional<User> findById(long id) {
|
|
throw new DaoException("Simulated user lookup failure", null);
|
|
}
|
|
|
|
@Override
|
|
public Optional<User> findByUsername(String username) {
|
|
throw new DaoException("Simulated username lookup failure", null);
|
|
}
|
|
|
|
@Override
|
|
public long create(Connection connection, User user) {
|
|
throw new DaoException("Simulated user create failure", null);
|
|
}
|
|
|
|
@Override
|
|
public boolean update(Connection connection, User user, boolean updatePassword) {
|
|
throw new DaoException("Simulated user update failure", null);
|
|
}
|
|
}
|
|
}
|