添加账号修改,后台管理
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.files.StoredFile;
|
||||
import com.yoyuzh.files.StoredFileRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:admin_api_test;MODE=MySQL;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.driver-class-name=org.h2.Driver",
|
||||
"spring.datasource.username=sa",
|
||||
"spring.datasource.password=",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.admin.usernames=admin",
|
||||
"app.storage.root-dir=./target/test-storage-admin",
|
||||
"app.cqu.require-login=true",
|
||||
"app.cqu.mock-enabled=false"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class AdminControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
private User portalUser;
|
||||
private User secondaryUser;
|
||||
private StoredFile storedFile;
|
||||
private StoredFile secondaryFile;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
storedFileRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
|
||||
portalUser = new User();
|
||||
portalUser.setUsername("alice");
|
||||
portalUser.setEmail("alice@example.com");
|
||||
portalUser.setPasswordHash("encoded-password");
|
||||
portalUser.setCreatedAt(LocalDateTime.now());
|
||||
portalUser.setLastSchoolStudentId("20230001");
|
||||
portalUser.setLastSchoolSemester("2025-2026-1");
|
||||
portalUser = userRepository.save(portalUser);
|
||||
|
||||
secondaryUser = new User();
|
||||
secondaryUser.setUsername("bob");
|
||||
secondaryUser.setEmail("bob@example.com");
|
||||
secondaryUser.setPasswordHash("encoded-password");
|
||||
secondaryUser.setCreatedAt(LocalDateTime.now().minusDays(1));
|
||||
secondaryUser = userRepository.save(secondaryUser);
|
||||
|
||||
storedFile = new StoredFile();
|
||||
storedFile.setUser(portalUser);
|
||||
storedFile.setFilename("report.pdf");
|
||||
storedFile.setPath("/");
|
||||
storedFile.setStorageName("report.pdf");
|
||||
storedFile.setContentType("application/pdf");
|
||||
storedFile.setSize(1024L);
|
||||
storedFile.setDirectory(false);
|
||||
storedFile.setCreatedAt(LocalDateTime.now());
|
||||
storedFile = storedFileRepository.save(storedFile);
|
||||
|
||||
secondaryFile = new StoredFile();
|
||||
secondaryFile.setUser(secondaryUser);
|
||||
secondaryFile.setFilename("notes.txt");
|
||||
secondaryFile.setPath("/docs");
|
||||
secondaryFile.setStorageName("notes.txt");
|
||||
secondaryFile.setContentType("text/plain");
|
||||
secondaryFile.setSize(256L);
|
||||
secondaryFile.setDirectory(false);
|
||||
secondaryFile.setCreatedAt(LocalDateTime.now().minusHours(2));
|
||||
secondaryFile = storedFileRepository.save(secondaryFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListUsersAndSummary() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/users?page=0&size=10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items[0].username").value("alice"))
|
||||
.andExpect(jsonPath("$.data.items[0].lastSchoolStudentId").value("20230001"))
|
||||
.andExpect(jsonPath("$.data.items[0].role").value("USER"))
|
||||
.andExpect(jsonPath("$.data.items[0].banned").value(false));
|
||||
|
||||
mockMvc.perform(get("/api/admin/summary"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.totalUsers").value(2))
|
||||
.andExpect(jsonPath("$.data.totalFiles").value(2))
|
||||
.andExpect(jsonPath("$.data.usersWithSchoolCache").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldSupportUserSearchPasswordAndStatusManagement() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/users?page=0&size=10&query=ali"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].username").value("alice"));
|
||||
|
||||
mockMvc.perform(patch("/api/admin/users/{userId}/role", portalUser.getId())
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{"role":"ADMIN"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.role").value("ADMIN"));
|
||||
|
||||
mockMvc.perform(patch("/api/admin/users/{userId}/status", portalUser.getId())
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{"banned":true}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.banned").value(true));
|
||||
|
||||
mockMvc.perform(put("/api/admin/users/{userId}/password", portalUser.getId())
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{"newPassword":"AdminSetPass1!"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.id").value(portalUser.getId()));
|
||||
|
||||
mockMvc.perform(post("/api/admin/users/{userId}/password/reset", secondaryUser.getId()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.temporaryPassword").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListAndDeleteFiles() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/files?page=0&size=10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("report.pdf"))
|
||||
.andExpect(jsonPath("$.data.items[0].ownerUsername").value("alice"));
|
||||
|
||||
mockMvc.perform(get("/api/admin/files?page=0&size=10&query=report&ownerQuery=ali"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("report.pdf"));
|
||||
|
||||
mockMvc.perform(delete("/api/admin/files/{fileId}", storedFile.getId()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "portal-user")
|
||||
void shouldRejectNonAdminUser() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/users?page=0&size=10"))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.msg").value("没有权限访问该资源"));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,14 @@ package com.yoyuzh.auth;
|
||||
import com.yoyuzh.auth.dto.AuthResponse;
|
||||
import com.yoyuzh.auth.dto.LoginRequest;
|
||||
import com.yoyuzh.auth.dto.RegisterRequest;
|
||||
import com.yoyuzh.auth.dto.UpdateUserAvatarRequest;
|
||||
import com.yoyuzh.auth.dto.UpdateUserPasswordRequest;
|
||||
import com.yoyuzh.auth.dto.UpdateUserProfileRequest;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.files.FileService;
|
||||
import com.yoyuzh.files.InitiateUploadResponse;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import com.yoyuzh.files.storage.PreparedUpload;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -12,6 +18,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@@ -21,6 +28,8 @@ import java.util.Optional;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -45,6 +54,9 @@ class AuthServiceTest {
|
||||
@Mock
|
||||
private FileService fileService;
|
||||
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
@InjectMocks
|
||||
private AuthService authService;
|
||||
|
||||
@@ -137,6 +149,17 @@ class AuthServiceTest {
|
||||
.hasMessageContaining("用户名或密码错误");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBannedUserLogin() {
|
||||
LoginRequest request = new LoginRequest("alice", "plain-password");
|
||||
when(authenticationManager.authenticate(any()))
|
||||
.thenThrow(new DisabledException("disabled"));
|
||||
|
||||
assertThatThrownBy(() -> authService.login(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("账号已被封禁");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateDefaultDirectoriesForDevLoginUser() {
|
||||
when(userRepository.findByUsername("demo")).thenReturn(Optional.empty());
|
||||
@@ -157,4 +180,128 @@ class AuthServiceTest {
|
||||
assertThat(response.refreshToken()).isEqualTo("refresh-token");
|
||||
verify(fileService).ensureDefaultDirectories(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateCurrentUserProfile() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setDisplayName("Alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setBio("old bio");
|
||||
user.setPreferredLanguage("zh-CN");
|
||||
user.setRole(UserRole.USER);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
UpdateUserProfileRequest request = new UpdateUserProfileRequest(
|
||||
"Alicia",
|
||||
"newalice@example.com",
|
||||
"new bio",
|
||||
"en-US"
|
||||
);
|
||||
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
when(userRepository.existsByEmail("newalice@example.com")).thenReturn(false);
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
var response = authService.updateProfile("alice", request);
|
||||
|
||||
assertThat(response.displayName()).isEqualTo("Alicia");
|
||||
assertThat(response.email()).isEqualTo("newalice@example.com");
|
||||
assertThat(response.bio()).isEqualTo("new bio");
|
||||
assertThat(response.preferredLanguage()).isEqualTo("en-US");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldChangePasswordAndIssueFreshTokens() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setDisplayName("Alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setPreferredLanguage("zh-CN");
|
||||
user.setRole(UserRole.USER);
|
||||
user.setPasswordHash("encoded-old");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
UpdateUserPasswordRequest request = new UpdateUserPasswordRequest("OldPass1!", "NewPass1!A");
|
||||
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
when(passwordEncoder.matches("OldPass1!", "encoded-old")).thenReturn(true);
|
||||
when(passwordEncoder.encode("NewPass1!A")).thenReturn("encoded-new");
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
when(jwtTokenProvider.generateAccessToken(1L, "alice")).thenReturn("new-access");
|
||||
when(refreshTokenService.issueRefreshToken(user)).thenReturn("new-refresh");
|
||||
|
||||
AuthResponse response = authService.changePassword("alice", request);
|
||||
|
||||
assertThat(response.accessToken()).isEqualTo("new-access");
|
||||
assertThat(response.refreshToken()).isEqualTo("new-refresh");
|
||||
verify(refreshTokenService).revokeAllForUser(1L);
|
||||
verify(passwordEncoder).encode("NewPass1!A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordChangeWhenCurrentPasswordIsWrong() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setPasswordHash("encoded-old");
|
||||
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
when(passwordEncoder.matches("WrongPass1!", "encoded-old")).thenReturn(false);
|
||||
|
||||
assertThatThrownBy(() -> authService.changePassword("alice", new UpdateUserPasswordRequest("WrongPass1!", "NewPass1!A")))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("当前密码错误");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInitiateAvatarUploadThroughStorage() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
when(fileContentStorage.prepareUpload(eq(1L), eq("/.avatar"), any(), eq("image/png"), eq(2048L)))
|
||||
.thenReturn(new PreparedUpload(true, "https://upload.example.com/avatar", "PUT", java.util.Map.of("Content-Type", "image/png"), "avatar-generated.png"));
|
||||
|
||||
InitiateUploadResponse response = authService.initiateAvatarUpload(
|
||||
"alice",
|
||||
new UpdateUserAvatarRequest("face.png", "image/png", 2048L, "avatar-generated.png")
|
||||
);
|
||||
|
||||
assertThat(response.direct()).isTrue();
|
||||
assertThat(response.uploadUrl()).isEqualTo("https://upload.example.com/avatar");
|
||||
assertThat(response.storageName()).endsWith(".png");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteAvatarUploadAndReplacePreviousAvatar() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setDisplayName("Alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setPreferredLanguage("zh-CN");
|
||||
user.setRole(UserRole.USER);
|
||||
user.setAvatarStorageName("old-avatar.png");
|
||||
user.setAvatarContentType("image/png");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
||||
when(fileContentStorage.createDownloadUrl(anyLong(), eq("/.avatar"), eq("new-avatar.webp"), any()))
|
||||
.thenReturn("https://cdn.example.com/avatar.webp");
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
var response = authService.completeAvatarUpload(
|
||||
"alice",
|
||||
new UpdateUserAvatarRequest("face.webp", "image/webp", 4096L, "new-avatar.webp")
|
||||
);
|
||||
|
||||
verify(fileContentStorage).completeUpload(1L, "/.avatar", "new-avatar.webp", "image/webp", 4096L);
|
||||
verify(fileContentStorage).deleteFile(1L, "/.avatar", "old-avatar.png");
|
||||
assertThat(response.avatarUrl()).isEqualTo("https://cdn.example.com/avatar.webp");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user