using Microsoft.Extensions.Options; using Tiku.Application.Storage; using Tiku.Infrastructure.Storage; namespace Tiku.UnitTests.Storage; public sealed class AliyunOssObjectStorageServiceTests { private static readonly Guid TenantId = Guid.Parse("11111111-1111-1111-1111-111111111111"); [Fact] public void Validate_object_key_requires_tenant_prefix_like_original_storage_service() { using var service = CreateConfiguredService(); var exception = Assert.Throws(() => service.ValidateObjectKey(TenantId, "other-tenant/file.pdf")); Assert.Equal("OBJECT_KEY_TENANT_PREFIX_REQUIRED", exception.Code); } [Fact] public void Validate_mime_type_and_size_use_configured_allow_list() { using var service = CreateConfiguredService(new ObjectStorageOptions { MaxUploadBytes = 100, AllowedMimePrefixes = ["image/"], AllowedMimeTypes = ["application/pdf"] }); Assert.Equal("image/png", service.ValidateMimeType(" Image/PNG ")); Assert.Equal(99, service.ValidateFileSize(99)); Assert.Equal("MIME_TYPE_NOT_ALLOWED", Assert.Throws(() => service.ValidateMimeType("text/html")).Code); Assert.Equal("FILE_TOO_LARGE", Assert.Throws(() => service.ValidateFileSize(101)).Code); } [Fact] public async Task Sign_download_requires_complete_aliyun_oss_options() { using var service = CreateUnconfiguredService(); var exception = await Assert.ThrowsAsync(() => service.SignDownloadAsync(new ObjectStorageDownloadSignRequest( TenantId, ObjectStorageProviders.AliyunOss, "bucket", $"{TenantId:N}/path/file.pdf", TimeSpan.FromMinutes(15)))); Assert.Contains("ALIYUN_OSS_ACCESS_KEY_ID", exception.Message, StringComparison.Ordinal); } [Fact] public async Task Sign_download_generates_aliyun_oss_presigned_get_url() { using var service = CreateConfiguredService(); var signedUrl = await service.SignDownloadAsync(new ObjectStorageDownloadSignRequest( TenantId, ObjectStorageProviders.AliyunOss, "tiku-assets", $"{TenantId:N}/handbook.pdf", TimeSpan.FromMinutes(15), FileName: "handbook.pdf", Disposition: "attachment")); Assert.Equal(ObjectStorageProviders.AliyunOss, signedUrl.Provider); Assert.Equal("GET", signedUrl.Method); Assert.Equal("aliyun-oss-signature-url-v4", signedUrl.SignatureMode); Assert.Contains("tiku-assets", signedUrl.Url.Host, StringComparison.Ordinal); Assert.Contains($"{TenantId:N}/handbook.pdf", signedUrl.Url.AbsoluteUri, StringComparison.Ordinal); Assert.Contains("x-oss-", signedUrl.Url.Query, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Sign_upload_generates_aliyun_oss_presigned_put_url_with_content_type_header() { using var service = CreateConfiguredService(); var signedUrl = await service.SignUploadAsync(new ObjectStorageUploadSignRequest( TenantId, ObjectStorageProviders.AliyunOss, "tiku-assets", $"{TenantId:N}/upload.pdf", "upload.pdf", "application/pdf", 1024, TimeSpan.FromMinutes(10))); Assert.Equal("PUT", signedUrl.Method); Assert.Contains("x-oss-", signedUrl.Url.Query, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Cdn_url_download_returns_public_or_provider_managed_url_without_oss_signing() { using var service = CreateUnconfiguredService(); var signedUrl = await service.SignDownloadAsync(new ObjectStorageDownloadSignRequest( TenantId, ObjectStorageProviders.ExternalUrl, null, null, TimeSpan.FromMinutes(5), "https://cdn.example.com/file.pdf")); Assert.Equal("public-or-provider-managed", signedUrl.SignatureMode); Assert.Equal("https://cdn.example.com/file.pdf", signedUrl.Url.AbsoluteUri); } [Fact] public void Aliyun_oss_options_allow_empty_optional_configuration() { Assert.True(AliyunOssOptions.BeValid(new AliyunOssOptions())); Assert.False(AliyunOssOptions.BeValid(new AliyunOssOptions { PresignDefaultMinutes = 0 })); } [Fact] public async Task Local_dev_round_trips_through_configured_s3_endpoint() { var endpoint = Environment.GetEnvironmentVariable("TIKU_S3_TEST_ENDPOINT"); if (string.IsNullOrWhiteSpace(endpoint)) return; using var service = new AliyunOssObjectStorageService( Options.Create(new ObjectStorageOptions { DefaultProvider = ObjectStorageProviders.LocalDev }), Options.Create(new AliyunOssOptions()), Options.Create(new S3CompatibleOptions { Endpoint = endpoint, AccessKey = Environment.GetEnvironmentVariable("TIKU_S3_TEST_ACCESS_KEY") ?? "tiku", SecretKey = Environment.GetEnvironmentVariable("TIKU_S3_TEST_SECRET_KEY") ?? "tiku_minio_dev", Region = "us-east-1" })); var objectKey = $"{TenantId:N}/tests/{Guid.NewGuid():N}.json"; const string payload = "{\"storage\":\"minio\"}"; await using var input = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(payload)); var write = await service.WriteObjectAsync(new ObjectStorageWriteRequest( TenantId, ObjectStorageProviders.LocalDev, "tiku-tests", objectKey, "application/json", input, input.Length)); var metadata = await service.HeadObjectAsync(new ObjectStorageHeadRequest( TenantId, ObjectStorageProviders.LocalDev, "tiku-tests", objectKey)); await using var output = await service.OpenReadAsync(new ObjectStorageReadRequest( TenantId, ObjectStorageProviders.LocalDev, "tiku-tests", objectKey)); using var reader = new StreamReader(output); Assert.Equal("s3-compatible-put-object", write.VerificationSource); Assert.Equal(input.Length, metadata.SizeBytes); Assert.Equal(payload, await reader.ReadToEndAsync()); } private static AliyunOssObjectStorageService CreateConfiguredService(ObjectStorageOptions? storageOptions = null) { return new AliyunOssObjectStorageService( Options.Create(storageOptions ?? new ObjectStorageOptions()), Options.Create(new AliyunOssOptions { Region = "cn-hangzhou", AccessKeyId = "test-access-key-id", AccessKeySecret = "test-access-key-secret" }), Options.Create(new S3CompatibleOptions { Endpoint = "127.0.0.1:9000", AccessKey = "test-access-key", SecretKey = "test-secret-key", Region = "us-east-1" })); } private static AliyunOssObjectStorageService CreateUnconfiguredService() { return new AliyunOssObjectStorageService(Options.Create(new ObjectStorageOptions()), Options.Create(new AliyunOssOptions()), Options.Create(new S3CompatibleOptions())); } }