You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
30 KiB

1 month ago
��diff --git a/crm-file/src/test/java/com/crm/file/service/impl/FileApiImplTest.java b/crm-file/src/test/java/com/crm/file/service/impl/FileApiImplTest.java
index 19336cb..f2133dd 100644
--- a/crm-file/src/test/java/com/crm/file/service/impl/FileApiImplTest.java
+++ b/crm-file/src/test/java/com/crm/file/service/impl/FileApiImplTest.java
@@ -10,10 +10,13 @@ import com.crm.file.constant.FileConstants;
import com.crm.file.domain.dto.FileDownloadDTO;
import com.crm.file.domain.dto.FileInfoDTO;
import com.crm.file.domain.dto.MultipartInitDTO;
+import com.crm.file.domain.dto.ThumbnailDTO;
import com.crm.file.domain.dto.UploadSession;
import com.crm.file.domain.entity.FileInfo;
import com.crm.file.service.IFileInfoService;
import com.crm.file.service.KkFileViewClient;
+import com.crm.file.service.ThumbnailPlaceholderService;
+import com.crm.file.task.ThumbnailGenerationTask;
import io.minio.ComposeObjectArgs;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
@@ -76,6 +79,10 @@ class FileApiImplTest {
private ValueOperations<String, Object> valueOperations;
@Mock
private KkFileViewClient kkFileViewClient;
+ @Mock
+ private ThumbnailPlaceholderService thumbnailPlaceholderService;
+ @Mock
+ private ThumbnailGenerationTask thumbnailGenerationTask;
private FileProperties fileProperties;
@@ -87,7 +94,7 @@ class FileApiImplTest {
fileProperties = new FileProperties();
fileProperties.getMinio().setBucket("crm");
fileApi = new FileApiImpl(minioClient, fileProperties, fileInfoService, identifierGenerator,
- redisTemplate, kkFileViewClient);
+ redisTemplate, kkFileViewClient, thumbnailPlaceholderService, thumbnailGenerationTask);
}
private InputStream streamOf(String content) {
@@ -228,6 +235,84 @@ class FileApiImplTest {
e -> assertThat(e.getCode()).isEqualTo(FileConstants.CODE_FILE_SIZE_EXCEEDED));
}
+ /*-------- C%�fe�PcY�/b0}�mAZ6}Z�?PENDING/UNSUPPORTED --------*/
+
+ @Test
+ @DisplayName("�mAZ6}e�gR�X -> isSupported=true -> thumbnail_status=PENDING + ��*a&l�[�P�"��q�W")
+ void upload_image_triggersThumbnailGeneration() throws Exception {
+ when(identifierGenerator.nextId(any())).thenReturn(FILE_ID);
+ when(thumbnailGenerationTask.isSupported("image/png", "png")).thenReturn(true);
+
+ fileApi.upload(streamOf("x"), 1L, "photo.png", "image/png", BIZ_DOMAIN);
+
+ ArgumentCaptor<FileInfo> saveCap = ArgumentCaptor.forClass(FileInfo.class);
+ verify(fileInfoService).save(saveCap.capture());
+ assertThat(saveCap.getValue().getThumbnailStatus()).isEqualTo(FileConstants.THUMBNAIL_STATUS_PENDING);
+ verify(thumbnailGenerationTask).generate(FILE_ID);
+ }
+
+ @Test
+ @DisplayName("�mAZ6} PDF -> isSupported=true -> thumbnail_status=PENDING + ��*a&l�[�P�"��q�W")
+ void upload_pdf_triggersThumbnailGeneration() throws Exception {
+ when(identifierGenerator.nextId(any())).thenReturn(FILE_ID);
+ when(thumbnailGenerationTask.isSupported("application/pdf", "pdf")).thenReturn(true);
+
+ fileApi.upload(streamOf("x"), 1L, "contract.pdf", "application/pdf", BIZ_DOMAIN);
+
+ ArgumentCaptor<FileInfo> saveCap = ArgumentCaptor.forClass(FileInfo.class);
+ verify(fileInfoService).save(saveCap.capture());
+ assertThat(saveCap.getValue().getThumbnailStatus()).isEqualTo(FileConstants.THUMBNAIL_STATUS_PENDING);
+ verify(thumbnailGenerationTask).generate(FILE_ID);
+ }
+
+ @Test
+ @DisplayName("�mAZ6} Office�IWocx�?> isSupported=true -> thumbnail_status=PENDING + ��*a&l�[�P�"��q�W")
+ void upload_office_triggersThumbnailGeneration() throws Exception {
+ when(identifierGenerator.nextId(any())).thenReturn(FILE_ID);
+ when(thumbnailGenerationTask.isSupported(any(), org.mockito.ArgumentMatchers.eq("docx"))).thenReturn(true);
+
+ fileApi.upload(streamOf("x"), 1L, "report.docx",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document", BIZ_DOMAIN);
+
+ ArgumentCaptor<FileInfo> saveCap = ArgumentCaptor.forClass(FileInfo.class);
+ verify(fileInfoService).save(saveCap.capture());
+ assertThat(saveCap.getValue().getThumbnailStatus()).isEqualTo(FileConstants.THUMBNAIL_STATUS_PENDING);
+ verify(thumbnailGenerationTask).generate(FILE_ID);
+ }
+
+ @Test
+ @DisplayName("�mAZ6}ȕ�pXm��V}txt�?> isSupported=false -> thumbnail_status=UNSUPPORTED + �m�]A_\m�0}�Y�0Se��?)
+ void upload_nonImage_marksUnsupported() throws Exception {
+ when(identifierGenerator.nextId(any())).thenReturn(FILE_ID);
+
+ fileApi.upload(streamOf("x"), 1L, "data.txt", null, BIZ_DOMAIN);
+
+ ArgumentCaptor<FileInfo> saveCap = ArgumentCaptor.forClass(FileInfo.class);
+ verify(fileInfoService).save(saveCap.capture());
+ assertThat(saveCap.getValue().getThumbnailStatus()).isEqualTo(FileConstants.THUMBNAIL_STATUS_UNSUPPORTED);
+ verify(thumbnailGenerationTask, never()).generate(anyLong());
+ }
+
+ @Test
+ @DisplayName("R��U�XZ�X߂e�gR�X -> isSupported=true -> thumbnail_status=PENDING + ��*a&l�[�P�"��q�W")
+ void completeMultipart_image_triggersThumbnailGeneration() throws Exception {
+ UploadSession session = buildSession();
+ session.setOriginalName("photo.jpg");
+ when(redisTemplate.opsForValue()).thenReturn(valueOperations);
+ when(valueOperations.get(SESSION_KEY)).thenReturn(session);
+ stubUploadedChunks("chunks/" + UPLOAD_ID + "/1", "chunks/" + UPLOAD_ID + "/2", "chunks/" + UPLOAD_ID + "/3");
+ when(identifierGenerator.nextId(any())).thenReturn(FILE_ID);
+ when(minioClient.removeObjects(any(RemoveObjectsArgs.class))).thenReturn(List.of());
+ when(thumbnailGenerationTask.isSupported(any(), any())).thenReturn(true);
+
+ fileApi.completeMultipart(UPLOAD_ID);
+
+ ArgumentCaptor<FileInfo> saveCap = ArgumentCaptor.forClass(FileInfo.class);
+ verify(fileInfoService).save(saveCap.capture());
+ assertThat(saveCap.getValue().getThumbnailStatus()).isEqualTo(FileConstants.THUMBNAIL_STATUS_PENDING);
+ verify(thumbnailGenerationTask).generate(FILE_ID);
+ }
+
/*-------- getInfo�3l�ᯓnTaq�t?--------*/
@Test
@@ -396,6 +481,180 @@ class FileApiImplTest {
verify(minioClient, never()).getPresignedObjectUrl(any(GetPresignedObjectUrlArgs.class));
}
+ /*-------- getThumbnail�1l #��0Xm�tf�w�[?--------*/
+
+ private static final byte[] THUMB_BYTES = "thumbnail-data".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] PLACEHOLDER_BYTES = "placeholder-data".getBytes(StandardCharsets.UTF_8);
+
+ @Test
+ @DisplayName("READY �5�� ?-> `m?MinIO �� Y #��0Xm��\Q~e�?image/jpeg + cacheable=true")
+ void getThumbnail_ready_returnsFromMinio() throws Exception {
+ FileInfo entity = new FileInfo();
+ entity.setId(FILE_ID);
+ entity.setOriginalName("photo.jpg");
+ entity.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_READY);
+ when(fileInfoService.getById(FILE_ID)).thenReturn(entity);
+ GetObjectResponse mockStream = mock(GetObjectResponse.class);
+ when(mockStream.readAllBytes()).thenReturn(THUMB_BYTES);
+ when(minioClient.getObject(any(GetObjectArgs.class))).thenReturn(mockStream);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ ArgumentCaptor<GetObjectArgs> getCap = ArgumentCaptor.forClass(GetObjectArgs.class);
+ verify(minioClient).getObject(getCap.capture());
+ assertThat(getCap.getValue().bucket()).isEqualTo("crm");
+ assertThat(getCap.getValue().object()).isEqualTo(FileConstants.THUMBNAIL_PREFIX + FILE_ID + ".jpg");
+ assertThat(dto.getContent()).isEqualTo(THUMB_BYTES);
+ assertThat(dto.getContentType()).isEqualTo("image/jpeg");
+ assertThat(dto.isCacheable()).isTrue();
+ verify(thumbnailPlaceholderService, never()).getPlaceholder(any());
+ }
+
+ @Test
+ @DisplayName("UNSUPPORTED �5�� ?-> ig�emW��r�}e�?+ cacheable=false�|\ {Yt>�k� MinIO C%�fe�Pc�w�[?)
+ void getThumbnail_unsupported_returnsPlaceholder() {
+ FileInfo entity = new FileInfo();
+ entity.setId(FILE_ID);
+ entity.setOriginalName("data.txt");
+ entity.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_UNSUPPORTED);
+ when(fileInfoService.getById(FILE_ID)).thenReturn(entity);
+ when(thumbnailPlaceholderService.getPlaceholder("txt")).thenReturn(PLACEHOLDER_BYTES);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(PLACEHOLDER_BYTES);
+ assertThat(dto.getContentType()).isEqualTo("image/png");
+ assertThat(dto.isCacheable()).isFalse();
+ verifyNoInteractions(minioClient);
+ }
+
+ @Test
+ @DisplayName("FAILED �5�� ?-> ig�emW��r�}e�?+ cacheable=false")
+ void getThumbnail_failed_returnsPlaceholder() {
+ FileInfo entity = new FileInfo();
+ entity.setId(FILE_ID);
+ entity.setOriginalName("report.pdf");
+ entity.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_FAILED);
+ when(fileInfoService.getById(FILE_ID)).thenReturn(entity);
+ when(thumbnailPlaceholderService.getPlaceholder("pdf")).thenReturn(PLACEHOLDER_BYTES);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(PLACEHOLDER_BYTES);
+ assertThat(dto.getContentType()).isEqualTo("image/png");
+ assertThat(dto.isCacheable()).isFalse();
+ verifyNoInteractions(minioClient);
+ }
+
+ @Test
+ @DisplayName("PENDING + ~��p#e��,a�YZ�~\�"��q�W -> ���]aq READY -> ig�em*��q #��0Xm + cacheable=true")
+ void getThumbnail_pending_syncFallback_generatesAndReturns() throws Exception {
+ FileInfo pending = new FileInfo();
+ pending.setId(FILE_ID);
+ pending.setOriginalName("photo.jpg");
+ pending.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_PENDING);
+ FileInfo ready = new FileInfo();
+ ready.setId(FILE_ID);
+ ready.setOriginalName("photo.jpg");
+ ready.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_READY);
+ // �h(h��̓?PENDING�}\�`�Y�0Se��,a�`���]aq READY
+ when(fileInfoService.getById(FILE_ID)).thenReturn(pending, ready);
+ when(thumbnailGenerationTask.tryGenerateSync(FILE_ID)).thenReturn(true);
+ GetObjectResponse mockStream = mock(GetObjectResponse.class);
+ when(mockStream.readAllBytes()).thenReturn(THUMB_BYTES);
+ when(minioClient.getObject(any(GetObjectArgs.class))).thenReturn(mockStream);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(THUMB_BYTES);
+ assertThat(dto.getContentType()).isEqualTo("image/jpeg");
+ assertThat(dto.isCacheable()).isTrue();
+ assertThat(dto.getStatusCode()).isEqualTo(200);
+ verify(thumbnailGenerationTask).tryGenerateSync(FILE_ID);
+ }
+
+ @Test
+ @DisplayName("PENDING + ~��p#e�o���� + ^g�����tnTi`m?PENDING -> W��r�}e�?+ no-cache + statusCode=202")
+ void getThumbnail_pending_lockFailed_timeoutReturns202() {
+ fileProperties.getThumbnail().setSyncWaitTimeout(java.time.Duration.ofMillis(100));
+ FileInfo entity = new FileInfo();
+ entity.setId(FILE_ID);
+ entity.setOriginalName("photo.jpg");
+ entity.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_PENDING);
+ when(fileInfoService.getById(FILE_ID)).thenReturn(entity);
+ when(thumbnailGenerationTask.tryGenerateSync(FILE_ID)).thenReturn(false);
+ when(thumbnailPlaceholderService.getPlaceholder("jpg")).thenReturn(PLACEHOLDER_BYTES);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(PLACEHOLDER_BYTES);
+ assertThat(dto.getContentType()).isEqualTo("image/png");
+ assertThat(dto.isCacheable()).isFalse();
+ assertThat(dto.getStatusCode()).isEqualTo(202);
+ }
+
+ @Test
+ @DisplayName("PENDING + ~��p#e�o���� + �~
Y�}ȓ�q�h�[�P�`m���Y9p~\�W�'WEADY�?> ig�em*��q #��0Xm")
+ void getThumbnail_pending_lockFailed_readyDuringWait_returnsThumbnail() throws Exception {
+ fileProperties.getThumbnail().setSyncWaitTimeout(java.time.Duration.ofSeconds(5));
+ FileInfo pending = new FileInfo();
+ pending.setId(FILE_ID);
+ pending.setOriginalName("photo.jpg");
+ pending.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_PENDING);
+ FileInfo ready = new FileInfo();
+ ready.setId(FILE_ID);
+ ready.setOriginalName("photo.jpg");
+ ready.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_READY);
+ // �h(haq PENDING��\�u�t!2i�[�P�`m���Y�[�clu��,aI_ READY
+ when(fileInfoService.getById(FILE_ID)).thenReturn(pending, ready);
+ when(thumbnailGenerationTask.tryGenerateSync(FILE_ID)).thenReturn(false);
+ GetObjectResponse mockStream = mock(GetObjectResponse.class);
+ when(mockStream.readAllBytes()).thenReturn(THUMB_BYTES);
+ when(minioClient.getObject(any(GetObjectArgs.class))).thenReturn(mockStream);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(THUMB_BYTES);
+ assertThat(dto.isCacheable()).isTrue();
+ assertThat(dto.getStatusCode()).isEqualTo(200);
+ }
+
+ @Test
+ @DisplayName("PENDING + ~��p#e��,a�Ycm�USe��,a�``m�]jo READY�"XSe��,aQ0�t�0}-> W��r�}e�?+ 200")
+ void getThumbnail_pending_syncGenerateFailed_returnsPlaceholder() {
+ FileInfo entity = new FileInfo();
+ entity.setId(FILE_ID);
+ entity.setOriginalName("photo.jpg");
+ entity.setThumbnailStatus(FileConstants.THUMBNAIL_STATUS_PENDING);
+ when(fileInfoService.getById(FILE_ID)).thenReturn(entity);
+ when(thumbnailGenerationTask.tryGenerateSync(FILE_ID)).thenReturn(true);
+ when(thumbnailPlaceholderService.getPlaceholder("jpg")).thenReturn(PLACEHOLDER_BYTES);
+
+ ThumbnailDTO dto = fileApi.getThumbnail(String.valueOf(FILE_ID));
+
+ assertThat(dto.getContent()).isEqualTo(PLACEHOLDER_BYTES);
+ assertThat(dto.isCacheable()).isFalse();
+ assertThat(dto.getStatusCode()).isEqualTo(200);
+ verifyNoInteractions(minioClient);
+ }
+
+ @Test
+ @DisplayName("C%�fe��paqÓ�r}“�V"k�m�]�tf���} -> 40401")
+ void getThumbnail_notFound_throwsResourceNotExist() {
+ when(fileInfoService.getById(FILE_ID)).thenReturn(null);
+ assertThatThrownBy(() -> fileApi.getThumbnail(String.valueOf(FILE_ID)))
+ .isInstanceOf(ResourceNotExistException.class);
+ verifyNoInteractions(minioClient, thumbnailPlaceholderService);
+ }
+
+ @Test
+ @DisplayName("C%�fe�?fileId ȕ�pvx͓Nq!} -> 40401")
+ void getThumbnail_malformedFileId_throwsResourceNotExist() {
+ assertThatThrownBy(() -> fileApi.getThumbnail("not-a-number"))
+ .isInstanceOf(ResourceNotExistException.class);
+ verify(fileInfoService, never()).getById(anyLong());
+ }
+
/*-------- R��U�X�mAZ6}�m Y���[?--------*/
private static final String UPLOAD_ID = "u-0001";