using System.Text; using File.Infrastructure.Storage; using FluentAssertions; using Microsoft.Extensions.Options; using Moq; using Xunit; using Xunit.Abstractions; namespace BuildFlow.File.Tests; /// 真实 S3 兼容存储往返测试,供隔离 MinIO SOP 阶段执行。 public sealed class S3StorageServiceIntegrationTests(ITestOutputHelper output) { [Fact] [Trait("Category", "External")] public async Task UploadDownloadAndZeroByteObjectsRoundTrip() { var endpoint = GetRequiredVariable("BUILDFLOW_FILE_S3_TEST_ENDPOINT"); var accessKey = GetRequiredVariable("BUILDFLOW_FILE_S3_TEST_ACCESS_KEY"); var secretKey = GetRequiredVariable("BUILDFLOW_FILE_S3_TEST_SECRET_KEY"); var options = new Mock>(); options.SetupGet(x => x.CurrentValue).Returns(new S3Options { Endpoint = endpoint, AccessKey = accessKey, SecretKey = secretKey, Region = "us-east-1", }); var storage = new S3StorageService(options.Object); var bucket = $"buildflow-sop-{Guid.NewGuid():N}"; var payloadKey = "payload.bin"; var zeroByteKey = "empty.bin"; var bucketCreated = false; try { await storage.CreateBucketAsync(bucket, CancellationToken.None); bucketCreated = true; var payload = Encoding.UTF8.GetBytes("BuildFlow isolated MinIO round trip"); await using (var payloadStream = new MemoryStream(payload, writable: false)) { await storage.UploadFileAsync( bucket, payloadKey, payloadStream, "application/octet-stream", CancellationToken.None); } var download = await storage.DownloadFileAsync(bucket, payloadKey, CancellationToken.None); await using (download.Stream) await using (var downloaded = new MemoryStream()) { await download.Stream.CopyToAsync(downloaded, CancellationToken.None); downloaded.ToArray().Should().Equal(payload); } download.ContentType.Should().Be("application/octet-stream"); await using (var emptyStream = new MemoryStream(Array.Empty(), writable: false)) { await storage.UploadFileAsync( bucket, zeroByteKey, emptyStream, "application/octet-stream", CancellationToken.None); } (await storage.ObjectExistsAsync(bucket, zeroByteKey, CancellationToken.None)).Should().BeTrue(); (await storage.GetObjectSizeAsync(bucket, zeroByteKey, CancellationToken.None)).Should().Be(0); var listed = await storage.ListObjectsAsync(bucket, null, 100, null, CancellationToken.None); listed.Files.Select(x => x.Key).Should().BeEquivalentTo(payloadKey, zeroByteKey); output.WriteLine($"SOP_METRIC s3_object_count={listed.Files.Count}"); output.WriteLine($"SOP_METRIC s3_object_bytes={payload.Length}"); } finally { if (bucketCreated) { await storage.DeleteFileAsync(bucket, payloadKey, CancellationToken.None); await storage.DeleteFileAsync(bucket, zeroByteKey, CancellationToken.None); await storage.DeleteBucketAsync(bucket, CancellationToken.None); } } } private static string GetRequiredVariable(string name) => Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException($"必须设置环境变量 {name} 才能运行真实 S3 集成测试。"); }