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

@@ -0,0 +1,28 @@
# 转码产物查询接口
## 目标
- 媒体维度查询 rendition 与帧图产物。
- 分集维度聚合转码任务、rendition、帧图状态。
- `/admin/dashboard/transcode` 改为明确 VO,OpenAPI 不再出现匿名 Object。
## 设计
1. `MediaTranscodeOutputService` 统一读取媒体、任务、rendition 与 thumbnail。
2. rendition 按高度、视频码率、质量倒序,id 正序保证稳定。
3. thumbnail 优先读取 `thumbnails.vtt`,解析 cue 后分页;无记录返回 `EMPTY`,
记录失败返回 `FAILED`,不把缺产物当服务错误。
4. 分集聚合对不存在、无视频、无媒资、无任务返回明确 status 和空列表。
5. dashboard 使用强类型 queue/failed task VO,保留现有字段语义并补 total。
## TDD
- 先提交媒体有/无产物、不存在、VTT 解析与分页边界测试。
- 再提交接口、VO、聚合服务和 dashboard 类型收敛实现。
- 门禁:`mvn -pl fruitshorts-module-media test`。
## 红线
- 不改 `/app` 契约、specs、ReelShortWeb、Flutter。
- 不改 P1 入队与手动重转逻辑。
- 不启停服务,不操作真机。
+25
View File
@@ -25314,3 +25314,28 @@ $ mvn -pl fruitshorts-admin-api -am test
后台控制器改动连同既有 admin 契约、权限、服务测试全部通过。
未启停服务、未执行数据库迁移、未操作真机。
## 2026-10-22 转码产物查询接口 P2
- 分支:`feature/media-transcode-output-api`
- 基线:`develop` / `origin/develop` = `f66eae5`
- 隔离工作树:`/private/tmp/fruitserver-media-output-api`
- 计划:`docs/plans/2026-10-22-media-transcode-output-api.md`
### TDD 红测
先补媒体清晰度、帧图分页、VTT 解析、后台聚合与 dashboard 强类型契约测试,
生产代码保持未实现。
```text
$ mvn -pl fruitshorts-module-media test
[INFO] Compiling 3 source files with javac [debug release 17] to target/test-classes
[ERROR] MediaTranscodeOutputServiceTest.java:[37,13] 找不到符号
符号: 类 MediaTranscodeOutputService
[INFO] fruitshorts-module-media ........................... FAILURE
[INFO] BUILD FAILURE
```
解读:失败原因正是查询服务尚未实现。`fruitshorts-module-media` 新增
`spring-boot-starter-test` test scope,使指定门禁能实际执行本次单元测试。
下一步实现 VTT 解析、稳定排序、分页和 admin 聚合 VO 后转绿。
@@ -0,0 +1,52 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.service.DashboardService;
import com.fruitshorts.admin.support.ControllerContractSupport;
import com.fruitshorts.admin.vo.TranscodeDashboardRespVO;
import com.fruitshorts.media.vo.TranscodeQueueStatsVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
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 DashboardTranscodeContractTest {
private DashboardService dashboardService;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
dashboardService = mock(DashboardService.class);
mockMvc = ControllerContractSupport.mockMvcOf(
new DashboardAdminController(dashboardService));
}
@Test
void should_return_typed_queue_and_failed_tasks() throws Exception {
TranscodeQueueStatsVO queue = new TranscodeQueueStatsVO(
10L, 2L, 1L, 5L, 1L, 1L, 20L);
TranscodeDashboardRespVO vo = new TranscodeDashboardRespVO();
vo.setQueue(queue);
vo.setFailedTasks(List.of());
vo.setNotes(List.of("queue total"));
given(dashboardService.transcode()).willReturn(vo);
mockMvc.perform(get("/admin/dashboard/transcode"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.queue.total").value(10))
.andExpect(jsonPath("$.data.queue.queued").value(2))
.andExpect(jsonPath("$.data.queue.running").value(1))
.andExpect(jsonPath("$.data.queue.success").value(5))
.andExpect(jsonPath("$.data.queue.failed").value(1))
.andExpect(jsonPath("$.data.failedTasks").isArray());
}
}
@@ -0,0 +1,61 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.service.EpisodeTranscodeAdminService;
import com.fruitshorts.admin.support.ControllerContractSupport;
import com.fruitshorts.admin.vo.EpisodeTranscodeRespVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
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 EpisodeTranscodeContractTest {
private EpisodeTranscodeAdminService service;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
service = mock(EpisodeTranscodeAdminService.class);
mockMvc = ControllerContractSupport.mockMvcOf(
new EpisodeTranscodeController(service));
}
@Test
void should_return_aggregated_episode_transcode_state() throws Exception {
EpisodeTranscodeRespVO vo = new EpisodeTranscodeRespVO();
vo.setEpisodeId(31L);
vo.setMediaAssetId(88L);
vo.setTaskId(8L);
vo.setStatus("SUCCESS");
vo.setProgress(100);
given(service.detail(31L)).willReturn(vo);
mockMvc.perform(get("/admin/episodes/31/transcode"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.episodeId").value("31"))
.andExpect(jsonPath("$.data.mediaAssetId").value("88"))
.andExpect(jsonPath("$.data.status").value("SUCCESS"))
.andExpect(jsonPath("$.data.progress").value(100));
}
@Test
void should_return_explicit_empty_state_when_episode_has_no_video() throws Exception {
EpisodeTranscodeRespVO vo = new EpisodeTranscodeRespVO();
vo.setEpisodeId(31L);
vo.setStatus("NO_VIDEO");
given(service.detail(31L)).willReturn(vo);
mockMvc.perform(get("/admin/episodes/31/transcode"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("NO_VIDEO"))
.andExpect(jsonPath("$.data.renditions").isEmpty());
}
}
@@ -0,0 +1,90 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.support.ControllerContractSupport;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.media.service.MediaTranscodeOutputService;
import com.fruitshorts.media.vo.MediaRenditionRespVO;
import com.fruitshorts.media.vo.MediaThumbnailRespVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
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 MediaTranscodeOutputContractTest {
private MediaTranscodeOutputService outputService;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
outputService = mock(MediaTranscodeOutputService.class);
mockMvc = ControllerContractSupport.mockMvcOf(
new MediaTranscodeOutputController(outputService));
}
@Test
void should_return_typed_rendition_list() throws Exception {
MediaRenditionRespVO vo = new MediaRenditionRespVO();
vo.setQuality("1080p");
vo.setQualityName("1080P");
vo.setResolution("1920x1080");
vo.setVideoBitrateKbps(5000);
vo.setVideoCodec("H.264");
vo.setAudioCodec("AAC");
vo.setContainer("HLS");
vo.setHlsUrl("/files/master.m3u8");
vo.setSizeBytes(4096L);
vo.setStatus("ready");
given(outputService.renditions(88L)).willReturn(List.of(vo));
mockMvc.perform(get("/admin/media/88/renditions"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data[0].quality").value("1080p"))
.andExpect(jsonPath("$.data[0].resolution").value("1920x1080"))
.andExpect(jsonPath("$.data[0].videoBitrateKbps").value(5000))
.andExpect(jsonPath("$.data[0].hlsUrl").value("/files/master.m3u8"));
}
@Test
void should_return_not_found_for_missing_media() throws Exception {
given(outputService.renditions(404L))
.willThrow(new ServiceException(ErrorCode.DATA_NOT_FOUND, "媒资不存在"));
mockMvc.perform(get("/admin/media/404/renditions"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ErrorCode.DATA_NOT_FOUND.getCode()))
.andExpect(jsonPath("$.message").value("媒资不存在"));
}
@Test
void should_return_empty_thumbnail_status_without_error() throws Exception {
MediaThumbnailRespVO vo = new MediaThumbnailRespVO();
vo.setStatus("EMPTY");
vo.setPageIndex(1);
vo.setPageSize(100);
vo.setFrames(PageResult.empty());
given(outputService.thumbnails(88L, 1, 100)).willReturn(vo);
mockMvc.perform(get("/admin/media/88/thumbnails"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.status").value("EMPTY"))
.andExpect(jsonPath("$.data.frames.total").value(0))
.andExpect(jsonPath("$.data.frames.list").isEmpty());
verify(outputService).thumbnails(88L, 1, 100);
}
}
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
import com.fruitshorts.media.service.MediaTranscodeOutputService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
class EpisodeTranscodeAdminServiceTest {
private DramaEpisodeMapper episodeMapper;
private MediaTranscodeOutputService outputService;
private EpisodeTranscodeAdminService service;
@BeforeEach
void setUp() {
episodeMapper = mock(DramaEpisodeMapper.class);
outputService = mock(MediaTranscodeOutputService.class);
service = new EpisodeTranscodeAdminService(episodeMapper, outputService);
}
@Test
void should_return_episode_not_found_state_without_aggregating_media() {
given(episodeMapper.selectById(404L)).willReturn(null);
var result = service.detail(404L);
assertThat(result.getStatus()).isEqualTo("EPISODE_NOT_FOUND");
assertThat(result.getEpisodeId()).isEqualTo(404L);
assertThat(result.getRenditions()).isEmpty();
verifyNoInteractions(outputService);
}
@Test
void should_return_no_video_state_when_episode_video_url_is_blank() {
DramaEpisode episode = new DramaEpisode();
episode.setId(31L);
episode.setVideoUrl("");
given(episodeMapper.selectById(31L)).willReturn(episode);
var result = service.detail(31L);
assertThat(result.getStatus()).isEqualTo("NO_VIDEO");
assertThat(result.getRenditions()).isEmpty();
verifyNoInteractions(outputService);
}
}
+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);
}
}