Fix Android WebView API access and mobile shell layout

This commit is contained in:
yoyuzh
2026-04-03 14:37:21 +08:00
parent f02ff9342f
commit 56f2a9fe0d
121 changed files with 4751 additions and 700 deletions

View File

@@ -31,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -42,6 +43,9 @@ class FileServiceTest {
@Mock
private StoredFileRepository storedFileRepository;
@Mock
private FileBlobRepository fileBlobRepository;
@Mock
private FileContentStorage fileContentStorage;
@@ -56,7 +60,14 @@ class FileServiceTest {
void setUp() {
FileStorageProperties properties = new FileStorageProperties();
properties.setMaxFileSize(500L * 1024 * 1024);
fileService = new FileService(storedFileRepository, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties);
fileService = new FileService(
storedFileRepository,
fileBlobRepository,
fileContentStorage,
fileShareLinkRepository,
adminMetricsService,
properties
);
}
@Test
@@ -65,6 +76,11 @@ class FileServiceTest {
MockMultipartFile multipartFile = new MockMultipartFile(
"file", "notes.txt", "text/plain", "hello".getBytes());
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> {
FileBlob blob = invocation.getArgument(0);
blob.setId(100L);
return blob;
});
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
StoredFile file = invocation.getArgument(0);
file.setId(10L);
@@ -75,22 +91,28 @@ class FileServiceTest {
assertThat(response.id()).isEqualTo(10L);
assertThat(response.path()).isEqualTo("/docs");
verify(fileContentStorage).upload(7L, "/docs", "notes.txt", multipartFile);
verify(fileContentStorage).uploadBlob(org.mockito.ArgumentMatchers.argThat(key -> key != null && key.startsWith("blobs/")), eq(multipartFile));
verify(fileBlobRepository).save(org.mockito.ArgumentMatchers.argThat(blob ->
blob.getObjectKey() != null
&& blob.getObjectKey().startsWith("blobs/")
&& blob.getSize().equals(5L)
&& "text/plain".equals(blob.getContentType())));
}
@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"));
when(fileContentStorage.prepareBlobUpload(eq("/docs"), eq("notes.txt"), org.mockito.ArgumentMatchers.argThat(key -> key != null && key.startsWith("blobs/")), eq("text/plain"), eq(12L)))
.thenReturn(new PreparedUpload(true, "https://upload.example.com", "PUT", Map.of("Content-Type", "text/plain"), "blobs/upload-1"));
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);
assertThat(response.storageName()).startsWith("blobs/");
verify(fileContentStorage).prepareBlobUpload(eq("/docs"), eq("notes.txt"), org.mockito.ArgumentMatchers.argThat(key -> key != null && key.startsWith("blobs/")), eq("text/plain"), eq(12L));
}
@Test
@@ -98,20 +120,25 @@ class FileServiceTest {
User user = createUser(7L);
long uploadSize = 500L * 1024 * 1024;
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "movie.zip")).thenReturn(false);
when(fileContentStorage.prepareUpload(7L, "/docs", "movie.zip", "application/zip", uploadSize))
.thenReturn(new PreparedUpload(true, "https://upload.example.com", "PUT", Map.of(), "movie.zip"));
when(fileContentStorage.prepareBlobUpload(eq("/docs"), eq("movie.zip"), org.mockito.ArgumentMatchers.argThat(key -> key != null && key.startsWith("blobs/")), eq("application/zip"), eq(uploadSize)))
.thenReturn(new PreparedUpload(true, "https://upload.example.com", "PUT", Map.of(), "blobs/upload-2"));
InitiateUploadResponse response = fileService.initiateUpload(user,
new InitiateUploadRequest("/docs", "movie.zip", "application/zip", uploadSize));
assertThat(response.direct()).isTrue();
verify(fileContentStorage).prepareUpload(7L, "/docs", "movie.zip", "application/zip", uploadSize);
verify(fileContentStorage).prepareBlobUpload(eq("/docs"), eq("movie.zip"), org.mockito.ArgumentMatchers.argThat(key -> key != null && key.startsWith("blobs/")), eq("application/zip"), eq(uploadSize));
}
@Test
void shouldCompleteDirectUploadAndPersistMetadata() {
User user = createUser(7L);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> {
FileBlob blob = invocation.getArgument(0);
blob.setId(101L);
return blob;
});
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
StoredFile file = invocation.getArgument(0);
file.setId(11L);
@@ -119,10 +146,44 @@ class FileServiceTest {
});
FileMetadataResponse response = fileService.completeUpload(user,
new CompleteUploadRequest("/docs", "notes.txt", "notes.txt", "text/plain", 12L));
new CompleteUploadRequest("/docs", "notes.txt", "blobs/upload-3", "text/plain", 12L));
assertThat(response.id()).isEqualTo(11L);
verify(fileContentStorage).completeUpload(7L, "/docs", "notes.txt", "text/plain", 12L);
verify(fileContentStorage).completeBlobUpload("blobs/upload-3", "text/plain", 12L);
}
@Test
void shouldDeleteUploadedBlobWhenMetadataSaveFails() {
User user = createUser(7L);
MockMultipartFile multipartFile = new MockMultipartFile(
"file", "notes.txt", "text/plain", "hello".getBytes());
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "docs")).thenReturn(true);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
assertThatThrownBy(() -> fileService.upload(user, "/docs", multipartFile))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("insert failed");
verify(fileContentStorage).deleteBlob(org.mockito.ArgumentMatchers.argThat(
key -> key != null && key.startsWith("blobs/")));
}
@Test
void shouldDeleteCompletedUploadBlobWhenMetadataSaveFails() {
User user = createUser(7L);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "docs")).thenReturn(true);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
assertThatThrownBy(() -> fileService.completeUpload(user,
new CompleteUploadRequest("/docs", "notes.txt", "blobs/upload-fail", "text/plain", 12L)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("insert failed");
verify(fileContentStorage).deleteBlob("blobs/upload-fail");
}
@Test
@@ -131,14 +192,15 @@ class FileServiceTest {
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/projects/site", "logo.png")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "projects")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/projects", "site")).thenReturn(false);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
fileService.completeUpload(user,
new CompleteUploadRequest("/projects/site", "logo.png", "logo.png", "image/png", 12L));
new CompleteUploadRequest("/projects/site", "logo.png", "blobs/upload-4", "image/png", 12L));
verify(fileContentStorage).ensureDirectory(7L, "/projects");
verify(fileContentStorage).ensureDirectory(7L, "/projects/site");
verify(fileContentStorage).completeUpload(7L, "/projects/site", "logo.png", "image/png", 12L);
verify(fileContentStorage).completeBlobUpload("blobs/upload-4", "image/png", 12L);
verify(storedFileRepository, times(3)).save(any(StoredFile.class));
}
@@ -146,14 +208,14 @@ class FileServiceTest {
void shouldRenameFileThroughConfiguredStorage() {
User user = createUser(7L);
StoredFile storedFile = createFile(10L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(storedFile));
when(storedFileRepository.findDetailedById(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");
verify(fileContentStorage, never()).renameFile(any(), any(), any(), any());
}
@Test
@@ -162,7 +224,7 @@ class FileServiceTest {
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.findDetailedById(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));
@@ -171,7 +233,7 @@ class FileServiceTest {
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));
verify(fileContentStorage, never()).renameDirectory(any(), any(), any(), any());
}
@Test
@@ -179,7 +241,7 @@ class FileServiceTest {
User user = createUser(7L);
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
StoredFile targetDirectory = createDirectory(11L, user, "/", "下载");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(file));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "下载")).thenReturn(Optional.of(targetDirectory));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/下载", "notes.txt")).thenReturn(false);
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
@@ -188,7 +250,7 @@ class FileServiceTest {
assertThat(response.path()).isEqualTo("/下载");
assertThat(file.getPath()).isEqualTo("/下载");
verify(fileContentStorage).moveFile(7L, "/docs", "/下载", "notes.txt");
verify(fileContentStorage, never()).moveFile(any(), any(), any(), any());
}
@Test
@@ -197,7 +259,7 @@ class FileServiceTest {
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
StoredFile targetDirectory = createDirectory(11L, user, "/", "图片");
StoredFile childFile = createFile(12L, user, "/docs/archive", "nested.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "图片")).thenReturn(Optional.of(targetDirectory));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片", "archive")).thenReturn(false);
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
@@ -209,7 +271,7 @@ class FileServiceTest {
assertThat(response.path()).isEqualTo("/图片/archive");
assertThat(directory.getPath()).isEqualTo("/图片");
assertThat(childFile.getPath()).isEqualTo("/图片/archive");
verify(fileContentStorage).renameDirectory(7L, "/docs/archive", "/图片/archive", List.of(childFile));
verify(fileContentStorage, never()).renameDirectory(any(), any(), any(), any());
}
@Test
@@ -219,7 +281,7 @@ class FileServiceTest {
StoredFile docsDirectory = createDirectory(11L, user, "/", "docs");
StoredFile archiveDirectory = createDirectory(12L, user, "/docs", "archive");
StoredFile descendantDirectory = createDirectory(13L, user, "/docs/archive", "nested");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
.thenReturn(Optional.of(docsDirectory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs", "archive"))
@@ -235,9 +297,10 @@ class FileServiceTest {
@Test
void shouldCopyFileToAnotherDirectory() {
User user = createUser(7L);
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
FileBlob blob = createBlob(50L, "blobs/blob-copy", 5L, "text/plain");
StoredFile file = createFile(10L, user, "/docs", "notes.txt", blob);
StoredFile targetDirectory = createDirectory(11L, user, "/", "下载");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(file));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "下载")).thenReturn(Optional.of(targetDirectory));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/下载", "notes.txt")).thenReturn(false);
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
@@ -252,7 +315,8 @@ class FileServiceTest {
assertThat(response.id()).isEqualTo(20L);
assertThat(response.path()).isEqualTo("/下载");
verify(fileContentStorage).copyFile(7L, "/docs", "/下载", "notes.txt");
assertThat(file.getBlob()).isSameAs(blob);
verify(fileContentStorage, never()).copyFile(any(), any(), any(), any());
}
@Test
@@ -261,9 +325,11 @@ class FileServiceTest {
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
StoredFile targetDirectory = createDirectory(11L, user, "/", "图片");
StoredFile childDirectory = createDirectory(12L, user, "/docs/archive", "nested");
StoredFile childFile = createFile(13L, user, "/docs/archive", "notes.txt");
StoredFile nestedFile = createFile(14L, user, "/docs/archive/nested", "todo.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
FileBlob childBlob = createBlob(51L, "blobs/blob-archive-1", 5L, "text/plain");
FileBlob nestedBlob = createBlob(52L, "blobs/blob-archive-2", 5L, "text/plain");
StoredFile childFile = createFile(13L, user, "/docs/archive", "notes.txt", childBlob);
StoredFile nestedFile = createFile(14L, user, "/docs/archive/nested", "todo.txt", nestedBlob);
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "图片")).thenReturn(Optional.of(targetDirectory));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片", "archive")).thenReturn(false);
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive"))
@@ -282,10 +348,7 @@ class FileServiceTest {
FileMetadataResponse response = fileService.copy(user, 10L, "/图片");
assertThat(response.path()).isEqualTo("/图片/archive");
verify(fileContentStorage).ensureDirectory(7L, "/图片/archive");
verify(fileContentStorage).ensureDirectory(7L, "/图片/archive/nested");
verify(fileContentStorage).copyFile(7L, "/docs/archive", "/图片/archive", "notes.txt");
verify(fileContentStorage).copyFile(7L, "/docs/archive/nested", "/图片/archive/nested", "todo.txt");
verify(fileContentStorage, never()).copyFile(any(), any(), any(), any());
}
@Test
@@ -295,7 +358,7 @@ class FileServiceTest {
StoredFile docsDirectory = createDirectory(11L, user, "/", "docs");
StoredFile archiveDirectory = createDirectory(12L, user, "/docs", "archive");
StoredFile descendantDirectory = createDirectory(13L, user, "/docs/archive", "nested");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
.thenReturn(Optional.of(docsDirectory));
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs", "archive"))
@@ -313,7 +376,7 @@ class FileServiceTest {
User owner = createUser(1L);
User requester = createUser(2L);
StoredFile storedFile = createFile(100L, owner, "/docs", "notes.txt");
when(storedFileRepository.findById(100L)).thenReturn(Optional.of(storedFile));
when(storedFileRepository.findDetailedById(100L)).thenReturn(Optional.of(storedFile));
assertThatThrownBy(() -> fileService.delete(requester, 100L))
.isInstanceOf(BusinessException.class)
@@ -324,18 +387,51 @@ class FileServiceTest {
void shouldDeleteDirectoryWithNestedFilesViaStorage() {
User user = createUser(7L);
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt");
FileBlob blob = createBlob(60L, "blobs/blob-delete", 5L, "text/plain");
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt", blob);
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
when(storedFileRepository.countByBlobId(60L)).thenReturn(1L);
fileService.delete(user, 10L);
verify(fileContentStorage).deleteDirectory(7L, "/docs/archive", List.of(childFile));
verify(fileContentStorage).deleteBlob("blobs/blob-delete");
verify(fileBlobRepository).delete(blob);
verify(storedFileRepository).deleteAll(List.of(childFile));
verify(storedFileRepository).delete(directory);
}
@Test
void shouldDeleteSharedBlobOnlyWhenLastReferenceIsRemoved() {
User user = createUser(7L);
FileBlob blob = createBlob(70L, "blobs/blob-shared", 5L, "text/plain");
StoredFile storedFile = createFile(15L, user, "/docs", "shared.txt", blob);
when(storedFileRepository.findDetailedById(15L)).thenReturn(Optional.of(storedFile));
when(storedFileRepository.countByBlobId(70L)).thenReturn(2L);
fileService.delete(user, 15L);
verify(fileContentStorage, never()).deleteBlob(any());
verify(fileBlobRepository, never()).delete(any());
verify(storedFileRepository).delete(storedFile);
}
@Test
void shouldDeleteBlobObjectWhenLastReferenceIsRemoved() {
User user = createUser(7L);
FileBlob blob = createBlob(71L, "blobs/blob-last", 5L, "text/plain");
StoredFile storedFile = createFile(16L, user, "/docs", "last.txt", blob);
when(storedFileRepository.findDetailedById(16L)).thenReturn(Optional.of(storedFile));
when(storedFileRepository.countByBlobId(71L)).thenReturn(1L);
fileService.delete(user, 16L);
verify(fileContentStorage).deleteBlob("blobs/blob-last");
verify(fileBlobRepository).delete(blob);
verify(storedFileRepository).delete(storedFile);
}
@Test
void shouldListFilesByPathWithPagination() {
User user = createUser(7L);
@@ -370,9 +466,9 @@ class FileServiceTest {
void shouldUseSignedDownloadUrlWhenStorageSupportsDirectDownload() {
User user = createUser(7L);
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
when(storedFileRepository.findDetailedById(22L)).thenReturn(Optional.of(file));
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
when(fileContentStorage.createDownloadUrl(7L, "/docs", "notes.txt", "notes.txt"))
when(fileContentStorage.createBlobDownloadUrl("blobs/blob-22", "notes.txt"))
.thenReturn("https://download.example.com/file");
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
@@ -384,7 +480,7 @@ class FileServiceTest {
void shouldFallbackToBackendDownloadUrlWhenStorageIsLocal() {
User user = createUser(7L);
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
when(storedFileRepository.findDetailedById(22L)).thenReturn(Optional.of(file));
when(fileContentStorage.supportsDirectDownload()).thenReturn(false);
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
@@ -401,12 +497,12 @@ class FileServiceTest {
StoredFile childFile = createFile(12L, user, "/docs/archive", "notes.txt");
StoredFile nestedFile = createFile(13L, user, "/docs/archive/nested", "todo.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive"))
.thenReturn(List.of(childDirectory, childFile, nestedFile));
when(fileContentStorage.readFile(7L, "/docs/archive", "notes.txt"))
when(fileContentStorage.readBlob("blobs/blob-12"))
.thenReturn("hello".getBytes(StandardCharsets.UTF_8));
when(fileContentStorage.readFile(7L, "/docs/archive/nested", "todo.txt"))
when(fileContentStorage.readBlob("blobs/blob-13"))
.thenReturn("world".getBytes(StandardCharsets.UTF_8));
var response = fileService.download(user, 10L);
@@ -430,15 +526,15 @@ class FileServiceTest {
assertThat(entries).containsEntry("archive/nested/", "");
assertThat(entries).containsEntry("archive/notes.txt", "hello");
assertThat(entries).containsEntry("archive/nested/todo.txt", "world");
verify(fileContentStorage).readFile(7L, "/docs/archive", "notes.txt");
verify(fileContentStorage).readFile(7L, "/docs/archive/nested", "todo.txt");
verify(fileContentStorage).readBlob("blobs/blob-12");
verify(fileContentStorage).readBlob("blobs/blob-13");
}
@Test
void shouldCreateShareLinkForOwnedFile() {
User user = createUser(7L);
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
when(storedFileRepository.findDetailedById(22L)).thenReturn(Optional.of(file));
when(fileShareLinkRepository.save(any(FileShareLink.class))).thenAnswer(invocation -> {
FileShareLink shareLink = invocation.getArgument(0);
shareLink.setId(100L);
@@ -457,7 +553,8 @@ class FileServiceTest {
void shouldImportSharedFileIntoRecipientWorkspace() {
User owner = createUser(7L);
User recipient = createUser(8L);
StoredFile sourceFile = createFile(22L, owner, "/docs", "notes.txt");
FileBlob blob = createBlob(80L, "blobs/blob-import", 5L, "text/plain");
StoredFile sourceFile = createFile(22L, owner, "/docs", "notes.txt", blob);
FileShareLink shareLink = new FileShareLink();
shareLink.setId(100L);
shareLink.setToken("share-token-1");
@@ -471,21 +568,30 @@ class FileServiceTest {
file.setId(200L);
return file;
});
when(fileContentStorage.readFile(7L, "/docs", "notes.txt"))
.thenReturn("hello".getBytes(StandardCharsets.UTF_8));
FileMetadataResponse response = fileService.importSharedFile(recipient, "share-token-1", "/下载");
assertThat(response.id()).isEqualTo(200L);
assertThat(response.path()).isEqualTo("/下载");
assertThat(response.filename()).isEqualTo("notes.txt");
verify(fileContentStorage).storeImportedFile(
eq(8L),
eq("/下载"),
eq("notes.txt"),
eq(sourceFile.getContentType()),
aryEq("hello".getBytes(StandardCharsets.UTF_8))
);
verify(fileContentStorage, never()).storeImportedFile(any(), any(), any(), any(), any());
verify(fileContentStorage, never()).readFile(any(), any(), any());
}
@Test
void shouldDeleteImportedBlobWhenMetadataSaveFails() {
User recipient = createUser(8L);
byte[] content = "hello".getBytes(StandardCharsets.UTF_8);
when(storedFileRepository.existsByUserIdAndPathAndFilename(8L, "/下载", "notes.txt")).thenReturn(false);
when(storedFileRepository.existsByUserIdAndPathAndFilename(8L, "/", "下载")).thenReturn(true);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
assertThatThrownBy(() -> fileService.importExternalFile(recipient, "/下载", "notes.txt", "text/plain", content.length, content))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("insert failed");
verify(fileContentStorage).deleteBlob(org.mockito.ArgumentMatchers.argThat(
key -> key != null && key.startsWith("blobs/")));
}
private User createUser(Long id) {
@@ -498,7 +604,21 @@ class FileServiceTest {
return user;
}
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;
}
private StoredFile createFile(Long id, User user, String path, String filename) {
return createFile(id, user, path, filename, createBlob(id, "blobs/blob-" + id, 5L, "text/plain"));
}
private StoredFile createFile(Long id, User user, String path, String filename, FileBlob blob) {
StoredFile file = new StoredFile();
file.setId(id);
file.setUser(user);
@@ -506,7 +626,8 @@ class FileServiceTest {
file.setPath(path);
file.setSize(5L);
file.setDirectory(false);
file.setStorageName(filename);
file.setContentType("text/plain");
file.setBlob(blob);
file.setCreatedAt(LocalDateTime.now());
return file;
}
@@ -516,6 +637,7 @@ class FileServiceTest {
directory.setDirectory(true);
directory.setContentType("directory");
directory.setSize(0L);
directory.setBlob(null);
return directory;
}
}