using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dpz.Core.Entity.Base;
using Dpz.Core.Entity.Base.Image;
using Dpz.Core.Infrastructure;
using Dpz.Core.Service.ObjectStorage;
using Dpz.Core.Shard.Service;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;

namespace Dpz.Core.Server.ObjectStorage.Test;

[TestFixture]
public class UpyunUploadMetadataTest
{
    [Test]
    public async Task UploadAsync_ImageHeaders_ReturnsImageMetadata()
    {
        using var httpClient = CreateHttpClient(() =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent([]),
            };
            response.Headers.Add("x-upyun-file-type", "png");
            response.Headers.Add("x-upyun-width", "640");
            response.Headers.Add("x-upyun-height", "480");
            response.Headers.Add("x-upyun-frames", "1");
            return response;
        });
        var upload = new UpyunUpload(httpClient, NullLogger<UpyunUpload>.Instance);

        var result = await upload.UploadAsync(
            ["images", "demo.png"],
            () => new ByteArrayContent([1, 2, 3]),
            CreateOperator(),
            size: 3,
            hash: "local-md5"
        );

        Assert.That(result, Is.TypeOf<ImageMetadata>());
        var image = (ImageMetadata)result;
        Assert.Multiple(() =>
        {
            Assert.That(image.Url, Is.EqualTo("https://cdn.example.com/images/demo.png"));
            Assert.That(image.Width, Is.EqualTo(640));
            Assert.That(image.Height, Is.EqualTo(480));
            Assert.That(image.Frames, Is.EqualTo(1));
            Assert.That(image.Hash, Is.EqualTo("local-md5"));
            Assert.That(image.Size, Is.EqualTo(3));
            Assert.That(image.Format.Kind, Is.EqualTo(ImageFormatKind.Png));
        });
    }

    [Test]
    public async Task UploadAsync_NonImageHeaders_ReturnsFileMetadata()
    {
        using var httpClient = CreateHttpClient(() =>
            new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent([]) }
        );
        var upload = new UpyunUpload(httpClient, NullLogger<UpyunUpload>.Instance);

        var result = await upload.UploadAsync(
            ["files", "readme.txt"],
            () => new StringContent("hello"),
            CreateOperator(),
            size: 5,
            hash: "local-md5"
        );

        Assert.That(result, Is.TypeOf<FileMetadata>());
        var file = (FileMetadata)result;
        Assert.Multiple(() =>
        {
            Assert.That(file.Url, Is.EqualTo("https://cdn.example.com/files/readme.txt"));
            Assert.That(file.Size, Is.EqualTo(5));
            Assert.That(file.Hash, Is.EqualTo("local-md5"));
        });
    }

    [Test]
    public async Task UploadFileAsync_SmallFile_UsesMetadataUrl()
    {
        using var httpClient = CreateHttpClient(() => throw new InvalidOperationException());
        var uploader = new ParallelChunkBreakpointUpload<UpyunOperator>(
            NullLogger<ParallelChunkBreakpointUpload<UpyunOperator>>.Instance,
            httpClient,
            CreateOperator()
        );
        await using var stream = new MemoryStream([1, 2, 3]);
        var cloudFile = new CloudFile(stream, ["files", "small.txt"]);

        var result = await uploader.UploadFileAsync(
            cloudFile,
            (_, _, size, hash, _, _) =>
                Task.FromResult<IFileMetadata>(
                    new FileMetadata("https://cdn.example.com/files/small.txt", size, hash)
                )
        );

        Assert.That(result, Is.Not.Null);
        Assert.Multiple(() =>
        {
            Assert.That(result!.Value.Url, Is.EqualTo("https://cdn.example.com/files/small.txt"));
            Assert.That(result.Value.Md5, Is.Not.Null.And.Not.Empty);
        });
    }

    private static UpyunOperator CreateOperator() =>
        new()
        {
            Bucket = "bucket",
            Host = "https://cdn.example.com",
            Operator = "operator",
            Password = "password",
        };

    private static HttpClient CreateHttpClient(Func<HttpResponseMessage> responseFactory) =>
        new(new StubHttpMessageHandler(responseFactory))
        {
            BaseAddress = new Uri("https://v0.api.upyun.com"),
        };

    private sealed class StubHttpMessageHandler(Func<HttpResponseMessage> responseFactory)
        : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken
        )
        {
            return Task.FromResult(responseFactory());
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个针对又拍云(Upyun)对象存储服务上传功能的单元测试类,使用 NUnit 测试框架编写。主要测试上传文件时元数据的处理逻辑。

主要组成部分

1. 测试类结构

[TestFixture]
public class UpyunUploadMetadataTest
  • 使用 [TestFixture] 标记为 NUnit 测试类
  • 专门测试 UpyunUpload 类的元数据处理功能

2. 三个核心测试方法

Test 1: UploadAsync_ImageHeaders_ReturnsImageMetadata

测试目的:验证上传图片文件时,能正确解析又拍云返回的图片元数据

测试流程

  1. 创建模拟 HTTP 客户端,返回包含图片信息的响应头:
    • x-upyun-file-type: png
    • x-upyun-width: 640
    • x-upyun-height: 480
    • x-upyun-frames: 1
  2. 调用上传方法
  3. 断言返回结果是 ImageMetadata 类型
  4. 验证图片元数据的各个字段(URL、宽度、高度、帧数、哈希值、大小、格式等)

Test 2: UploadAsync_NonImageHeaders_ReturnsFileMetadata

测试目的:验证上传非图片文件时,返回基础文件元数据

测试流程

  1. 创建模拟 HTTP 客户端,返回不包含图片特定头的响应
  2. 上传文本文件
  3. 断言返回结果是 FileMetadata 类型
  4. 验证文件元数据(URL、大小、哈希值)

Test 3: UploadFileAsync_SmallFile_UsesMetadataUrl

测试目的:验证小文件上传时使用元数据中的 URL

测试流程

  1. 创建会抛出异常的模拟客户端(确保不会真正发送 HTTP 请求)
  2. 使用 ParallelChunkBreakpointUpload 上传小文件
  3. 验证返回的 URL 和 MD5 值

3. 辅助方法

CreateOperator()

创建又拍云操作配置对象,包含:

  • Bucket: 存储空间名称
  • Host: CDN 域名
  • Operator: 操作员账号
  • Password: 操作员密码

CreateHttpClient()

创建带有自定义响应的 HTTP 客户端,用于模拟又拍云 API 的响应

StubHttpMessageHandler

核心测试辅助类,继承 HttpMessageHandler

  • 通过工厂函数 responseFactory 自定义 HTTP 响应
  • 避免真实的网络请求
  • 实现可控的测试环境

技术要点

  1. 依赖注入模拟:使用 NullLogger 避免测试中的日志依赖
  2. HTTP 客户端模拟:通过自定义 HttpMessageHandler 实现完全可控的 HTTP 响应
  3. 资源管理:使用 usingawait using 确保资源正确释放
  4. 断言组织:使用 Assert.Multiple 批量验证多个条件
  5. 类型检查:验证返回的元数据类型是否符合预期(ImageMetadata vs FileMetadata

测试价值

  • ✅ 验证图片和普通文件的区分逻辑
  • ✅ 确保元数据正确解析
  • ✅ 测试小文件上传的特殊处理
  • ✅ 无需真实网络环境即可完成测试
评论加载中...