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 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)); } + /*-------- 缂╃暐鍥捐Е鍙戯細涓婁紶鍚?PENDING/UNSUPPORTED --------*/ + + @Test + @DisplayName("涓婁紶鍥剧墖 -> isSupported=true -> thumbnail_status=PENDING + 鎻愪氦寮傛鐢熸垚") + 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 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("涓婁紶 PDF -> isSupported=true -> thumbnail_status=PENDING + 鎻愪氦寮傛鐢熸垚") + 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 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("涓婁紶 Office锛坉ocx锛?> isSupported=true -> thumbnail_status=PENDING + 鎻愪氦寮傛鐢熸垚") + 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 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("涓婁紶闈炲浘鐗囷紙txt锛?> isSupported=false -> thumbnail_status=UNSUPPORTED + 涓嶆彁浜ゅ紓姝ョ敓鎴?) + void upload_nonImage_marksUnsupported() throws Exception { + when(identifierGenerator.nextId(any())).thenReturn(FILE_ID); + + fileApi.upload(streamOf("x"), 1L, "data.txt", null, BIZ_DOMAIN); + + ArgumentCaptor 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("鍒嗙墖鍚堝苟鍥剧墖 -> isSupported=true -> thumbnail_status=PENDING + 鎻愪氦寮傛鐢熸垚") + 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 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锛氳鎯呮煡璇?--------*/ @Test @@ -396,6 +481,180 @@ class FileApiImplTest { verify(minioClient, never()).getPresignedObjectUrl(any(GetPresignedObjectUrlArgs.class)); } + /*-------- getThumbnail锛氱缉鐣ュ浘璇昏矾寰?--------*/ + + 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 鐘舵€?-> 浠?MinIO 鎷夌缉鐣ュ浘锛岃繑鍥?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 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 鐘舵€?-> 杩斿洖鍗犱綅鍥?+ cacheable=false锛屼笉瑙︾ MinIO 缂╃暐鍥捐矾寰?) + 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 鐘舵€?-> 杩斿洖鍗犱綅鍥?+ 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 + 鑾烽攣鎴愬姛鍚屾鐢熸垚 -> 閲嶆煡 READY -> 杩斿洖鐪熺缉鐣ュ浘 + 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); + // 棣栨鏌?PENDING锛屽悓姝ョ敓鎴愬悗閲嶆煡 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 + 鑾烽攣澶辫触 + 杞瓒呮椂浠?PENDING -> 鍗犱綅鍥?+ 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 + 鑾烽攣澶辫触 + 绛夊緟鏈熼棿寮傛浠诲姟瀹屾垚锛圧EADY锛?> 杩斿洖鐪熺缉鐣ュ浘") + 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); + // 棣栨煡 PENDING锛岃疆璇㈡椂寮傛浠诲姟宸插畬鎴愬彉 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 + 鑾烽攣鎴愬姛浣嗙敓鎴愬悗浠嶉潪 READY锛堢敓鎴愬け璐ワ級-> 鍗犱綅鍥?+ 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("缂╃暐鍥炬煡鏃狅紙鏂囦欢涓嶅瓨鍦級 -> 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("缂╃暐鍥?fileId 闈炴硶鏍煎紡 -> 40401") + void getThumbnail_malformedFileId_throwsResourceNotExist() { + assertThatThrownBy(() -> fileApi.getThumbnail("not-a-number")) + .isInstanceOf(ResourceNotExistException.class); + verify(fileInfoService, never()).getById(anyLong()); + } + /*-------- 鍒嗙墖涓婁紶涓夋寮?--------*/ private static final String UPLOAD_ID = "u-0001";