using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Dpz.Core.Entity.Base.Image;
using Dpz.Core.Service.ObjectStorage;
using Dpz.Core.Service.ObjectStorage.Services;
using Dpz.Core.Shard.Service;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
namespace Dpz.Core.Server.ObjectStorage.Test;
/// <summary>
/// 腾讯云 COS 与数据万象配置验证 S3 对象存储集成测试
/// </summary>
[TestFixture]
public class S3ObjectStorageServiceIntegrationTest
{
private readonly IConfiguration _configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Test.json")
.Build();
/// <summary>
/// 验证文本文件上传、下载、目录列表、元信息读取、另存和删除的完整链路
/// </summary>
[Test]
public async Task S3_UploadDownloadListMetadataSaveAndDelete_ShouldComplete()
{
AssertS3Configured();
var cdnHost = _configuration["S3:CdnHost"]!;
var objectStorageService = CreateObjectStorageService();
var testRoot = CreateTestRoot();
var key = $"{testRoot}/hello.txt";
var savePath = Path.Combine(
TestContext.CurrentContext.WorkDirectory,
$"{Guid.NewGuid():N}.txt"
);
try
{
var bytes = Encoding.UTF8.GetBytes("COS/S3 integration test, 中文内容");
var uploadResult = await objectStorageService.UploadAsync(
bytes,
["Test", "S3", testRoot.Split('/')[2]],
"hello.txt"
);
Assert.That(uploadResult.Url, Does.StartWith(cdnHost));
await using (var stream = await objectStorageService.DownloadAsync(key))
{
using var reader = new StreamReader(stream, Encoding.UTF8);
Assert.That(
await reader.ReadToEndAsync(),
Is.EqualTo(Encoding.UTF8.GetString(bytes))
);
}
var information = await objectStorageService.GetFileInformationAsync(key);
Assert.Multiple(() =>
{
Assert.That(information.Length, Is.EqualTo(bytes.Length));
Assert.That(information.Md5, Is.Not.Null.And.Not.Empty);
});
var folderList = await objectStorageService.GetFolderListAsync(testRoot);
Assert.That(folderList.Any(x => x.Name == "hello.txt"), Is.True);
await objectStorageService.SaveAsAsync(key, savePath);
Assert.That(
await File.ReadAllTextAsync(savePath),
Is.EqualTo(Encoding.UTF8.GetString(bytes))
);
}
finally
{
if (File.Exists(savePath))
{
File.Delete(savePath);
}
await objectStorageService.DeleteAsync(key);
}
}
/// <summary>
/// 验证 PNG 上传后能通过腾讯云数据万象适配为 ImageMetadata
/// </summary>
[Test]
public async Task S3_UploadPng_ShouldReturnCloudInfiniteImageMetadata()
{
AssertS3Configured();
var provider = CreateServiceProvider();
var objectStorageService = provider.GetRequiredService<IObjectStorageOperation>();
var cloudInfiniteService = provider.GetRequiredService<ICloudInfiniteService>();
var testRoot = CreateTestRoot();
var key = $"{testRoot}/pixel.png";
try
{
var bytes = Convert.FromBase64String(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
);
var result = await objectStorageService.UploadAsync(
bytes,
["Test", "S3", testRoot.Split('/')[2]],
"pixel.png"
);
Assert.That(result, Is.TypeOf<ImageMetadata>());
var metadata = (ImageMetadata)result;
var directMetadata = await cloudInfiniteService.GetImageMetadataAsync(key, result.Url);
Assert.Multiple(() =>
{
Assert.That(metadata.Width, Is.EqualTo(1));
Assert.That(metadata.Height, Is.EqualTo(1));
Assert.That(metadata.Format.Kind, Is.EqualTo(ImageFormatKind.Png));
Assert.That(metadata.Size, Is.GreaterThan(0));
Assert.That(directMetadata, Is.Not.Null);
Assert.That(directMetadata!.Value.Width, Is.EqualTo(1));
Assert.That(directMetadata.Value.Height, Is.EqualTo(1));
Assert.That(directMetadata.Value.Format.Kind, Is.EqualTo(ImageFormatKind.Png));
Assert.That(directMetadata.Value.Size, Is.GreaterThan(0));
});
}
finally
{
await objectStorageService.DeleteAsync(key);
}
}
/// <summary>
/// 验证 SVG 上传后能从 COS 原图兜底解析为 ImageMetadata
/// </summary>
[Test]
public async Task S3_UploadSvg_ShouldReturnCloudInfiniteSvgMetadata()
{
AssertS3Configured();
var provider = CreateServiceProvider();
var objectStorageService = provider.GetRequiredService<IObjectStorageOperation>();
var cloudInfiniteService = provider.GetRequiredService<ICloudInfiniteService>();
var testRoot = CreateTestRoot();
var key = $"{testRoot}/vector.svg";
try
{
var bytes = Encoding.UTF8.GetBytes(
"""
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="32"></svg>
"""
);
var result = await objectStorageService.UploadAsync(
bytes,
["Test", "S3", testRoot.Split('/')[2]],
"vector.svg"
);
var metadata = await cloudInfiniteService.GetImageMetadataAsync(key, result.Url);
Assert.That(metadata, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(metadata!.Value.Width, Is.EqualTo(24));
Assert.That(metadata.Value.Height, Is.EqualTo(32));
Assert.That(metadata.Value.Format.Kind, Is.EqualTo(ImageFormatKind.Svg));
Assert.That(metadata.Value.Size, Is.GreaterThan(0));
});
}
finally
{
await objectStorageService.DeleteAsync(key);
}
}
/// <summary>
/// 验证 CloudFile 上传会返回公开访问地址和文件 MD5
/// </summary>
[Test]
public async Task S3_UploadFileAsync_ShouldReturnMd5Address()
{
AssertS3Configured();
var cdnHost = _configuration["S3:CdnHost"]!;
var objectStorageService = CreateObjectStorageService();
var testRoot = CreateTestRoot();
var key = $"{testRoot}/cloud-file.txt";
try
{
await using var stream = new MemoryStream("CloudFile 上传测试"u8.ToArray());
var result = await objectStorageService.UploadFileAsync(
new CloudFile(stream, ["Test", "S3", testRoot.Split('/')[2], "cloud-file.txt"])
);
Assert.That(result, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(result!.Value.Url, Does.StartWith(cdnHost));
Assert.That(result.Value.Md5, Is.Not.Empty);
});
}
finally
{
await objectStorageService.DeleteAsync(key);
}
}
/// <summary>
/// 验证空目录 marker 会被删除、包含文件的目录不会删除
/// </summary>
[Test]
public async Task S3_CleanEmptyDirectories_ShouldDeleteEmptyMarkersOnly()
{
AssertS3Configured();
IServiceCollection services = new ServiceCollection();
services.AddSingleton(_configuration);
services.AddS3ObjectStorage();
services.AddLogging(configure => configure.AddConsole());
var provider = services.BuildServiceProvider();
var objectStorageService = provider.GetRequiredService<IObjectStorageOperation>();
var amazonS3 = provider.GetRequiredService<IAmazonS3>();
var bucket = _configuration["S3:Bucket"]!;
var testRoot = CreateTestRoot();
var emptyMarkerKey = $"{testRoot}/empty-dir/";
var occupiedMarkerKey = $"{testRoot}/keep/";
var fileKey = $"{testRoot}/keep/hello.txt";
var zeroByteFileKey = $"{testRoot}/empty-file.txt";
try
{
await amazonS3.PutObjectAsync(
new PutObjectRequest
{
BucketName = bucket,
Key = emptyMarkerKey,
InputStream = new MemoryStream([]),
}
);
await amazonS3.PutObjectAsync(
new PutObjectRequest
{
BucketName = bucket,
Key = occupiedMarkerKey,
InputStream = new MemoryStream([]),
}
);
await amazonS3.PutObjectAsync(
new PutObjectRequest
{
BucketName = bucket,
Key = fileKey,
InputStream = new MemoryStream("keep"u8.ToArray()),
}
);
await amazonS3.PutObjectAsync(
new PutObjectRequest
{
BucketName = bucket,
Key = zeroByteFileKey,
InputStream = new MemoryStream([]),
}
);
var deletedCount = await objectStorageService.CleanEmptyDirectoriesAsync(testRoot);
Assert.That(deletedCount, Is.EqualTo(1));
Assert.That(await ObjectExistsAsync(amazonS3, bucket, emptyMarkerKey), Is.False);
Assert.That(await ObjectExistsAsync(amazonS3, bucket, occupiedMarkerKey), Is.True);
Assert.That(await ObjectExistsAsync(amazonS3, bucket, fileKey), Is.True);
Assert.That(await ObjectExistsAsync(amazonS3, bucket, zeroByteFileKey), Is.True);
}
finally
{
await DeleteObjectIfExistsAsync(amazonS3, bucket, emptyMarkerKey);
await DeleteObjectIfExistsAsync(amazonS3, bucket, occupiedMarkerKey);
await DeleteObjectIfExistsAsync(amazonS3, bucket, fileKey);
await DeleteObjectIfExistsAsync(amazonS3, bucket, zeroByteFileKey);
}
}
/// <summary>
/// 验证 S3 测试所需配置完整,不完整时跳过真实 COS 集成测试
/// </summary>
private void AssertS3Configured()
{
var requiredKeys = new[] { "Endpoint", "SecretId", "SecretKey", "Bucket", "CdnHost" };
foreach (var key in requiredKeys)
{
var value = _configuration[$"S3:{key}"];
if (string.IsNullOrWhiteSpace(value) || value.StartsWith('<'))
{
Assert.Ignore($"appsettings.Test.json 缺少可用的 S3:{key} 配置");
}
}
}
/// <summary>
/// 创建只使用 S3/COS 注册的对象存储服务,避免依赖默认又拍云注册
/// </summary>
private IObjectStorageOperation CreateObjectStorageService()
{
return CreateServiceProvider().GetRequiredService<IObjectStorageOperation>();
}
private ServiceProvider CreateServiceProvider()
{
IServiceCollection services = new ServiceCollection();
services.AddSingleton(_configuration);
services.AddS3ObjectStorage();
services.AddLogging(configure => configure.AddConsole());
return services.BuildServiceProvider();
}
/// <summary>
/// 创建每次测试独立的远程目录,方便 finally 清理测试文件
/// </summary>
private static string CreateTestRoot() => $"Test/S3/{Guid.NewGuid():N}";
/// <summary>
/// 判断对象是否存在于 COS
/// </summary>
private static async Task<bool> ObjectExistsAsync(IAmazonS3 amazonS3, string bucket, string key)
{
var response = await amazonS3.ListObjectsV2Async(
new ListObjectsV2Request { BucketName = bucket, Prefix = key }
);
return response.S3Objects?.Any(x => x.Key == key) == true;
}
/// <summary>
/// 删除测试对象,忽略不存在的对象
/// </summary>
private static async Task DeleteObjectIfExistsAsync(
IAmazonS3 amazonS3,
string bucket,
string key
)
{
await amazonS3.DeleteObjectAsync(
new DeleteObjectRequest { BucketName = bucket, Key = key }
);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个针对 S3 对象存储服务(主要是腾讯云 COS)的 NUnit 集成测试类。代码验证了对象存储服务的各项核心功能是否正常工作。
整体结构
类定义
[TestFixture]
public class S3ObjectStorageServiceIntegrationTest
- 使用 NUnit 测试框架
- 从
appsettings.Test.json加载配置 - 包含 6 个测试方法和 5 个辅助方法
核心测试方法
1. S3_UploadDownloadListMetadataSaveAndDelete_ShouldComplete
验证完整的文件操作链路:
- ✅ 上传文本文件
- ✅ 下载并验证内容
- ✅ 获取文件元信息(大小、MD5)
- ✅ 列出目录内容
- ✅ 另存为本地文件
- ✅ 删除远程文件
2. S3_UploadPng_ShouldReturnCloudInfiniteImageMetadata
验证图片处理功能:
- 上传 1x1 像素的 PNG 图片(Base64 编码)
- 验证返回
ImageMetadata(宽、高、格式、大小) - 通过 腾讯云数据万象(Cloud Infinite)服务直接获取元数据并验证
3. S3_UploadSvg_ShouldReturnCloudInfiniteSvgMetadata
验证 SVG 矢量图处理:
- 上传包含
width="24" height="32"的 SVG - 验证能正确解析 SVG 的宽高信息
4. S3_UploadFileAsync_ShouldReturnMd5Address
验证 CloudFile 上传接口:
- 使用
MemoryStream上传 - 验证返回公开访问 URL 和文件 MD5 哈希值
5. S3_CleanEmptyDirectories_ShouldDeleteEmptyMarkersOnly
验证空目录清理逻辑:
创建文件结构:
├── empty-dir/ (空目录标记)
├── keep/ (目录标记)
│ └── hello.txt (文件)
└── empty-file.txt (0字节文件)
执行清理后:
✅ empty-dir/ 被删除
✅ keep/ 保留(包含文件)
✅ empty-file.txt 保留(虽然0字节但不是目录标记)
辅助方法
AssertS3Configured()
检查配置完整性:
必需配置项:Endpoint, SecretId, SecretKey, Bucket, CdnHost
如果缺失或包含占位符(如 <your-xxx>),跳过测试
CreateTestRoot()
每次测试创建唯一目录:
格式:Test/S3/{GUID}
作用:隔离不同测试,方便清理
ObjectExistsAsync() / DeleteObjectIfExistsAsync()
直接使用 AWS S3 SDK 操作对象,用于验证和清理。
关键技术点
1. 依赖注入配置
services.AddS3ObjectStorage(); // 注册 S3 服务
services.AddLogging(); // 添加控制台日志
2. 多重断言
Assert.Multiple(() => {
Assert.That(metadata.Width, Is.EqualTo(1));
Assert.That(metadata.Height, Is.EqualTo(1));
// ... 多个断言
});
所有断言会执行完毕后统一报告失败,不会在第一个失败时停止。
3. 资源清理
每个测试使用 try-finally 确保:
- 临时文件被删除
- 远程对象被清理
- 避免测试污染
4. 集成测试特点
- 使用真实的云服务(腾讯云 COS)
- 需要有效的配置才能运行
- 验证完整的业务流程而非单元逻辑
测试覆盖范围
| 功能 | 测试方法 |
|---|---|
| 文件上传/下载 | ✅ 测试 1, 4 |
| 元数据获取 | ✅ 测试 1, 2, 3 |
| 目录列表 | ✅ 测试 1 |
| 图片处理 | ✅ 测试 2, 3 |
| 空目录清理 | ✅ 测试 5 |
| CDN URL 生成 | ✅ 测试 1, 4 |
这是一个典型的云服务集成测试套件,用于确保对象存储抽象层能正确对接腾讯云 COS 服务。
AI 正在分析代码…
评论加载中...