添加新测试
This commit is contained in:
252
backend/src/test/java/com/yoyuzh/admin/AdminServiceTest.java
Normal file
252
backend/src/test/java/com/yoyuzh/admin/AdminServiceTest.java
Normal file
@@ -0,0 +1,252 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.yoyuzh.auth.PasswordPolicy;
|
||||
import com.yoyuzh.auth.RegistrationInviteService;
|
||||
import com.yoyuzh.auth.RefreshTokenService;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.auth.UserRole;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.files.FileService;
|
||||
import com.yoyuzh.files.StoredFile;
|
||||
import com.yoyuzh.files.StoredFileRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
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.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminServiceTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
@Mock
|
||||
private FileService fileService;
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Mock
|
||||
private RefreshTokenService refreshTokenService;
|
||||
@Mock
|
||||
private RegistrationInviteService registrationInviteService;
|
||||
|
||||
private AdminService adminService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
adminService = new AdminService(
|
||||
userRepository, storedFileRepository, fileService,
|
||||
passwordEncoder, refreshTokenService, registrationInviteService);
|
||||
}
|
||||
|
||||
// --- getSummary ---
|
||||
|
||||
@Test
|
||||
void shouldReturnSummaryWithCountsAndInviteCode() {
|
||||
when(userRepository.count()).thenReturn(5L);
|
||||
when(storedFileRepository.count()).thenReturn(42L);
|
||||
when(registrationInviteService.getCurrentInviteCode()).thenReturn("INV-001");
|
||||
|
||||
AdminSummaryResponse summary = adminService.getSummary();
|
||||
|
||||
assertThat(summary.totalUsers()).isEqualTo(5L);
|
||||
assertThat(summary.totalFiles()).isEqualTo(42L);
|
||||
assertThat(summary.inviteCode()).isEqualTo("INV-001");
|
||||
}
|
||||
|
||||
// --- listUsers ---
|
||||
|
||||
@Test
|
||||
void shouldListUsersWithPagination() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
when(userRepository.searchByUsernameOrEmail(anyString(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of(user)));
|
||||
|
||||
PageResponse<AdminUserResponse> response = adminService.listUsers(0, 10, "alice");
|
||||
|
||||
assertThat(response.items()).hasSize(1);
|
||||
assertThat(response.items().get(0).username()).isEqualTo("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeNullQueryToEmptyStringWhenListingUsers() {
|
||||
when(userRepository.searchByUsernameOrEmail(anyString(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
adminService.listUsers(0, 10, null);
|
||||
|
||||
verify(userRepository).searchByUsernameOrEmail(eq(""), any());
|
||||
}
|
||||
|
||||
// --- listFiles ---
|
||||
|
||||
@Test
|
||||
void shouldListFilesWithPagination() {
|
||||
User owner = createUser(1L, "alice", "alice@example.com");
|
||||
StoredFile file = createFile(10L, owner, "/docs", "report.pdf");
|
||||
when(storedFileRepository.searchAdminFiles(anyString(), anyString(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of(file)));
|
||||
|
||||
PageResponse<AdminFileResponse> response = adminService.listFiles(0, 10, "report", "alice");
|
||||
|
||||
assertThat(response.items()).hasSize(1);
|
||||
assertThat(response.items().get(0).filename()).isEqualTo("report.pdf");
|
||||
assertThat(response.items().get(0).ownerUsername()).isEqualTo("alice");
|
||||
}
|
||||
|
||||
// --- deleteFile ---
|
||||
|
||||
@Test
|
||||
void shouldDeleteFileByDelegatingToFileService() {
|
||||
User owner = createUser(1L, "alice", "alice@example.com");
|
||||
StoredFile file = createFile(10L, owner, "/docs", "report.pdf");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
|
||||
|
||||
adminService.deleteFile(10L);
|
||||
|
||||
verify(fileService).delete(owner, 10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenDeletingNonExistentFile() {
|
||||
when(storedFileRepository.findById(99L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> adminService.deleteFile(99L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("文件不存在");
|
||||
}
|
||||
|
||||
// --- updateUserRole ---
|
||||
|
||||
@Test
|
||||
void shouldUpdateUserRole() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
AdminUserResponse response = adminService.updateUserRole(1L, UserRole.ADMIN);
|
||||
|
||||
assertThat(user.getRole()).isEqualTo(UserRole.ADMIN);
|
||||
verify(userRepository).save(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWhenUpdatingRoleForNonExistentUser() {
|
||||
when(userRepository.findById(99L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> adminService.updateUserRole(99L, UserRole.ADMIN))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("用户不存在");
|
||||
}
|
||||
|
||||
// --- updateUserBanned ---
|
||||
|
||||
@Test
|
||||
void shouldBanUserAndRevokeTokens() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
adminService.updateUserBanned(1L, true);
|
||||
|
||||
assertThat(user.isBanned()).isTrue();
|
||||
verify(refreshTokenService).revokeAllForUser(1L);
|
||||
verify(userRepository).save(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnbanUserAndRevokeExistingTokens() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
user.setBanned(true);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
adminService.updateUserBanned(1L, false);
|
||||
|
||||
assertThat(user.isBanned()).isFalse();
|
||||
verify(refreshTokenService).revokeAllForUser(1L);
|
||||
}
|
||||
|
||||
// --- updateUserPassword ---
|
||||
|
||||
@Test
|
||||
void shouldUpdateUserPasswordAndRevokeTokens() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
||||
when(passwordEncoder.encode("NewStr0ng!Pass")).thenReturn("hashed");
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
adminService.updateUserPassword(1L, "NewStr0ng!Pass");
|
||||
|
||||
assertThat(user.getPasswordHash()).isEqualTo("hashed");
|
||||
verify(refreshTokenService).revokeAllForUser(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWeakPasswordWhenUpdating() {
|
||||
assertThatThrownBy(() -> adminService.updateUserPassword(1L, "weakpass"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("密码至少10位");
|
||||
verify(userRepository, never()).findById(any());
|
||||
}
|
||||
|
||||
// --- resetUserPassword ---
|
||||
|
||||
@Test
|
||||
void shouldResetUserPasswordAndReturnTemporaryPassword() {
|
||||
User user = createUser(1L, "alice", "alice@example.com");
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
|
||||
when(passwordEncoder.encode(anyString())).thenReturn("hashed");
|
||||
when(userRepository.save(user)).thenReturn(user);
|
||||
|
||||
AdminPasswordResetResponse response = adminService.resetUserPassword(1L);
|
||||
|
||||
assertThat(response.temporaryPassword()).isNotBlank();
|
||||
assertThat(PasswordPolicy.isStrong(response.temporaryPassword())).isTrue();
|
||||
verify(refreshTokenService).revokeAllForUser(1L);
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private User createUser(Long id, String username, String email) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash("hashed");
|
||||
user.setRole(UserRole.USER);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
return user;
|
||||
}
|
||||
|
||||
private StoredFile createFile(Long id, User owner, String path, String filename) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(id);
|
||||
file.setUser(owner);
|
||||
file.setPath(path);
|
||||
file.setFilename(filename);
|
||||
file.setSize(1024L);
|
||||
file.setDirectory(false);
|
||||
file.setCreatedAt(LocalDateTime.now());
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.yoyuzh.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PasswordPolicyTest {
|
||||
|
||||
@Test
|
||||
void shouldRejectNullPassword() {
|
||||
assertThat(PasswordPolicy.isStrong(null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordShorterThanTenCharacters() {
|
||||
assertThat(PasswordPolicy.isStrong("Abc1!defg")).isFalse(); // 9 chars
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptPasswordWithExactlyTenCharacters() {
|
||||
assertThat(PasswordPolicy.isStrong("Abcdefg1!x")).isTrue(); // 10 chars
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordMissingUppercase() {
|
||||
assertThat(PasswordPolicy.isStrong("abcdefg1!x")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordMissingLowercase() {
|
||||
assertThat(PasswordPolicy.isStrong("ABCDEFG1!X")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordMissingDigit() {
|
||||
assertThat(PasswordPolicy.isStrong("Abcdefgh!x")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPasswordMissingSpecialCharacter() {
|
||||
assertThat(PasswordPolicy.isStrong("Abcdefg12x")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptStrongPasswordWithAllRequirements() {
|
||||
assertThat(PasswordPolicy.isStrong("MyP@ssw0rd!")).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "short", "nouppercase1!", "NOLOWERCASE1!", "NoSpecialChar1", "NoDigit!AbcXyz"})
|
||||
void shouldRejectWeakPasswords(String password) {
|
||||
assertThat(PasswordPolicy.isStrong(password)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptLongPasswordWithAllRequirements() {
|
||||
assertThat(PasswordPolicy.isStrong("MyV3ryStr0ng&SecureP@ssword2024!")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectEmptyPassword() {
|
||||
assertThat(PasswordPolicy.isStrong("")).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.yoyuzh.common;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class GlobalExceptionHandlerTest {
|
||||
|
||||
private GlobalExceptionHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new GlobalExceptionHandler();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn400ForUnknownBusinessException() {
|
||||
BusinessException ex = new BusinessException(ErrorCode.UNKNOWN, "操作失败");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleBusinessException(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(response.getBody().msg()).isEqualTo("操作失败");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn401ForNotLoggedInException() {
|
||||
BusinessException ex = new BusinessException(ErrorCode.NOT_LOGGED_IN, "未登录");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleBusinessException(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn403ForPermissionDeniedException() {
|
||||
BusinessException ex = new BusinessException(ErrorCode.PERMISSION_DENIED, "无权限");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleBusinessException(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn404ForFileNotFoundException() {
|
||||
BusinessException ex = new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleBusinessException(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn400WithFirstValidationMessageForMethodArgumentNotValidException() throws Exception {
|
||||
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(new Object(), "target");
|
||||
bindingResult.addError(new FieldError("target", "username", "用户名不能为空"));
|
||||
bindingResult.addError(new FieldError("target", "email", "邮箱格式不正确"));
|
||||
MethodArgumentNotValidException ex = new MethodArgumentNotValidException(null, bindingResult);
|
||||
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleValidationException(ex);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(response.getBody().msg()).isEqualTo("用户名不能为空");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn400WithDefaultMessageWhenNoValidationMessages() throws Exception {
|
||||
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(new Object(), "target");
|
||||
MethodArgumentNotValidException ex = new MethodArgumentNotValidException(null, bindingResult);
|
||||
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleValidationException(ex);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(response.getBody().msg()).isEqualTo("请求参数不合法");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn400ForConstraintViolationException() {
|
||||
ConstraintViolation<?> violation = mock(ConstraintViolation.class);
|
||||
when(violation.getMessage()).thenReturn("密码不能为空");
|
||||
ConstraintViolationException ex = new ConstraintViolationException(Set.of(violation));
|
||||
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleValidationException(ex);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(response.getBody().msg()).isEqualTo("密码不能为空");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn403ForAccessDeniedException() {
|
||||
AccessDeniedException ex = new AccessDeniedException("forbidden");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleAccessDenied(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
assertThat(response.getBody().msg()).isEqualTo("没有权限访问该资源");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn401ForBadCredentialsException() {
|
||||
BadCredentialsException ex = new BadCredentialsException("bad credentials");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleBadCredentials(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
assertThat(response.getBody().msg()).isEqualTo("用户名或密码错误");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn500ForUnhandledException() {
|
||||
RuntimeException ex = new RuntimeException("unexpected");
|
||||
ResponseEntity<ApiResponse<Void>> response = handler.handleUnknown(ex);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
assertThat(response.getBody().msg()).isEqualTo("服务器内部错误");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.yoyuzh.auth.CustomUserDetailsService;
|
||||
import com.yoyuzh.auth.JwtTokenProvider;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.ErrorCode;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtAuthenticationFilterTest {
|
||||
|
||||
@Mock
|
||||
private JwtTokenProvider jwtTokenProvider;
|
||||
@Mock
|
||||
private CustomUserDetailsService userDetailsService;
|
||||
@Mock
|
||||
private FilterChain filterChain;
|
||||
|
||||
private JwtAuthenticationFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService);
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughRequestWithNoAuthorizationHeader() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
verify(jwtTokenProvider, never()).validateToken(any());
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughRequestWithNonBearerAuthorizationHeader() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Basic dXNlcjpwYXNz");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
verify(jwtTokenProvider, never()).validateToken(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughRequestWithInvalidToken() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer invalid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
when(jwtTokenProvider.validateToken("invalid-token")).thenReturn(false);
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughWhenUserNotFound() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer valid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
when(jwtTokenProvider.validateToken("valid-token")).thenReturn(true);
|
||||
when(jwtTokenProvider.getUsername("valid-token")).thenReturn("alice");
|
||||
when(userDetailsService.loadDomainUser("alice"))
|
||||
.thenThrow(new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户不存在"));
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughWhenSessionIdDoesNotMatch() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer valid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
User domainUser = createDomainUser("alice", "session-1");
|
||||
when(jwtTokenProvider.validateToken("valid-token")).thenReturn(true);
|
||||
when(jwtTokenProvider.getUsername("valid-token")).thenReturn("alice");
|
||||
when(userDetailsService.loadDomainUser("alice")).thenReturn(domainUser);
|
||||
when(jwtTokenProvider.hasMatchingSession("valid-token", "session-1")).thenReturn(false);
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassThroughWhenUserIsDisabled() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer valid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
User domainUser = createDomainUser("alice", "session-1");
|
||||
UserDetails disabledUserDetails = org.springframework.security.core.userdetails.User.builder()
|
||||
.username("alice")
|
||||
.password("hashed")
|
||||
.disabled(true)
|
||||
.authorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))
|
||||
.build();
|
||||
when(jwtTokenProvider.validateToken("valid-token")).thenReturn(true);
|
||||
when(jwtTokenProvider.getUsername("valid-token")).thenReturn("alice");
|
||||
when(userDetailsService.loadDomainUser("alice")).thenReturn(domainUser);
|
||||
when(jwtTokenProvider.hasMatchingSession("valid-token", "session-1")).thenReturn(true);
|
||||
when(userDetailsService.loadUserByUsername("alice")).thenReturn(disabledUserDetails);
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetAuthenticationWhenTokenIsValidAndUserIsActive() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer valid-token");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
User domainUser = createDomainUser("alice", "session-1");
|
||||
UserDetails activeUserDetails = org.springframework.security.core.userdetails.User.builder()
|
||||
.username("alice")
|
||||
.password("hashed")
|
||||
.authorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))
|
||||
.build();
|
||||
when(jwtTokenProvider.validateToken("valid-token")).thenReturn(true);
|
||||
when(jwtTokenProvider.getUsername("valid-token")).thenReturn("alice");
|
||||
when(userDetailsService.loadDomainUser("alice")).thenReturn(domainUser);
|
||||
when(jwtTokenProvider.hasMatchingSession("valid-token", "session-1")).thenReturn(true);
|
||||
when(userDetailsService.loadUserByUsername("alice")).thenReturn(activeUserDetails);
|
||||
|
||||
filter.doFilterInternal(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(request, response);
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull();
|
||||
assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("alice");
|
||||
}
|
||||
|
||||
private User createDomainUser(String username, String sessionId) {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername(username);
|
||||
user.setEmail(username + "@example.com");
|
||||
user.setPasswordHash("hashed");
|
||||
user.setActiveSessionId(sessionId);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
return user;
|
||||
}
|
||||
|
||||
// Helper to avoid unused import warning on Mockito.any()
|
||||
private static <T> T any() {
|
||||
return org.mockito.ArgumentMatchers.any();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.config.FileStorageProperties;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
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.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Covers edge cases not addressed in FileServiceTest.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FileServiceEdgeCaseTest {
|
||||
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
@Mock
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
private FileService fileService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setMaxFileSize(500L * 1024 * 1024);
|
||||
fileService = new FileService(storedFileRepository, fileContentStorage, fileShareLinkRepository, properties);
|
||||
}
|
||||
|
||||
// --- normalizeDirectoryPath edge cases ---
|
||||
|
||||
@Test
|
||||
void shouldRejectPathContainingDotDot() {
|
||||
User user = createUser(1L);
|
||||
|
||||
assertThatThrownBy(() -> fileService.mkdir(user, "/docs/../secret"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("路径不合法");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeBackslashesInPath() {
|
||||
User user = createUser(1L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(1L, "/", "docs")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(inv -> {
|
||||
StoredFile f = inv.getArgument(0);
|
||||
f.setId(10L);
|
||||
return f;
|
||||
});
|
||||
|
||||
// backslash should be treated as path separator and normalized
|
||||
FileMetadataResponse response = fileService.mkdir(user, "\\docs");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/docs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeTrailingSlashInPath() {
|
||||
User user = createUser(1L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(1L, "/", "docs")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(inv -> {
|
||||
StoredFile f = inv.getArgument(0);
|
||||
f.setId(10L);
|
||||
return f;
|
||||
});
|
||||
|
||||
FileMetadataResponse response = fileService.mkdir(user, "/docs/");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/docs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeDoubleSlashInPath() {
|
||||
User user = createUser(1L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(1L, "/", "docs")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(inv -> {
|
||||
StoredFile f = inv.getArgument(0);
|
||||
f.setId(10L);
|
||||
return f;
|
||||
});
|
||||
|
||||
FileMetadataResponse response = fileService.mkdir(user, "//docs");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/docs");
|
||||
}
|
||||
|
||||
// --- mkdir edge cases ---
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingRootDirectory() {
|
||||
User user = createUser(1L);
|
||||
|
||||
assertThatThrownBy(() -> fileService.mkdir(user, "/"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("根目录无需创建");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingAlreadyExistingDirectory() {
|
||||
User user = createUser(1L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(1L, "/", "docs")).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> fileService.mkdir(user, "/docs"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("目录已存在");
|
||||
}
|
||||
|
||||
// --- download redirect for direct download ---
|
||||
|
||||
@Test
|
||||
void shouldReturn302RedirectWhenStorageSupportsDirectDownloadForFile() {
|
||||
User user = createUser(1L);
|
||||
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
||||
when(fileContentStorage.createDownloadUrl(1L, "/docs", "notes.txt", "notes.txt"))
|
||||
.thenReturn("https://cdn.example.com/notes.txt");
|
||||
|
||||
ResponseEntity<?> response = fileService.download(user, 10L);
|
||||
|
||||
assertThat(response.getStatusCodeValue()).isEqualTo(302);
|
||||
assertThat(response.getHeaders().getFirst(HttpHeaders.LOCATION))
|
||||
.isEqualTo("https://cdn.example.com/notes.txt");
|
||||
}
|
||||
|
||||
// --- createShareLink edge cases ---
|
||||
|
||||
@Test
|
||||
void shouldRejectCreatingShareLinkForDirectory() {
|
||||
User user = createUser(1L);
|
||||
StoredFile directory = createDirectory(10L, user, "/", "docs");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
|
||||
assertThatThrownBy(() -> fileService.createShareLink(user, 10L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("目录暂不支持分享链接");
|
||||
}
|
||||
|
||||
// --- getDownloadUrl edge cases ---
|
||||
|
||||
@Test
|
||||
void shouldRejectDownloadUrlForDirectory() {
|
||||
User user = createUser(1L);
|
||||
StoredFile directory = createDirectory(10L, user, "/", "docs");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
|
||||
assertThatThrownBy(() -> fileService.getDownloadUrl(user, 10L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("目录不支持下载");
|
||||
}
|
||||
|
||||
// --- upload size limit ---
|
||||
|
||||
@Test
|
||||
void shouldRejectUploadExceedingMaxFileSize() {
|
||||
User user = createUser(1L);
|
||||
long oversizedFile = 500L * 1024 * 1024 + 1;
|
||||
|
||||
assertThatThrownBy(() -> fileService.initiateUpload(user,
|
||||
new InitiateUploadRequest("/docs", "big.zip", "application/zip", oversizedFile)))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("文件大小超出限制");
|
||||
}
|
||||
|
||||
// --- rename no-op when name unchanged ---
|
||||
|
||||
@Test
|
||||
void shouldReturnUnchangedFileWhenRenameToSameName() {
|
||||
User user = createUser(1L);
|
||||
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
|
||||
|
||||
FileMetadataResponse response = fileService.rename(user, 10L, "notes.txt");
|
||||
|
||||
assertThat(response.filename()).isEqualTo("notes.txt");
|
||||
verify(storedFileRepository, org.mockito.Mockito.never()).save(any());
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername("user-" + id);
|
||||
user.setEmail("user-" + id + "@example.com");
|
||||
user.setPasswordHash("encoded");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
return user;
|
||||
}
|
||||
|
||||
private StoredFile createFile(Long id, User user, String path, String filename) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(id);
|
||||
file.setUser(user);
|
||||
file.setFilename(filename);
|
||||
file.setPath(path);
|
||||
file.setStorageName(filename);
|
||||
file.setSize(5L);
|
||||
file.setDirectory(false);
|
||||
file.setCreatedAt(LocalDateTime.now());
|
||||
return file;
|
||||
}
|
||||
|
||||
private StoredFile createDirectory(Long id, User user, String path, String filename) {
|
||||
StoredFile dir = createFile(id, user, path, filename);
|
||||
dir.setDirectory(true);
|
||||
dir.setContentType("directory");
|
||||
dir.setSize(0L);
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user