using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Amazon.S3;
using Amazon.S3.Model;
using Dpz.Core.Service.ObjectStorage;
using Dpz.Core.Service.ObjectStorage.Services.Impl;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;
namespace Dpz.Core.Server.ObjectStorage.Test;
/// <summary>
/// COS 视频服务的轻量单元测试
/// </summary>
[TestFixture]
public class CosVideoCloudServiceTest
{
[Test]
public void ParsePlaylist_WhenMediaPlaylist_ReturnsDurations()
{
const string playlist = """
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:5.100,
segment-1.ts
#EXTINF:4.9,
segment-2.ts
#EXT-X-ENDLIST
""";
var result = CosVideoCloudService.ParsePlaylist(playlist);
Assert.Multiple(() =>
{
Assert.That(result.Duration, Is.EqualTo(10).Within(0.0001));
Assert.That(result.Points, Is.EqualTo(new[] { 5.1, 4.9 }).Within(0.0001));
Assert.That(result.Variants, Is.Empty);
});
}
[Test]
public void ParsePlaylist_WhenMasterPlaylist_ReturnsHighestBandwidthFirst()
{
const string playlist = """
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=1280x720
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1920x1080
1080p/index.m3u8
""";
var result = CosVideoCloudService.ParsePlaylist(playlist);
Assert.Multiple(() =>
{
Assert.That(result.Variants, Has.Length.EqualTo(2));
Assert.That(result.Variants[0], Is.EqualTo("1080p/index.m3u8"));
});
}
[Test]
public void ParseMediaInfo_WhenDurationExists_ReturnsMetadata()
{
var document = XDocument.Parse(
"""
<Response>
<MediaInfo>
<Format>
<Duration>12.345000</Duration>
</Format>
</MediaInfo>
</Response>
"""
);
var result = CosVideoCloudService.ParseMediaInfo(document);
Assert.That(result, Is.Not.Null);
Assert.That(result!.Duration, Is.EqualTo(12.345).Within(0.0001));
}
[Test]
public async Task GetVideoMetaAsync_WhenMasterPlaylist_SelectsHighestBandwidthMediaPlaylist()
{
var service = CreateService(
new Dictionary<string, string>
{
["Video/demo/master.m3u8"] = """
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=100
low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=200
high/index.m3u8
""",
["Video/demo/high/index.m3u8"] = """
#EXTM3U
#EXTINF:3.5,
1.ts
#EXTINF:4,
2.ts
#EXT-X-ENDLIST
""",
}
);
var result = await service.GetVideoMetaAsync("/Video/demo/master.m3u8");
Assert.That(result.Success, Is.True, result.Message);
Assert.That(result.Data, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(result.Data!.Duration, Is.EqualTo(7.5).Within(0.0001));
Assert.That(result.Data.Points, Is.EqualTo(new[] { 3.5, 4 }).Within(0.0001));
});
}
[Test]
public async Task VideoScreenshotAsync_WhenSnapshotSucceeds_UploadsWebpCover()
{
var uploaded = new MemoryStream();
string? uploadedKey = null;
string? uploadedContentType = null;
var service = CreateService(
new Dictionary<string, string>
{
["Video/demo/1080p.m3u8"] = """
#EXTM3U
#EXTINF:10,
1.ts
#EXT-X-ENDLIST
""",
},
_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent([1, 2, 3, 4]),
},
request =>
{
uploadedKey = request.Key;
uploadedContentType = request.ContentType;
request.InputStream.CopyTo(uploaded);
}
);
var result = await service.VideoScreenshotAsync(
"/Video/demo/1080p.m3u8",
TimeSpan.FromSeconds(5)
);
Assert.That(result.Success, Is.True, result.Message);
Assert.Multiple(() =>
{
Assert.That(uploadedKey, Is.EqualTo("Video/demo/video.webp"));
Assert.That(uploadedContentType, Is.EqualTo("image/jpeg"));
Assert.That(uploaded.Length, Is.GreaterThan(0));
Assert.That(result.Data, Is.EqualTo("https://cdn.example.com/Video/demo/video.webp"));
});
}
private static CosVideoCloudService CreateService(
Dictionary<string, string> objects,
Func<HttpRequestMessage, HttpResponseMessage>? responseFactory = null,
Action<PutObjectRequest>? uploadCallback = null
)
{
var s3 = new Mock<IAmazonS3>();
s3.Setup(x => x.GetObjectAsync(It.IsAny<GetObjectRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(
(GetObjectRequest request, CancellationToken _) =>
new GetObjectResponse
{
BucketName = request.BucketName,
Key = request.Key,
ResponseStream = new MemoryStream(
Encoding.UTF8.GetBytes(objects[request.Key])
),
}
);
s3.Setup(x => x.PutObjectAsync(It.IsAny<PutObjectRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(
(PutObjectRequest request, CancellationToken _) =>
{
uploadCallback?.Invoke(request);
return new PutObjectResponse { HttpStatusCode = HttpStatusCode.OK };
}
);
var options = new S3ObjectStorageOptions
{
Endpoint = "https://cos.ap-guangzhou.myqcloud.com",
SecretId = "secret-id",
SecretKey = "secret-key",
Bucket = "bucket-123",
CdnHost = "https://cdn.example.com",
Region = "ap-guangzhou",
}.Validate();
var httpClient = new HttpClient(new StubHttpMessageHandler(responseFactory));
return new CosVideoCloudService(
s3.Object,
options,
new CosRequestSigner(options),
httpClient,
NullLogger<CosVideoCloudService>.Instance
);
}
private sealed class StubHttpMessageHandler(
Func<HttpRequestMessage, HttpResponseMessage>? responseFactory
) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
return Task.FromResult(
responseFactory?.Invoke(request)
?? new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
"""
<Response>
<MediaInfo>
<Format>
<Duration>10</Duration>
</Format>
</MediaInfo>
</Response>
"""
),
}
);
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个针对 CosVideoCloudService(腾讯云 COS 视频服务)的单元测试类,使用 NUnit 测试框架编写。
主要测试场景
1. ParsePlaylist_WhenMediaPlaylist_ReturnsDurations
测试解析 HLS 媒体播放列表(Media Playlist)的功能:
- 输入一个包含两个视频片段的 M3U8 播放列表
- 验证能正确解析出总时长(10秒)
- 验证能提取各片段时长点(5.1秒和4.9秒)
- 验证变体列表为空
2. ParsePlaylist_WhenMasterPlaylist_ReturnsHighestBandwidthFirst
测试解析 HLS 主播放列表(Master Playlist)的功能:
- 输入包含多个码率变体的主播放列表
- 验证返回的变体列表按带宽从高到低排序
- 确认最高带宽的 1080p 变体排在第一位
3. ParseMediaInfo_WhenDurationExists_ReturnsMetadata
测试从 XML 响应中解析媒体信息的功能:
- 解析包含视频时长的 XML 文档
- 验证能正确提取时长元数据(12.345秒)
4. GetVideoMetaAsync_WhenMasterPlaylist_SelectsHighestBandwidthMediaPlaylist
测试获取视频元数据的完整流程:
- Mock 了主播放列表和媒体播放列表的 S3 对象
- 验证服务能自动选择最高带宽的变体
- 从该变体的媒体播放列表中计算总时长和时间点
5. VideoScreenshotAsync_WhenSnapshotSucceeds_UploadsWebpCover
测试视频截图功能:
- Mock HTTP 响应返回截图数据
- 验证截图被正确上传到 S3(路径为
Video/demo/video.webp) - 验证 Content-Type 设置为
image/jpeg - 验证返回正确的 CDN URL
辅助方法
CreateService
创建测试用的服务实例,包含三个可选参数:
objects: Mock 的 S3 对象字典(key → 文件内容)responseFactory: 自定义 HTTP 响应工厂uploadCallback: 上传时的回调函数
Mock 了:
IAmazonS3的GetObjectAsync和PutObjectAsync方法- HTTP 客户端(通过
StubHttpMessageHandler) - S3 配置选项
StubHttpMessageHandler
自定义的 HTTP 消息处理器,用于 Mock HTTP 请求:
- 默认返回包含媒体信息的 XML 响应
- 可通过
responseFactory自定义响应
技术要点
- 使用 Moq 框架模拟 AWS S3 客户端
- 使用 NUnit 的
Assert.Multiple进行批量断言 - 使用 C# 11 的原始字符串字面量(
""")提高可读性 - 完整覆盖播放列表解析、视频元数据获取、视频截图等核心功能
这是一个结构良好的轻量级单元测试,有效隔离了外部依赖(S3、HTTP),确保测试的快速和可靠性。
AI 正在分析代码…
评论加载中...