完善前端直连oss功能,新增重命名以及删除文件功能,完善后端接口
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurityConfigTest {
|
||||
|
||||
@Test
|
||||
void corsConfigurationShouldAllowPatchRequests() {
|
||||
CorsProperties corsProperties = new CorsProperties();
|
||||
corsProperties.setAllowedOrigins(java.util.List.of("https://yoyuzh.xyz"));
|
||||
|
||||
SecurityConfig securityConfig = new SecurityConfig(
|
||||
null,
|
||||
null,
|
||||
new ObjectMapper(),
|
||||
corsProperties
|
||||
);
|
||||
|
||||
CorsConfigurationSource source = securityConfig.corsConfigurationSource();
|
||||
CorsConfiguration configuration = source.getCorsConfiguration(
|
||||
new org.springframework.mock.web.MockHttpServletRequest("OPTIONS", "/api/files/1/rename"));
|
||||
|
||||
assertThat(configuration).isNotNull();
|
||||
assertThat(configuration.getAllowedMethods()).contains("PATCH");
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ 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 com.yoyuzh.files.storage.PreparedUpload;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
@@ -13,14 +14,16 @@ import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -31,21 +34,20 @@ class FileServiceTest {
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
private FileService fileService;
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
private FileService fileService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setRootDir(tempDir.toString());
|
||||
properties.setMaxFileSize(50 * 1024 * 1024);
|
||||
fileService = new FileService(storedFileRepository, properties);
|
||||
fileService = new FileService(storedFileRepository, fileContentStorage, properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreUploadedFileUnderUserDirectory() {
|
||||
void shouldStoreUploadedFileViaConfiguredStorage() {
|
||||
User user = createUser(7L);
|
||||
MockMultipartFile multipartFile = new MockMultipartFile(
|
||||
"file", "notes.txt", "text/plain", "hello".getBytes());
|
||||
@@ -60,8 +62,71 @@ class FileServiceTest {
|
||||
|
||||
assertThat(response.id()).isEqualTo(10L);
|
||||
assertThat(response.path()).isEqualTo("/docs");
|
||||
assertThat(response.directory()).isFalse();
|
||||
assertThat(tempDir.resolve("7/docs/notes.txt")).exists();
|
||||
verify(fileContentStorage).upload(7L, "/docs", "notes.txt", multipartFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInitiateDirectUploadThroughStorage() {
|
||||
User user = createUser(7L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
|
||||
when(fileContentStorage.prepareUpload(7L, "/docs", "notes.txt", "text/plain", 12L))
|
||||
.thenReturn(new PreparedUpload(true, "https://upload.example.com", "PUT", Map.of("Content-Type", "text/plain"), "notes.txt"));
|
||||
|
||||
InitiateUploadResponse response = fileService.initiateUpload(user,
|
||||
new InitiateUploadRequest("/docs", "notes.txt", "text/plain", 12L));
|
||||
|
||||
assertThat(response.direct()).isTrue();
|
||||
assertThat(response.uploadUrl()).isEqualTo("https://upload.example.com");
|
||||
verify(fileContentStorage).prepareUpload(7L, "/docs", "notes.txt", "text/plain", 12L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteDirectUploadAndPersistMetadata() {
|
||||
User user = createUser(7L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
|
||||
StoredFile file = invocation.getArgument(0);
|
||||
file.setId(11L);
|
||||
return file;
|
||||
});
|
||||
|
||||
FileMetadataResponse response = fileService.completeUpload(user,
|
||||
new CompleteUploadRequest("/docs", "notes.txt", "notes.txt", "text/plain", 12L));
|
||||
|
||||
assertThat(response.id()).isEqualTo(11L);
|
||||
verify(fileContentStorage).completeUpload(7L, "/docs", "notes.txt", "text/plain", 12L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenameFileThroughConfiguredStorage() {
|
||||
User user = createUser(7L);
|
||||
StoredFile storedFile = createFile(10L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(storedFile));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "renamed.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
FileMetadataResponse response = fileService.rename(user, 10L, "renamed.txt");
|
||||
|
||||
assertThat(response.filename()).isEqualTo("renamed.txt");
|
||||
verify(fileContentStorage).renameFile(7L, "/docs", "notes.txt", "renamed.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenameDirectoryAndUpdateDescendantPaths() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt");
|
||||
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "renamed-archive")).thenReturn(false);
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
FileMetadataResponse response = fileService.rename(user, 10L, "renamed-archive");
|
||||
|
||||
assertThat(response.filename()).isEqualTo("renamed-archive");
|
||||
assertThat(childFile.getPath()).isEqualTo("/docs/renamed-archive");
|
||||
verify(fileContentStorage).renameDirectory(7L, "/docs/archive", "/docs/renamed-archive", List.of(childFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,6 +141,22 @@ class FileServiceTest {
|
||||
.hasMessageContaining("没有权限");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteDirectoryWithNestedFilesViaStorage() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt");
|
||||
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
|
||||
|
||||
fileService.delete(user, 10L);
|
||||
|
||||
verify(fileContentStorage).deleteDirectory(7L, "/docs/archive", List.of(childFile));
|
||||
verify(storedFileRepository).deleteAll(List.of(childFile));
|
||||
verify(storedFileRepository).delete(directory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListFilesByPathWithPagination() {
|
||||
User user = createUser(7L);
|
||||
@@ -100,15 +181,39 @@ class FileServiceTest {
|
||||
|
||||
fileService.ensureDefaultDirectories(user);
|
||||
|
||||
assertThat(tempDir.resolve("7/下载")).exists();
|
||||
assertThat(tempDir.resolve("7/文档")).exists();
|
||||
assertThat(tempDir.resolve("7/图片")).exists();
|
||||
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "下载");
|
||||
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "文档");
|
||||
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "图片");
|
||||
verify(fileContentStorage).ensureDirectory(7L, "/下载");
|
||||
verify(fileContentStorage).ensureDirectory(7L, "/文档");
|
||||
verify(fileContentStorage).ensureDirectory(7L, "/图片");
|
||||
verify(storedFileRepository, times(3)).save(any(StoredFile.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseSignedDownloadUrlWhenStorageSupportsDirectDownload() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
||||
when(fileContentStorage.createDownloadUrl(7L, "/docs", "notes.txt", "notes.txt"))
|
||||
.thenReturn("https://download.example.com/file");
|
||||
|
||||
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
|
||||
|
||||
assertThat(response.url()).isEqualTo("https://download.example.com/file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToBackendDownloadUrlWhenStorageIsLocal() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(false);
|
||||
|
||||
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
|
||||
|
||||
assertThat(response.url()).isEqualTo("/api/files/download/22");
|
||||
verify(fileContentStorage, never()).createDownloadUrl(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
@@ -131,4 +236,12 @@ class FileServiceTest {
|
||||
file.setCreatedAt(LocalDateTime.now());
|
||||
return file;
|
||||
}
|
||||
|
||||
private StoredFile createDirectory(Long id, User user, String path, String filename) {
|
||||
StoredFile directory = createFile(id, user, path, filename);
|
||||
directory.setDirectory(true);
|
||||
directory.setContentType("directory");
|
||||
directory.setSize(0L);
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user