Enable dual-device login and mobile APK update checks
This commit is contained in:
@@ -158,7 +158,8 @@ class FileServiceTest {
|
||||
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(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
|
||||
.thenReturn(Optional.of(createDirectory(20L, user, "/", "docs")));
|
||||
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
|
||||
|
||||
@@ -174,7 +175,8 @@ class FileServiceTest {
|
||||
void shouldDeleteCompletedUploadBlobWhenMetadataSaveFails() {
|
||||
User user = createUser(7L);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/", "docs")).thenReturn(true);
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
|
||||
.thenReturn(Optional.of(createDirectory(21L, user, "/", "docs")));
|
||||
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
|
||||
|
||||
@@ -190,8 +192,8 @@ class FileServiceTest {
|
||||
void shouldCreateMissingDirectoriesBeforeCompletingNestedUpload() {
|
||||
User user = createUser(7L);
|
||||
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(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "projects")).thenReturn(Optional.empty());
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/projects", "site")).thenReturn(Optional.empty());
|
||||
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
@@ -384,52 +386,74 @@ class FileServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteDirectoryWithNestedFilesViaStorage() {
|
||||
void shouldMoveDeletedDirectoryAndDescendantsIntoRecycleBinGroup() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile nestedDirectory = createDirectory(12L, user, "/docs/archive", "nested");
|
||||
FileBlob blob = createBlob(60L, "blobs/blob-delete", 5L, "text/plain");
|
||||
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt", blob);
|
||||
|
||||
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
|
||||
when(storedFileRepository.countByBlobId(60L)).thenReturn(1L);
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(nestedDirectory, childFile));
|
||||
|
||||
fileService.delete(user, 10L);
|
||||
|
||||
verify(fileContentStorage).deleteBlob("blobs/blob-delete");
|
||||
verify(fileBlobRepository).delete(blob);
|
||||
verify(storedFileRepository).deleteAll(List.of(childFile));
|
||||
verify(storedFileRepository).delete(directory);
|
||||
assertThat(directory.getDeletedAt()).isNotNull();
|
||||
assertThat(directory.isRecycleRoot()).isTrue();
|
||||
assertThat(directory.getRecycleGroupId()).isNotBlank();
|
||||
assertThat(directory.getRecycleOriginalPath()).isEqualTo("/docs");
|
||||
assertThat(directory.getPath()).startsWith("/.recycle/");
|
||||
|
||||
assertThat(nestedDirectory.getDeletedAt()).isEqualTo(directory.getDeletedAt());
|
||||
assertThat(nestedDirectory.isRecycleRoot()).isFalse();
|
||||
assertThat(nestedDirectory.getRecycleGroupId()).isEqualTo(directory.getRecycleGroupId());
|
||||
assertThat(nestedDirectory.getRecycleOriginalPath()).isEqualTo("/docs/archive");
|
||||
|
||||
assertThat(childFile.getDeletedAt()).isEqualTo(directory.getDeletedAt());
|
||||
assertThat(childFile.isRecycleRoot()).isFalse();
|
||||
assertThat(childFile.getRecycleGroupId()).isEqualTo(directory.getRecycleGroupId());
|
||||
assertThat(childFile.getRecycleOriginalPath()).isEqualTo("/docs/archive");
|
||||
|
||||
verify(fileContentStorage, never()).deleteBlob(any());
|
||||
verify(fileBlobRepository, never()).delete(any());
|
||||
verify(storedFileRepository, never()).deleteAll(any());
|
||||
verify(storedFileRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteSharedBlobOnlyWhenLastReferenceIsRemoved() {
|
||||
void shouldKeepSharedBlobWhenFileMovesIntoRecycleBin() {
|
||||
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);
|
||||
|
||||
assertThat(storedFile.getDeletedAt()).isNotNull();
|
||||
assertThat(storedFile.isRecycleRoot()).isTrue();
|
||||
verify(fileContentStorage, never()).deleteBlob(any());
|
||||
verify(fileBlobRepository, never()).delete(any());
|
||||
verify(storedFileRepository).delete(storedFile);
|
||||
verify(storedFileRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteBlobObjectWhenLastReferenceIsRemoved() {
|
||||
void shouldDeleteExpiredRecycleBinBlobWhenLastReferenceIsRemoved() {
|
||||
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));
|
||||
storedFile.setDeletedAt(LocalDateTime.now().minusDays(11));
|
||||
storedFile.setRecycleRoot(true);
|
||||
storedFile.setRecycleGroupId("recycle-group-1");
|
||||
storedFile.setRecycleOriginalPath("/docs");
|
||||
storedFile.setPath("/.recycle/recycle-group-1/docs");
|
||||
when(storedFileRepository.findByDeletedAtBefore(any(LocalDateTime.class))).thenReturn(List.of(storedFile));
|
||||
when(storedFileRepository.countByBlobId(71L)).thenReturn(1L);
|
||||
|
||||
fileService.delete(user, 16L);
|
||||
fileService.pruneExpiredRecycleBinItems();
|
||||
|
||||
verify(fileContentStorage).deleteBlob("blobs/blob-last");
|
||||
verify(fileBlobRepository).delete(blob);
|
||||
verify(storedFileRepository).delete(storedFile);
|
||||
verify(storedFileRepository).deleteAll(List.of(storedFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -582,7 +606,8 @@ class FileServiceTest {
|
||||
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(storedFileRepository.findByUserIdAndPathAndFilename(8L, "/", "下载"))
|
||||
.thenReturn(Optional.of(createDirectory(22L, recipient, "/", "下载")));
|
||||
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
doThrow(new IllegalStateException("insert failed")).when(storedFileRepository).save(any(StoredFile.class));
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
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.test.web.servlet.MockMvc;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
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.post;
|
||||
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:recycle_bin_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.storage.root-dir=./target/test-storage-recycle-bin"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class RecycleBinControllerIntegrationTest {
|
||||
|
||||
private static final Path STORAGE_ROOT = Path.of("./target/test-storage-recycle-bin").toAbsolutePath().normalize();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
@Autowired
|
||||
private FileBlobRepository fileBlobRepository;
|
||||
|
||||
private Long deletedFileId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
storedFileRepository.deleteAll();
|
||||
fileBlobRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
|
||||
if (Files.exists(STORAGE_ROOT)) {
|
||||
try (var paths = Files.walk(STORAGE_ROOT)) {
|
||||
paths.sorted((left, right) -> right.compareTo(left)).forEach(path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Files.createDirectories(STORAGE_ROOT);
|
||||
|
||||
User owner = new User();
|
||||
owner.setUsername("alice");
|
||||
owner.setEmail("alice@example.com");
|
||||
owner.setPhoneNumber("13800138000");
|
||||
owner.setPasswordHash("encoded-password");
|
||||
owner.setCreatedAt(LocalDateTime.now());
|
||||
owner = userRepository.save(owner);
|
||||
|
||||
StoredFile docsDirectory = new StoredFile();
|
||||
docsDirectory.setUser(owner);
|
||||
docsDirectory.setFilename("docs");
|
||||
docsDirectory.setPath("/");
|
||||
docsDirectory.setContentType("directory");
|
||||
docsDirectory.setSize(0L);
|
||||
docsDirectory.setDirectory(true);
|
||||
storedFileRepository.save(docsDirectory);
|
||||
|
||||
FileBlob blob = new FileBlob();
|
||||
blob.setObjectKey("blobs/recycle-notes");
|
||||
blob.setContentType("text/plain");
|
||||
blob.setSize(5L);
|
||||
blob.setCreatedAt(LocalDateTime.now());
|
||||
blob = fileBlobRepository.save(blob);
|
||||
|
||||
StoredFile file = new StoredFile();
|
||||
file.setUser(owner);
|
||||
file.setFilename("notes.txt");
|
||||
file.setPath("/docs");
|
||||
file.setContentType("text/plain");
|
||||
file.setSize(5L);
|
||||
file.setDirectory(false);
|
||||
file.setBlob(blob);
|
||||
deletedFileId = storedFileRepository.save(file).getId();
|
||||
|
||||
Path blobPath = STORAGE_ROOT.resolve("blobs").resolve("recycle-notes");
|
||||
Files.createDirectories(blobPath.getParent());
|
||||
Files.writeString(blobPath, "hello", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteListAndRestoreFileThroughRecycleBinApi() throws Exception {
|
||||
mockMvc.perform(delete("/api/files/{fileId}", deletedFileId)
|
||||
.with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(get("/api/files/list")
|
||||
.with(user("alice"))
|
||||
.param("path", "/docs")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items").isEmpty());
|
||||
|
||||
String recycleResponse = mockMvc.perform(get("/api/files/recycle-bin")
|
||||
.with(user("alice"))
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.items[0].path").value("/docs"))
|
||||
.andExpect(jsonPath("$.data.items[0].deletedAt").isNotEmpty())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
Number recycleRootId = JsonPath.read(recycleResponse, "$.data.items[0].id");
|
||||
|
||||
mockMvc.perform(post("/api/files/recycle-bin/{fileId}/restore", recycleRootId)
|
||||
.with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.path").value("/docs"));
|
||||
|
||||
mockMvc.perform(get("/api/files/recycle-bin")
|
||||
.with(user("alice"))
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items").isEmpty());
|
||||
|
||||
mockMvc.perform(get("/api/files/list")
|
||||
.with(user("alice"))
|
||||
.param("path", "/docs")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"));
|
||||
|
||||
StoredFile restoredFile = storedFileRepository.findById(deletedFileId).orElseThrow();
|
||||
assertThat(restoredFile.getDeletedAt()).isNull();
|
||||
assertThat(restoredFile.getRecycleGroupId()).isNull();
|
||||
assertThat(restoredFile.getRecycleOriginalPath()).isNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user