test(media): 覆盖转码产物查询接口

This commit is contained in:
Zhou Weidong committed 2026-10-22 14:10:15 +08:00
1 parent f66eae5baf
commit 090f149b31
10 files changed
+625

No files matched your search

+5
View File
@@ -25,5 +25,10 @@
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-system</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,213 @@
// Author: fruit.com.cn
package com.fruitshorts.media.service;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.media.dal.entity.MediaAsset;
import com.fruitshorts.media.dal.entity.MediaRendition;
import com.fruitshorts.media.dal.entity.MediaThumbnail;
import com.fruitshorts.media.dal.entity.TranscodeTask;
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
class MediaTranscodeOutputServiceTest {
private static final Long MEDIA_ASSET_ID = 88L;
private static final Long EPISODE_ID = 31L;
@TempDir
Path tempDir;
private MediaAssetMapper mediaAssetMapper;
private TranscodeService transcodeService;
private MediaStorageService storageService;
private PlaybackSigner signer;
private MediaTranscodeOutputService service;
@BeforeEach
void setUp() {
mediaAssetMapper = mock(MediaAssetMapper.class);
transcodeService = mock(TranscodeService.class);
storageService = mock(MediaStorageService.class);
signer = mock(PlaybackSigner.class);
service = new MediaTranscodeOutputService(
mediaAssetMapper, transcodeService, storageService, signer);
given(signer.sign(anyString())).willAnswer(invocation -> invocation.getArgument(0));
}
@Test
void should_return_renditions_sorted_from_highest_to_lowest() {
givenMedia();
given(transcodeService.listRenditionsByMediaAssetId(MEDIA_ASSET_ID))
.willReturn(List.of(
rendition(1L, "360p", 640, 360, 400),
rendition(2L, "1080p", 1920, 1080, 5000),
rendition(3L, "720p", 1280, 720, 2500)));
var result = service.renditions(MEDIA_ASSET_ID);
assertThat(result).extracting(vo -> vo.getQuality())
.containsExactly("1080p", "720p", "360p");
assertThat(result.get(0).getQualityName()).isEqualTo("1080P");
assertThat(result.get(0).getResolution()).isEqualTo("1920x1080");
assertThat(result.get(0).getVideoBitrateKbps()).isEqualTo(5000);
assertThat(result.get(0).getVideoCodec()).isEqualTo("H.264");
assertThat(result.get(0).getAudioCodec()).isEqualTo("AAC");
assertThat(result.get(0).getContainer()).isEqualTo("HLS");
assertThat(result.get(0).getHlsUrl()).isEqualTo("/files/master.m3u8");
assertThat(result.get(0).getSizeBytes()).isEqualTo(4096L);
assertThat(result.get(0).getStatus()).isEqualTo("ready");
}
@Test
void should_return_not_found_when_media_does_not_exist() {
given(mediaAssetMapper.selectById(404L)).willReturn(null);
assertThatThrownBy(() -> service.renditions(404L))
.isInstanceOf(ServiceException.class)
.hasMessage("媒资不存在");
}
@Test
void should_return_empty_thumbnail_status_when_product_is_missing() {
givenMedia();
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
.willReturn(task(EPISODE_ID));
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID)).willReturn(null);
var result = service.thumbnails(MEDIA_ASSET_ID, 1, 100);
assertThat(result.getStatus()).isEqualTo("EMPTY");
assertThat(result.getFrames().getTotal()).isZero();
assertThat(result.getFrames().getList()).isEmpty();
assertThat(result.getSprite()).isNull();
}
@Test
void should_page_vtt_frames_and_expose_sprite_metadata() throws Exception {
givenMedia();
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
.willReturn(task(EPISODE_ID));
MediaThumbnail thumbnail = thumbnail();
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID)).willReturn(thumbnail);
writeVtt("""
WEBVTT
00:00:00.000 --> 00:00:01.000
sprite_001.jpg#xywh=0,0,160,90
00:00:01.000 --> 00:00:02.000
sprite_001.jpg#xywh=160,0,160,90
00:00:02.000 --> 00:00:03.000
sprite_001.jpg#xywh=320,0,160,90
00:00:03.000 --> 00:00:04.000
sprite_002.jpg#xywh=0,0,160,90
""");
given(storageService.fileFor("transcoded/8/thumbnails/thumbnails.vtt"))
.willReturn(tempDir.resolve("thumbnails.vtt"));
given(storageService.toPublicUrl("transcoded/8/thumbnails/thumbnails.vtt"))
.willReturn("/files/transcoded/8/thumbnails/thumbnails.vtt");
given(storageService.toPublicUrl("transcoded/8/thumbnails/sprite_001.jpg"))
.willReturn("/files/transcoded/8/thumbnails/sprite_001.jpg");
given(storageService.toPublicUrl("transcoded/8/thumbnails/sprite_002.jpg"))
.willReturn("/files/transcoded/8/thumbnails/sprite_002.jpg");
var result = service.thumbnails(MEDIA_ASSET_ID, 2, 2);
assertThat(result.getStatus()).isEqualTo("READY");
assertThat(result.getPageIndex()).isEqualTo(2);
assertThat(result.getPageSize()).isEqualTo(2);
assertThat(result.getFrames().getTotal()).isEqualTo(4);
assertThat(result.getFrames().getList()).hasSize(2);
assertThat(result.getFrames().getList().get(0).getFrameIndex()).isEqualTo(2);
assertThat(result.getFrames().getList().get(0).getTimeMs()).isEqualTo(2000);
assertThat(result.getFrames().getList().get(0).getImageUrl())
.isEqualTo("/files/transcoded/8/thumbnails/sprite_001.jpg#xywh=320,0,160,90");
assertThat(result.getSprite().getColumns()).isEqualTo(2);
assertThat(result.getSprite().getRows()).isEqualTo(2);
assertThat(result.getSprite().getTotalFrames()).isEqualTo(4);
assertThat(result.getSprite().getThumbWidth()).isEqualTo(160);
assertThat(result.getSprite().getThumbHeight()).isEqualTo(90);
assertThat(result.getSprite().getSpriteUrls()).hasSize(2);
assertThat(result.getSprite().getVttUrl())
.isEqualTo("/files/transcoded/8/thumbnails/thumbnails.vtt");
}
@Test
void should_clamp_thumbnail_page_size_to_five_hundred() {
givenMedia();
given(transcodeService.findLatestTaskByMediaAssetId(MEDIA_ASSET_ID))
.willReturn(task(EPISODE_ID));
given(transcodeService.findThumbnailByEpisodeId(EPISODE_ID))
.willReturn(thumbnail());
var result = service.thumbnails(MEDIA_ASSET_ID, 1, 501);
assertThat(result.getPageIndex()).isEqualTo(1);
assertThat(result.getPageSize()).isEqualTo(500);
}
private void givenMedia() {
MediaAsset asset = new MediaAsset();
asset.setId(MEDIA_ASSET_ID);
asset.setFileName("video.mp4");
given(mediaAssetMapper.selectById(MEDIA_ASSET_ID)).willReturn(asset);
}
private MediaRendition rendition(Long id, String quality, int width, int height, int bitrate) {
MediaRendition rendition = new MediaRendition();
rendition.setId(id);
rendition.setQuality(quality);
rendition.setWidth(width);
rendition.setHeight(height);
rendition.setVideoBitrate(bitrate);
rendition.setAudioBitrate(64);
rendition.setFormat("HLS");
rendition.setPlayUrl("/files/master.m3u8");
rendition.setSizeBytes(4096L);
rendition.setStatus("ready");
return rendition;
}
private TranscodeTask task(Long episodeId) {
TranscodeTask task = new TranscodeTask();
task.setId(8L);
task.setMediaAssetId(MEDIA_ASSET_ID);
task.setEpisodeId(episodeId);
task.setStatus(TranscodeService.STATUS_SUCCESS);
return task;
}
private MediaThumbnail thumbnail() {
MediaThumbnail thumbnail = new MediaThumbnail();
thumbnail.setEpisodeId(EPISODE_ID);
thumbnail.setVttObjectKey("transcoded/8/thumbnails/thumbnails.vtt");
thumbnail.setSpritePrefix("transcoded/8/thumbnails/sprite_");
thumbnail.setColumns(2);
thumbnail.setRows(2);
thumbnail.setThumbWidth(160);
thumbnail.setThumbHeight(90);
thumbnail.setFrameCount(4);
thumbnail.setIntervalSec(1);
thumbnail.setStatus(MediaThumbnail.STATUS_READY);
return thumbnail;
}
private void writeVtt(String content) throws Exception {
Files.writeString(tempDir.resolve("thumbnails.vtt"), content);
}
}
@@ -0,0 +1,44 @@
// Author: fruit.com.cn
package com.fruitshorts.media.service;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ThumbnailVttParserTest {
@Test
void should_parse_timestamps_and_sprite_rectangles() {
String vtt = """
WEBVTT
00:00:00.000 --> 00:00:01.000
sprite_001.jpg#xywh=0,0,160,90
00:00:01.500 --> 00:00:02.500
sprite_002.jpg#xywh=160,90,160,90
""";
List<ThumbnailVttParser.Cue> cues = ThumbnailVttParser.parse(vtt);
assertThat(cues).hasSize(2);
assertThat(cues.get(0).startMs()).isZero();
assertThat(cues.get(0).endMs()).isEqualTo(1000);
assertThat(cues.get(0).spriteFile()).isEqualTo("sprite_001.jpg");
assertThat(cues.get(1).startMs()).isEqualTo(1500);
assertThat(cues.get(1).spriteFile()).isEqualTo("sprite_002.jpg");
assertThat(cues.get(1).x()).isEqualTo(160);
assertThat(cues.get(1).y()).isEqualTo(90);
assertThat(cues.get(1).width()).isEqualTo(160);
assertThat(cues.get(1).height()).isEqualTo(90);
}
@Test
void should_return_empty_list_when_vtt_is_invalid() {
assertThat(ThumbnailVttParser.parse("not a vtt")).isEmpty();
assertThat(ThumbnailVttParser.parse(null)).isEmpty();
}
}
@@ -0,0 +1,53 @@
// Author: fruit.com.cn
package com.fruitshorts.media.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fruitshorts.media.dal.entity.MediaRendition;
import com.fruitshorts.media.dal.entity.MediaThumbnail;
import com.fruitshorts.media.dal.entity.TranscodeTask;
import com.fruitshorts.media.dal.mapper.MediaRenditionMapper;
import com.fruitshorts.media.dal.mapper.MediaThumbnailMapper;
import com.fruitshorts.media.dal.mapper.TranscodeTaskMapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
class TranscodeQueueStatsTest {
@BeforeAll
static void initTableInfoCache() {
MapperBuilderAssistant assistant =
new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(assistant, TranscodeTask.class);
TableInfoHelper.initTableInfo(assistant, MediaRendition.class);
TableInfoHelper.initTableInfo(assistant, MediaThumbnail.class);
}
@Test
void should_return_typed_total_and_status_counts() {
TranscodeTaskMapper taskMapper = mock(TranscodeTaskMapper.class);
MediaRenditionMapper renditionMapper = mock(MediaRenditionMapper.class);
given(taskMapper.selectCount(any())).willReturn(10L, 2L, 1L, 5L, 1L, 1L);
given(renditionMapper.selectCount(null)).willReturn(20L);
TranscodeService service = new TranscodeService(
taskMapper, renditionMapper, mock(MediaThumbnailMapper.class),
mock(ThumbnailRecordService.class));
var stats = service.stats();
assertThat(stats.getTotal()).isEqualTo(10L);
assertThat(stats.getQueued()).isEqualTo(2L);
assertThat(stats.getRunning()).isEqualTo(1L);
assertThat(stats.getSuccess()).isEqualTo(5L);
assertThat(stats.getFailed()).isEqualTo(1L);
assertThat(stats.getCanceled()).isEqualTo(1L);
assertThat(stats.getRenditions()).isEqualTo(20L);
}
}