feat(files): add v2 task and metadata workflows
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package com.yoyuzh.api.v2.files;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
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.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:file_events_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-file-events"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class FileEventsV2ControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userRepository.deleteAll();
|
||||
User user = new User();
|
||||
user.setUsername("alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setPhoneNumber("13800138000");
|
||||
user.setPasswordHash("encoded-password");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRequireAuthenticationForFileEventStream() throws Exception {
|
||||
mockMvc.perform(get("/api/v2/files/events").with(anonymous()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOpenStreamAndSendReadyEvent() throws Exception {
|
||||
var result = mockMvc.perform(get("/api/v2/files/events")
|
||||
.with(user("alice"))
|
||||
.param("path", "/docs")
|
||||
.header("X-Yoyuzh-Client-Id", "tab-1"))
|
||||
.andExpect(request().asyncStarted())
|
||||
.andReturn();
|
||||
|
||||
String body = result.getResponse().getContentAsString();
|
||||
assertThat(result.getResponse().getStatus()).isEqualTo(200);
|
||||
assertThat(result.getResponse().getContentType()).startsWith("text/event-stream");
|
||||
assertThat(body).contains("READY");
|
||||
assertThat(body).contains("/docs");
|
||||
assertThat(body).contains("tab-1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.yoyuzh.api.v2.files;
|
||||
|
||||
import com.yoyuzh.api.v2.ApiV2ExceptionHandler;
|
||||
import com.yoyuzh.auth.CustomUserDetailsService;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.files.FileMetadataResponse;
|
||||
import com.yoyuzh.files.FileSearchQuery;
|
||||
import com.yoyuzh.files.FileSearchService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class FileSearchV2ControllerTest {
|
||||
|
||||
private FileSearchService fileSearchService;
|
||||
private CustomUserDetailsService userDetailsService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
fileSearchService = mock(FileSearchService.class);
|
||||
userDetailsService = mock(CustomUserDetailsService.class);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(new FileSearchV2Controller(fileSearchService, userDetailsService))
|
||||
.setControllerAdvice(new ApiV2ExceptionHandler())
|
||||
.setCustomArgumentResolvers(authenticationPrincipalResolver())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSearchFilesWithV2Envelope() throws Exception {
|
||||
User user = createUser(7L);
|
||||
when(userDetailsService.loadDomainUser("alice")).thenReturn(user);
|
||||
when(fileSearchService.search(eq(user), any(FileSearchQuery.class))).thenReturn(new PageResponse<>(
|
||||
List.of(new FileMetadataResponse(
|
||||
10L,
|
||||
"notes.txt",
|
||||
"/docs",
|
||||
5L,
|
||||
"text/plain",
|
||||
false,
|
||||
LocalDateTime.of(2026, 4, 8, 10, 0)
|
||||
)),
|
||||
1,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v2/files/search")
|
||||
.with(user(userDetails()))
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.param("name", "note")
|
||||
.param("type", "file")
|
||||
.param("sizeGte", "1")
|
||||
.param("sizeLte", "100")
|
||||
.param("createdGte", "2026-04-08T08:00:00")
|
||||
.param("createdLte", "2026-04-08T12:00:00")
|
||||
.param("updatedGte", "2026-04-08T09:00:00")
|
||||
.param("updatedLte", "2026-04-08T18:00:00")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedTypeFilter() throws Exception {
|
||||
mockMvc.perform(get("/api/v2/files/search")
|
||||
.with(user(userDetails()))
|
||||
.param("type", "image"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(2400))
|
||||
.andExpect(jsonPath("$.msg").value("文件类型筛选只支持 file 或 directory"));
|
||||
}
|
||||
|
||||
private UserDetails userDetails() {
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername("alice")
|
||||
.password("encoded")
|
||||
.authorities("ROLE_USER")
|
||||
.build();
|
||||
}
|
||||
|
||||
private HandlerMethodArgumentResolver authenticationPrincipalResolver() {
|
||||
UserDetails userDetails = userDetails();
|
||||
return new HandlerMethodArgumentResolver() {
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class)
|
||||
&& UserDetails.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory) {
|
||||
return userDetails;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername("alice");
|
||||
user.setEmail("alice@example.com");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.yoyuzh.api.v2.shares;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.files.FileBlob;
|
||||
import com.yoyuzh.files.FileBlobRepository;
|
||||
import com.yoyuzh.files.FileShareLink;
|
||||
import com.yoyuzh.files.FileShareLinkRepository;
|
||||
import com.yoyuzh.files.StoredFile;
|
||||
import com.yoyuzh.files.StoredFileRepository;
|
||||
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.http.MediaType;
|
||||
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 java.util.Comparator;
|
||||
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
|
||||
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:share_v2_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-share-v2"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class ShareV2ControllerIntegrationTest {
|
||||
|
||||
private static final Path STORAGE_ROOT = Path.of("./target/test-storage-share-v2").toAbsolutePath().normalize();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
@Autowired
|
||||
private FileBlobRepository fileBlobRepository;
|
||||
|
||||
@Autowired
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
private Long sharedFileId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
fileShareLinkRepository.deleteAll();
|
||||
storedFileRepository.deleteAll();
|
||||
fileBlobRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
if (Files.exists(STORAGE_ROOT)) {
|
||||
try (var paths = Files.walk(STORAGE_ROOT)) {
|
||||
paths.sorted(Comparator.reverseOrder()).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);
|
||||
|
||||
User recipient = new User();
|
||||
recipient.setUsername("bob");
|
||||
recipient.setEmail("bob@example.com");
|
||||
recipient.setPhoneNumber("13800138001");
|
||||
recipient.setPasswordHash("encoded-password");
|
||||
recipient.setCreatedAt(LocalDateTime.now());
|
||||
userRepository.save(recipient);
|
||||
|
||||
FileBlob blob = new FileBlob();
|
||||
blob.setObjectKey("blobs/share-v2-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);
|
||||
sharedFileId = storedFileRepository.save(file).getId();
|
||||
|
||||
Path blobPath = STORAGE_ROOT.resolve("blobs").resolve("share-v2-notes");
|
||||
Files.createDirectories(blobPath.getParent());
|
||||
Files.writeString(blobPath, "hello", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateReadVerifyImportAndDeleteOwnV2Share() throws Exception {
|
||||
String createResponse = mockMvc.perform(post("/api/v2/shares")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"password": "Share123",
|
||||
"shareName": "course-share",
|
||||
"allowImport": true,
|
||||
"allowDownload": true
|
||||
}
|
||||
""".formatted(sharedFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.shareName").value("course-share"))
|
||||
.andExpect(jsonPath("$.data.passwordRequired").value(true))
|
||||
.andExpect(jsonPath("$.data.file.filename").value("notes.txt"))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String token = JsonPath.read(createResponse, "$.data.token");
|
||||
Long shareId = ((Number) JsonPath.read(createResponse, "$.data.id")).longValue();
|
||||
|
||||
mockMvc.perform(get("/api/v2/shares/{token}", token).with(anonymous()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.passwordRequired").value(true))
|
||||
.andExpect(jsonPath("$.data.passwordVerified").value(false))
|
||||
.andExpect(jsonPath("$.data.file").value(nullValue()));
|
||||
|
||||
mockMvc.perform(post("/api/v2/shares/{token}/verify-password", token)
|
||||
.with(anonymous())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"password": "WrongPass1!"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(2400));
|
||||
|
||||
mockMvc.perform(post("/api/v2/shares/{token}/verify-password", token)
|
||||
.with(anonymous())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"password": "Share123"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.passwordVerified").value(true))
|
||||
.andExpect(jsonPath("$.data.file.filename").value("notes.txt"));
|
||||
|
||||
mockMvc.perform(post("/api/v2/shares/{token}/import", token)
|
||||
.with(user("bob"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/download",
|
||||
"password": "Share123"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.path").value("/download"));
|
||||
|
||||
mockMvc.perform(get("/api/v2/shares/mine")
|
||||
.with(user("alice"))
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].id").value(shareId))
|
||||
.andExpect(jsonPath("$.data.items[0].file.filename").value("notes.txt"));
|
||||
|
||||
mockMvc.perform(delete("/api/v2/shares/{id}", shareId)
|
||||
.with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(get("/api/v2/shares/mine")
|
||||
.with(user("alice"))
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDisabledOrExpiredV2ShareImports() throws Exception {
|
||||
String disabledResponse = mockMvc.perform(post("/api/v2/shares")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"allowImport": false,
|
||||
"allowDownload": true
|
||||
}
|
||||
""".formatted(sharedFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
String disabledToken = JsonPath.read(disabledResponse, "$.data.token");
|
||||
|
||||
mockMvc.perform(post("/api/v2/shares/{token}/import", disabledToken)
|
||||
.with(user("bob"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/download"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.code").value(2403));
|
||||
|
||||
String expiringResponse = mockMvc.perform(post("/api/v2/shares")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"allowImport": true,
|
||||
"allowDownload": true
|
||||
}
|
||||
""".formatted(sharedFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
String expiringToken = JsonPath.read(expiringResponse, "$.data.token");
|
||||
|
||||
FileShareLink expiringShare = fileShareLinkRepository.findByToken(expiringToken).orElseThrow();
|
||||
expiringShare.setExpiresAt(LocalDateTime.now().minusMinutes(1));
|
||||
fileShareLinkRepository.save(expiringShare);
|
||||
|
||||
mockMvc.perform(post("/api/v2/shares/{token}/import", expiringToken)
|
||||
.with(user("bob"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/download"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value(2404));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDenyDeletingOtherUsersShare() throws Exception {
|
||||
String createResponse = mockMvc.perform(post("/api/v2/shares")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d
|
||||
}
|
||||
""".formatted(sharedFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
Long shareId = ((Number) JsonPath.read(createResponse, "$.data.id")).longValue();
|
||||
|
||||
mockMvc.perform(delete("/api/v2/shares/{id}", shareId)
|
||||
.with(user("bob")))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value(2404));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.yoyuzh.api.v2.tasks;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.files.BackgroundTask;
|
||||
import com.yoyuzh.files.BackgroundTaskRepository;
|
||||
import com.yoyuzh.files.BackgroundTaskStatus;
|
||||
import com.yoyuzh.files.BackgroundTaskType;
|
||||
import com.yoyuzh.files.StoredFile;
|
||||
import com.yoyuzh.files.StoredFileRepository;
|
||||
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.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
|
||||
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:background_task_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-background-task"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class BackgroundTaskV2ControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private BackgroundTaskRepository backgroundTaskRepository;
|
||||
|
||||
@Autowired
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private Long archiveDirectoryId;
|
||||
private Long archiveFileId;
|
||||
private Long extractFileId;
|
||||
private Long mediaFileId;
|
||||
private Long foreignFileId;
|
||||
private Long deletedFileId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
backgroundTaskRepository.deleteAll();
|
||||
storedFileRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
|
||||
User alice = new User();
|
||||
alice.setUsername("alice");
|
||||
alice.setEmail("alice@example.com");
|
||||
alice.setPhoneNumber("13800138000");
|
||||
alice.setPasswordHash("encoded-password");
|
||||
alice.setCreatedAt(LocalDateTime.now());
|
||||
userRepository.save(alice);
|
||||
|
||||
User bob = new User();
|
||||
bob.setUsername("bob");
|
||||
bob.setEmail("bob@example.com");
|
||||
bob.setPhoneNumber("13800138001");
|
||||
bob.setPasswordHash("encoded-password");
|
||||
bob.setCreatedAt(LocalDateTime.now());
|
||||
bob = userRepository.save(bob);
|
||||
|
||||
archiveDirectoryId = storedFileRepository.save(createFile(alice, "/docs", "archive", true, null, 0L, null)).getId();
|
||||
archiveFileId = storedFileRepository.save(createFile(alice, "/docs", "archive-source.txt", false, "text/plain", 12L, null)).getId();
|
||||
extractFileId = storedFileRepository.save(createFile(alice, "/docs", "extract.zip", false, "application/zip", 32L, null)).getId();
|
||||
mediaFileId = storedFileRepository.save(createFile(alice, "/docs", "media.png", false, "image/png", 24L, null)).getId();
|
||||
foreignFileId = storedFileRepository.save(createFile(bob, "/docs", "foreign.zip", false, "application/zip", 32L, null)).getId();
|
||||
deletedFileId = storedFileRepository.save(createFile(alice, "/docs", "deleted.zip", false, "application/zip", 32L, LocalDateTime.now())).getId();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRequireAuthenticationForTaskEndpoints() throws Exception {
|
||||
mockMvc.perform(get("/api/v2/tasks").with(anonymous()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(anonymous())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": 1,
|
||||
"path": "/docs"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueueListGetAndCancelOwnedTasks() throws Exception {
|
||||
String archiveResponse = mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/archive",
|
||||
"correlationId": "archive-1"
|
||||
}
|
||||
""".formatted(archiveDirectoryId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.type").value("ARCHIVE"))
|
||||
.andExpect(jsonPath("$.data.status").value("QUEUED"))
|
||||
.andExpect(jsonPath("$.data.publicStateJson", containsString("\"fileId\":" + archiveDirectoryId)))
|
||||
.andExpect(jsonPath("$.data.publicStateJson", containsString("\"path\":\"/docs/archive\"")))
|
||||
.andExpect(jsonPath("$.data.publicStateJson", containsString("\"directory\":true")))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String extractResponse = mockMvc.perform(post("/api/v2/tasks/extract")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/extract.zip"
|
||||
}
|
||||
""".formatted(extractFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.type").value("EXTRACT"))
|
||||
.andExpect(jsonPath("$.data.status").value("QUEUED"))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String mediaResponse = mockMvc.perform(post("/api/v2/tasks/media-metadata")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/media.png"
|
||||
}
|
||||
""".formatted(mediaFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.type").value("MEDIA_META"))
|
||||
.andExpect(jsonPath("$.data.status").value("QUEUED"))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
Long archiveId = ((Number) JsonPath.read(archiveResponse, "$.data.id")).longValue();
|
||||
Long extractId = ((Number) JsonPath.read(extractResponse, "$.data.id")).longValue();
|
||||
Long mediaId = ((Number) JsonPath.read(mediaResponse, "$.data.id")).longValue();
|
||||
|
||||
mockMvc.perform(get("/api/v2/tasks")
|
||||
.with(user("alice"))
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.total").value(3))
|
||||
.andExpect(jsonPath("$.data.items[0].id").value(mediaId));
|
||||
|
||||
mockMvc.perform(get("/api/v2/tasks/{id}", archiveId).with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.id").value(archiveId))
|
||||
.andExpect(jsonPath("$.data.privateStateJson").doesNotExist());
|
||||
|
||||
mockMvc.perform(delete("/api/v2/tasks/{id}", extractId).with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("CANCELLED"));
|
||||
|
||||
BackgroundTask cancelled = backgroundTaskRepository.findById(extractId).orElseThrow();
|
||||
assertThat(cancelled.getStatus()).isEqualTo(BackgroundTaskStatus.CANCELLED);
|
||||
assertThat(cancelled.getFinishedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectOtherUsersTaskAccess() throws Exception {
|
||||
String response = mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/archive-source.txt"
|
||||
}
|
||||
""".formatted(archiveFileId)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
Long taskId = ((Number) JsonPath.read(response, "$.data.id")).longValue();
|
||||
|
||||
mockMvc.perform(get("/api/v2/tasks/{id}", taskId).with(user("bob")))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
mockMvc.perform(delete("/api/v2/tasks/{id}", taskId).with(user("bob")))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidTaskTargetsBeforeQueueing() throws Exception {
|
||||
mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/foreign.zip"
|
||||
}
|
||||
""".formatted(foreignFileId)))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value(2404));
|
||||
|
||||
mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/deleted.zip"
|
||||
}
|
||||
""".formatted(deletedFileId)))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value(2404));
|
||||
|
||||
mockMvc.perform(post("/api/v2/tasks/archive")
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"fileId": %d,
|
||||
"path": "/docs/client-path.zip"
|
||||
}
|
||||
""".formatted(extractFileId)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value(2400));
|
||||
}
|
||||
|
||||
private StoredFile createFile(User user,
|
||||
String path,
|
||||
String filename,
|
||||
boolean directory,
|
||||
String contentType,
|
||||
Long size,
|
||||
LocalDateTime deletedAt) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setUser(user);
|
||||
file.setPath(path);
|
||||
file.setFilename(filename);
|
||||
file.setDirectory(directory);
|
||||
file.setContentType(contentType);
|
||||
file.setSize(size);
|
||||
file.setDeletedAt(deletedAt);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.api.v2.ApiV2Exception;
|
||||
import com.yoyuzh.auth.User;
|
||||
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 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.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BackgroundTaskServiceTest {
|
||||
|
||||
@Mock
|
||||
private BackgroundTaskRepository backgroundTaskRepository;
|
||||
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
private BackgroundTaskService backgroundTaskService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
backgroundTaskService = new BackgroundTaskService(backgroundTaskRepository, storedFileRepository, new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTaskCreationForForeignFile() {
|
||||
User user = createUser(7L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(99L, 7L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.ARCHIVE,
|
||||
99L,
|
||||
"/docs/foreign.txt",
|
||||
null
|
||||
)).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessage("file not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTaskCreationForDeletedFile() {
|
||||
User user = createUser(7L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(100L, 7L)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.ARCHIVE,
|
||||
100L,
|
||||
"/docs/deleted.txt",
|
||||
null
|
||||
)).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessage("file not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTaskCreationWhenRequestedPathDoesNotMatchFile() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createStoredFile(11L, user, "/docs", "real.txt", false, "text/plain", 3L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(11L, 7L)).thenReturn(Optional.of(file));
|
||||
|
||||
assertThatThrownBy(() -> backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.ARCHIVE,
|
||||
11L,
|
||||
"/docs/fake.txt",
|
||||
null
|
||||
)).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessage("task path does not match file path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectExtractTaskForDirectory() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createStoredFile(12L, user, "/", "bundle", true, null, 0L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(12L, 7L)).thenReturn(Optional.of(directory));
|
||||
|
||||
assertThatThrownBy(() -> backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.EXTRACT,
|
||||
12L,
|
||||
"/bundle",
|
||||
null
|
||||
)).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessage("task target type is not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMediaMetadataTaskForNonMediaFile() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createStoredFile(13L, user, "/docs", "notes.txt", false, "text/plain", 9L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(13L, 7L)).thenReturn(Optional.of(file));
|
||||
|
||||
assertThatThrownBy(() -> backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.MEDIA_META,
|
||||
13L,
|
||||
"/docs/notes.txt",
|
||||
null
|
||||
)).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessage("media metadata task only supports media files");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateTaskStateFromServerFilePath() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createStoredFile(14L, user, "/docs", "photo.png", false, "image/png", 15L);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(14L, 7L)).thenReturn(Optional.of(file));
|
||||
when(backgroundTaskRepository.save(any(BackgroundTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
BackgroundTask task = backgroundTaskService.createQueuedFileTask(
|
||||
user,
|
||||
BackgroundTaskType.MEDIA_META,
|
||||
14L,
|
||||
"/docs/photo.png",
|
||||
"media-1"
|
||||
);
|
||||
|
||||
assertThat(task.getPublicStateJson()).contains("\"fileId\":14");
|
||||
assertThat(task.getPublicStateJson()).contains("\"path\":\"/docs/photo.png\"");
|
||||
assertThat(task.getPublicStateJson()).contains("\"filename\":\"photo.png\"");
|
||||
assertThat(task.getPublicStateJson()).contains("\"directory\":false");
|
||||
assertThat(task.getPublicStateJson()).contains("\"contentType\":\"image/png\"");
|
||||
assertThat(task.getPublicStateJson()).contains("\"size\":15");
|
||||
assertThat(task.getPrivateStateJson()).contains("\"taskType\":\"MEDIA_META\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClaimQueuedTaskOnlyWhenRepositoryTransitionSucceeds() {
|
||||
BackgroundTask task = createTask(1L, BackgroundTaskStatus.RUNNING);
|
||||
when(backgroundTaskRepository.claimQueuedTask(
|
||||
eq(1L),
|
||||
eq(BackgroundTaskStatus.QUEUED),
|
||||
eq(BackgroundTaskStatus.RUNNING),
|
||||
any()
|
||||
)).thenReturn(1);
|
||||
when(backgroundTaskRepository.findById(1L)).thenReturn(Optional.of(task));
|
||||
|
||||
Optional<BackgroundTask> result = backgroundTaskService.claimQueuedTask(1L);
|
||||
|
||||
assertThat(result).containsSame(task);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotClaimTaskWhenRepositoryTransitionWasSkipped() {
|
||||
when(backgroundTaskRepository.claimQueuedTask(
|
||||
eq(2L),
|
||||
eq(BackgroundTaskStatus.QUEUED),
|
||||
eq(BackgroundTaskStatus.RUNNING),
|
||||
any()
|
||||
)).thenReturn(0);
|
||||
|
||||
Optional<BackgroundTask> result = backgroundTaskService.claimQueuedTask(2L);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteRunningWorkerTaskAndMergePublicState() {
|
||||
BackgroundTask task = createTask(3L, BackgroundTaskStatus.RUNNING);
|
||||
task.setPublicStateJson("{\"fileId\":11}");
|
||||
when(backgroundTaskRepository.findById(3L)).thenReturn(Optional.of(task));
|
||||
when(backgroundTaskRepository.save(any(BackgroundTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
BackgroundTask result = backgroundTaskService.markWorkerTaskCompleted(3L, Map.of("worker", "noop"));
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(BackgroundTaskStatus.COMPLETED);
|
||||
assertThat(result.getFinishedAt()).isNotNull();
|
||||
assertThat(result.getErrorMessage()).isNull();
|
||||
assertThat(result.getPublicStateJson()).contains("\"fileId\":11");
|
||||
assertThat(result.getPublicStateJson()).contains("\"worker\":\"noop\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordWorkerFailureMessage() {
|
||||
BackgroundTask task = createTask(4L, BackgroundTaskStatus.RUNNING);
|
||||
when(backgroundTaskRepository.findById(4L)).thenReturn(Optional.of(task));
|
||||
when(backgroundTaskRepository.save(any(BackgroundTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
BackgroundTask result = backgroundTaskService.markWorkerTaskFailed(4L, "media parser unavailable");
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(BackgroundTaskStatus.FAILED);
|
||||
assertThat(result.getFinishedAt()).isNotNull();
|
||||
assertThat(result.getErrorMessage()).isEqualTo("media parser unavailable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindQueuedTaskIdsInCreatedOrderLimit() {
|
||||
BackgroundTask first = createTask(5L, BackgroundTaskStatus.QUEUED);
|
||||
BackgroundTask second = createTask(6L, BackgroundTaskStatus.QUEUED);
|
||||
when(backgroundTaskRepository.findByStatusOrderByCreatedAtAsc(eq(BackgroundTaskStatus.QUEUED), any()))
|
||||
.thenReturn(List.of(first, second));
|
||||
|
||||
List<Long> result = backgroundTaskService.findQueuedTaskIds(2);
|
||||
|
||||
assertThat(result).containsExactly(5L, 6L);
|
||||
}
|
||||
|
||||
private BackgroundTask createTask(Long id, BackgroundTaskStatus status) {
|
||||
BackgroundTask task = new BackgroundTask();
|
||||
task.setId(id);
|
||||
task.setType(BackgroundTaskType.MEDIA_META);
|
||||
task.setStatus(status);
|
||||
task.setUserId(7L);
|
||||
task.setPublicStateJson("{}");
|
||||
task.setPrivateStateJson("{}");
|
||||
return task;
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername("alice");
|
||||
return user;
|
||||
}
|
||||
|
||||
private StoredFile createStoredFile(Long id,
|
||||
User user,
|
||||
String path,
|
||||
String filename,
|
||||
boolean directory,
|
||||
String contentType,
|
||||
Long size) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(id);
|
||||
file.setUser(user);
|
||||
file.setPath(path);
|
||||
file.setFilename(filename);
|
||||
file.setDirectory(directory);
|
||||
file.setContentType(contentType);
|
||||
file.setSize(size);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
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 java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
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 BackgroundTaskWorkerTest {
|
||||
|
||||
@Mock
|
||||
private BackgroundTaskService backgroundTaskService;
|
||||
@Mock
|
||||
private BackgroundTaskHandler backgroundTaskHandler;
|
||||
|
||||
private BackgroundTaskWorker backgroundTaskWorker;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
backgroundTaskWorker = new BackgroundTaskWorker(backgroundTaskService, List.of(backgroundTaskHandler));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClaimAndCompleteQueuedTaskThroughNoopHandler() {
|
||||
BackgroundTask task = createTask(1L, BackgroundTaskType.ARCHIVE, BackgroundTaskStatus.RUNNING);
|
||||
when(backgroundTaskService.findQueuedTaskIds(5)).thenReturn(List.of(1L));
|
||||
when(backgroundTaskService.claimQueuedTask(1L)).thenReturn(Optional.of(task));
|
||||
when(backgroundTaskHandler.supports(BackgroundTaskType.ARCHIVE)).thenReturn(true);
|
||||
when(backgroundTaskHandler.handle(task)).thenReturn(new BackgroundTaskHandlerResult(Map.of("worker", "noop")));
|
||||
|
||||
int processedCount = backgroundTaskWorker.processQueuedTasks(5);
|
||||
|
||||
assertThat(processedCount).isEqualTo(1);
|
||||
verify(backgroundTaskHandler).handle(task);
|
||||
verify(backgroundTaskService).markWorkerTaskCompleted(1L, Map.of("worker", "noop"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipTaskThatWasNotClaimed() {
|
||||
when(backgroundTaskService.findQueuedTaskIds(5)).thenReturn(List.of(1L));
|
||||
when(backgroundTaskService.claimQueuedTask(1L)).thenReturn(Optional.empty());
|
||||
|
||||
int processedCount = backgroundTaskWorker.processQueuedTasks(5);
|
||||
|
||||
assertThat(processedCount).isZero();
|
||||
verify(backgroundTaskHandler, never()).handle(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkTaskFailedWhenHandlerThrows() {
|
||||
BackgroundTask task = createTask(2L, BackgroundTaskType.MEDIA_META, BackgroundTaskStatus.RUNNING);
|
||||
when(backgroundTaskService.findQueuedTaskIds(5)).thenReturn(List.of(2L));
|
||||
when(backgroundTaskService.claimQueuedTask(2L)).thenReturn(Optional.of(task));
|
||||
when(backgroundTaskHandler.supports(BackgroundTaskType.MEDIA_META)).thenReturn(true);
|
||||
when(backgroundTaskHandler.handle(task)).thenThrow(new IllegalStateException("media parser unavailable"));
|
||||
|
||||
int processedCount = backgroundTaskWorker.processQueuedTasks(5);
|
||||
|
||||
assertThat(processedCount).isEqualTo(1);
|
||||
verify(backgroundTaskService).markWorkerTaskFailed(2L, "media parser unavailable");
|
||||
}
|
||||
|
||||
private BackgroundTask createTask(Long id, BackgroundTaskType type, BackgroundTaskStatus status) {
|
||||
BackgroundTask task = new BackgroundTask();
|
||||
task.setId(id);
|
||||
task.setType(type);
|
||||
task.setStatus(status);
|
||||
task.setUserId(7L);
|
||||
task.setPublicStateJson("{}");
|
||||
task.setPrivateStateJson("{}");
|
||||
return task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
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.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:file_events_service_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-file-events-service"
|
||||
}
|
||||
)
|
||||
class FileEventPersistenceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private FileEventRepository fileEventRepository;
|
||||
|
||||
@MockBean
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
@MockBean
|
||||
private FileBlobRepository fileBlobRepository;
|
||||
|
||||
@MockBean
|
||||
private FileEntityRepository fileEntityRepository;
|
||||
|
||||
@MockBean
|
||||
private StoredFileEntityRepository storedFileEntityRepository;
|
||||
|
||||
@MockBean
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
@MockBean
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
@MockBean
|
||||
private AdminMetricsService adminMetricsService;
|
||||
|
||||
@MockBean
|
||||
private StoragePolicyService storagePolicyService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
fileEventRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistRenameEventWhenFileChanges() {
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
user.setUsername("alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(10L);
|
||||
file.setUser(user);
|
||||
file.setFilename("notes.txt");
|
||||
file.setPath("/docs");
|
||||
file.setContentType("text/plain");
|
||||
file.setSize(5L);
|
||||
file.setDirectory(false);
|
||||
file.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(storedFileRepository.findDetailedById(10L)).thenReturn(Optional.of(file));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "paper.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
FileMetadataResponse response = fileService.rename(user, 10L, "paper.txt");
|
||||
|
||||
assertThat(response.filename()).isEqualTo("paper.txt");
|
||||
assertThat(fileEventRepository.count()).isEqualTo(1L);
|
||||
|
||||
FileEvent event = fileEventRepository.findAll().get(0);
|
||||
assertThat(event.getEventType()).isEqualTo(FileEventType.RENAMED);
|
||||
assertThat(event.getFileId()).isEqualTo(10L);
|
||||
assertThat(event.getFromPath()).isEqualTo("/docs/notes.txt");
|
||||
assertThat(event.getToPath()).isEqualTo("/docs/paper.txt");
|
||||
assertThat(event.getPayloadJson()).contains("\"action\":\"RENAMED\"");
|
||||
assertThat(event.getPayloadJson()).contains("\"filename\":\"paper.txt\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.yoyuzh.api.v2.ApiV2Exception;
|
||||
import com.yoyuzh.auth.User;
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FileSearchServiceTest {
|
||||
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
private FileSearchService fileSearchService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
fileSearchService = new FileSearchService(storedFileRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSearchOwnedActiveFiles() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(10L, user, "/docs", "notes.txt", false);
|
||||
LocalDateTime createdGte = LocalDateTime.of(2026, 4, 8, 8, 0);
|
||||
LocalDateTime createdLte = LocalDateTime.of(2026, 4, 8, 12, 0);
|
||||
LocalDateTime updatedGte = LocalDateTime.of(2026, 4, 8, 9, 0);
|
||||
LocalDateTime updatedLte = LocalDateTime.of(2026, 4, 8, 18, 0);
|
||||
when(storedFileRepository.searchUserFiles(
|
||||
eq(7L),
|
||||
eq("note"),
|
||||
eq(false),
|
||||
eq(1L),
|
||||
eq(100L),
|
||||
eq(createdGte),
|
||||
eq(createdLte),
|
||||
eq(updatedGte),
|
||||
eq(updatedLte),
|
||||
eq(PageRequest.of(0, 20))
|
||||
)).thenReturn(new PageImpl<>(List.of(file), PageRequest.of(0, 20), 1));
|
||||
|
||||
var response = fileSearchService.search(user, new FileSearchQuery(
|
||||
" note ",
|
||||
false,
|
||||
1L,
|
||||
100L,
|
||||
createdGte,
|
||||
createdLte,
|
||||
updatedGte,
|
||||
updatedLte,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
assertThat(response.total()).isEqualTo(1);
|
||||
assertThat(response.items()).hasSize(1);
|
||||
assertThat(response.items().get(0).filename()).isEqualTo("notes.txt");
|
||||
assertThat(response.items().get(0).path()).isEqualTo("/docs");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDirectoryLogicalPathForDirectoryResults() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createFile(11L, user, "/docs", "archive", true);
|
||||
when(storedFileRepository.searchUserFiles(
|
||||
eq(7L),
|
||||
eq(null),
|
||||
eq(true),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(PageRequest.of(0, 20))
|
||||
)).thenReturn(new PageImpl<>(List.of(directory), PageRequest.of(0, 20), 1));
|
||||
|
||||
var response = fileSearchService.search(user, new FileSearchQuery(
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
assertThat(response.items().get(0).path()).isEqualTo("/docs/archive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidSearchRange() {
|
||||
User user = createUser(7L);
|
||||
|
||||
assertThatThrownBy(() -> fileSearchService.search(user, new FileSearchQuery(
|
||||
null,
|
||||
null,
|
||||
100L,
|
||||
1L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
20
|
||||
))).isInstanceOf(ApiV2Exception.class)
|
||||
.hasMessageContaining("文件大小范围不合法");
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername("user-" + id);
|
||||
user.setEmail("user-" + id + "@example.com");
|
||||
return user;
|
||||
}
|
||||
|
||||
private StoredFile createFile(Long id, User user, String path, String filename, boolean directory) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(id);
|
||||
file.setUser(user);
|
||||
file.setFilename(filename);
|
||||
file.setPath(path);
|
||||
file.setContentType(directory ? "directory" : "text/plain");
|
||||
file.setSize(directory ? 0L : 5L);
|
||||
file.setDirectory(directory);
|
||||
file.setCreatedAt(LocalDateTime.of(2026, 4, 8, 10, 0));
|
||||
file.setUpdatedAt(LocalDateTime.of(2026, 4, 8, 11, 0));
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MediaMetadataBackgroundTaskHandlerTest {
|
||||
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
@Mock
|
||||
private FileMetadataRepository fileMetadataRepository;
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
private MediaMetadataBackgroundTaskHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new MediaMetadataBackgroundTaskHandler(
|
||||
storedFileRepository,
|
||||
fileMetadataRepository,
|
||||
fileContentStorage,
|
||||
new ObjectMapper()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractImageMetadataFromPngBlob() throws Exception {
|
||||
BackgroundTask task = createTask(11L);
|
||||
StoredFile file = createFile(11L, false, "image/png", 64L, "blobs/photo.png");
|
||||
byte[] pngBytes = createPngBytes(2, 1);
|
||||
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(11L, 7L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.readBlob("blobs/photo.png")).thenReturn(pngBytes);
|
||||
when(fileMetadataRepository.findByFileIdAndName(11L, "media:contentType")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.findByFileIdAndName(11L, "media:size")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.findByFileIdAndName(11L, "media:width")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.findByFileIdAndName(11L, "media:height")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.save(any(FileMetadata.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
BackgroundTaskHandlerResult result = handler.handle(task);
|
||||
|
||||
assertThat(result.publicStatePatch()).containsEntry("worker", "media-metadata");
|
||||
assertThat(result.publicStatePatch()).containsEntry("metadataExtracted", true);
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaContentType", "image/png");
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaSize", 64L);
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaWidth", 2);
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaHeight", 1);
|
||||
verify(fileContentStorage).readBlob("blobs/photo.png");
|
||||
|
||||
ArgumentCaptor<FileMetadata> captor = ArgumentCaptor.forClass(FileMetadata.class);
|
||||
verify(fileMetadataRepository, times(4)).save(captor.capture());
|
||||
List<FileMetadata> saved = captor.getAllValues();
|
||||
assertThat(saved).extracting(FileMetadata::getName)
|
||||
.containsExactly("media:contentType", "media:size", "media:width", "media:height");
|
||||
assertThat(saved).extracting(FileMetadata::getValue)
|
||||
.containsExactly("image/png", "64", "2", "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWriteBaseMetadataForVideoBlobWithoutDimensions() {
|
||||
BackgroundTask task = createTask(12L);
|
||||
StoredFile file = createFile(12L, false, "video/mp4", 128L, "blobs/movie.mp4");
|
||||
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(12L, 7L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.readBlob("blobs/movie.mp4")).thenReturn(new byte[] {0, 1, 2});
|
||||
when(fileMetadataRepository.findByFileIdAndName(12L, "media:contentType")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.findByFileIdAndName(12L, "media:size")).thenReturn(Optional.empty());
|
||||
when(fileMetadataRepository.save(any(FileMetadata.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
BackgroundTaskHandlerResult result = handler.handle(task);
|
||||
|
||||
assertThat(result.publicStatePatch()).containsEntry("worker", "media-metadata");
|
||||
assertThat(result.publicStatePatch()).containsEntry("metadataExtracted", true);
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaContentType", "video/mp4");
|
||||
assertThat(result.publicStatePatch()).containsEntry("mediaSize", 128L);
|
||||
assertThat(result.publicStatePatch()).doesNotContainKeys("mediaWidth", "mediaHeight");
|
||||
verify(fileMetadataRepository, times(2)).save(any(FileMetadata.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMissingFileDirectoryOrBlob() {
|
||||
BackgroundTask task = createTask(13L);
|
||||
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(13L, 7L)).thenReturn(Optional.empty());
|
||||
assertThatThrownBy(() -> handler.handle(task))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("media metadata task file not found");
|
||||
|
||||
StoredFile directory = createFile(13L, true, null, 0L, null);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(13L, 7L)).thenReturn(Optional.of(directory));
|
||||
assertThatThrownBy(() -> handler.handle(task))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("media metadata task only supports files");
|
||||
|
||||
StoredFile missingBlob = createFile(13L, false, "image/png", 10L, null);
|
||||
when(storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(13L, 7L)).thenReturn(Optional.of(missingBlob));
|
||||
assertThatThrownBy(() -> handler.handle(task))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("media metadata task requires blob");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepNoopHandlerLimitedToArchiveAndExtract() {
|
||||
NoopBackgroundTaskHandler noop = new NoopBackgroundTaskHandler();
|
||||
assertThat(noop.supports(BackgroundTaskType.ARCHIVE)).isTrue();
|
||||
assertThat(noop.supports(BackgroundTaskType.EXTRACT)).isTrue();
|
||||
assertThat(noop.supports(BackgroundTaskType.MEDIA_META)).isFalse();
|
||||
}
|
||||
|
||||
private BackgroundTask createTask(Long fileId) {
|
||||
BackgroundTask task = new BackgroundTask();
|
||||
task.setId(99L);
|
||||
task.setType(BackgroundTaskType.MEDIA_META);
|
||||
task.setStatus(BackgroundTaskStatus.RUNNING);
|
||||
task.setUserId(7L);
|
||||
task.setPublicStateJson("{\"fileId\":" + fileId + "}");
|
||||
task.setPrivateStateJson("{\"fileId\":" + fileId + ",\"taskType\":\"MEDIA_META\"}");
|
||||
return task;
|
||||
}
|
||||
|
||||
private StoredFile createFile(Long id, boolean directory, String contentType, Long size, String objectKey) {
|
||||
StoredFile file = new StoredFile();
|
||||
file.setId(id);
|
||||
file.setDirectory(directory);
|
||||
file.setContentType(contentType);
|
||||
file.setSize(size);
|
||||
if (objectKey != null) {
|
||||
FileBlob blob = new FileBlob();
|
||||
blob.setId(100L);
|
||||
blob.setObjectKey(objectKey);
|
||||
blob.setContentType(contentType);
|
||||
blob.setSize(size);
|
||||
file.setBlob(blob);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private byte[] createPngBytes(int width, int height) throws Exception {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
image.setRGB(0, 0, Color.RED.getRGB());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user