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 了:

  • IAmazonS3GetObjectAsyncPutObjectAsync 方法
  • HTTP 客户端(通过 StubHttpMessageHandler
  • S3 配置选项

StubHttpMessageHandler

自定义的 HTTP 消息处理器,用于 Mock HTTP 请求:

  • 默认返回包含媒体信息的 XML 响应
  • 可通过 responseFactory 自定义响应

技术要点

  • 使用 Moq 框架模拟 AWS S3 客户端
  • 使用 NUnitAssert.Multiple 进行批量断言
  • 使用 C# 11 的原始字符串字面量""")提高可读性
  • 完整覆盖播放列表解析、视频元数据获取、视频截图等核心功能

这是一个结构良好的轻量级单元测试,有效隔离了外部依赖(S3、HTTP),确保测试的快速和可靠性。

评论加载中...