276 lines
9.6 KiB
Java
276 lines
9.6 KiB
Java
package com.yoyuzh.files;
|
|
|
|
import com.yoyuzh.admin.AdminMetricsService;
|
|
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 FileBlobRepository fileBlobRepository;
|
|
@Mock
|
|
private FileContentStorage fileContentStorage;
|
|
@Mock
|
|
private FileShareLinkRepository fileShareLinkRepository;
|
|
@Mock
|
|
private AdminMetricsService adminMetricsService;
|
|
|
|
private FileService fileService;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
FileStorageProperties properties = new FileStorageProperties();
|
|
properties.setMaxFileSize(500L * 1024 * 1024);
|
|
fileService = new FileService(
|
|
storedFileRepository,
|
|
fileBlobRepository,
|
|
fileContentStorage,
|
|
fileShareLinkRepository,
|
|
adminMetricsService,
|
|
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.findDetailedById(10L)).thenReturn(Optional.of(file));
|
|
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
|
when(fileContentStorage.createBlobDownloadUrl("blobs/blob-10", "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.findDetailedById(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.findDetailedById(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("文件大小超出限制");
|
|
}
|
|
|
|
@Test
|
|
void shouldRejectUploadExceedingUserMaxUploadSizeLimit() {
|
|
User user = createUser(1L);
|
|
user.setMaxUploadSizeBytes(1024L);
|
|
|
|
assertThatThrownBy(() -> fileService.initiateUpload(user,
|
|
new InitiateUploadRequest("/docs", "large.bin", "application/octet-stream", 1025L)))
|
|
.isInstanceOf(BusinessException.class)
|
|
.hasMessageContaining("文件大小超出限制");
|
|
}
|
|
|
|
@Test
|
|
void shouldRejectUploadWhenUserStorageQuotaInsufficient() {
|
|
User user = createUser(1L);
|
|
user.setStorageQuotaBytes(1024L);
|
|
when(storedFileRepository.sumFileSizeByUserId(1L)).thenReturn(900L);
|
|
|
|
assertThatThrownBy(() -> fileService.initiateUpload(user,
|
|
new InitiateUploadRequest("/docs", "quota.bin", "application/octet-stream", 200L)))
|
|
.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.findDetailedById(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.setSize(5L);
|
|
file.setDirectory(false);
|
|
file.setContentType("text/plain");
|
|
file.setBlob(createBlob(id, "blobs/blob-" + id, 5L, "text/plain"));
|
|
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);
|
|
dir.setBlob(null);
|
|
return dir;
|
|
}
|
|
|
|
private FileBlob createBlob(Long id, String objectKey, Long size, String contentType) {
|
|
FileBlob blob = new FileBlob();
|
|
blob.setId(id);
|
|
blob.setObjectKey(objectKey);
|
|
blob.setSize(size);
|
|
blob.setContentType(contentType);
|
|
blob.setCreatedAt(LocalDateTime.now());
|
|
return blob;
|
|
}
|
|
}
|