diff --git a/build-logic/src/main/kotlin/buildlogic.loadenv-conventions.gradle.kts b/build-logic/src/main/kotlin/buildlogic.loadenv-conventions.gradle.kts new file mode 100644 index 000000000..aa4b1c36d --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic.loadenv-conventions.gradle.kts @@ -0,0 +1,28 @@ +// 直接实现 dotenv 功能,避免插件依赖 +fun loadDotenv() { + val envFile = rootProject.file(".env") + if (envFile.exists()) { + envFile.readLines().forEach { line -> + val trimmedLine = line.trim() + if (trimmedLine.isNotEmpty() && !trimmedLine.startsWith("#")) { + val parts = trimmedLine.split("=", limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().removeSurrounding("\"").removeSurrounding("'") + if (key.isNotEmpty() && value.isNotEmpty()) { + // 设置到所有任务的环境变量中 + tasks.withType { environment(key, value) } + tasks.withType { environment(key, value) } + logger.debug("Loaded environment variable: $key") + } + } + } + } + logger.info("Loaded .env file from: ${envFile.absolutePath}") + } else { + logger.warn(".env file not found at: ${envFile.absolutePath}") + } +} + +// 加载 dotenv 配置 +loadDotenv() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3301043d..741138d99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ org-springframework-modulith = "2.0.0-M1" org-springframework-security = "6.5.5" org-testcontainers = "1.21.3" org-testng = "7.11.0" -project = "0.0.36" +project = "0.0.37" [libraries] ch-qos-logback-logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "ch-qos-logback" } diff --git a/integrate-test/oss/volcengine-tos/build.gradle.kts b/integrate-test/oss/volcengine-tos/build.gradle.kts new file mode 100644 index 000000000..1acf75d35 --- /dev/null +++ b/integrate-test/oss/volcengine-tos/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("buildlogic.kotlinspring-test-conventions") + id("buildlogic.spotless-conventions") + id("buildlogic.loadenv-conventions") +} + +dependencies { + implementation(projects.oss.ossVolcengineTos) + + testImplementation(libs.com.volcengine.ve.tos.java.sdk) + + testImplementation(projects.testtoolkit.testtoolkitSpringmvc) + testImplementation(projects.testtoolkit.testtoolkitTestcontainers) + testImplementation(libs.org.springframework.boot.spring.boot.starter.web) +} diff --git a/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/EnvFunctions.kt b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/EnvFunctions.kt new file mode 100644 index 000000000..d36f39a6f --- /dev/null +++ b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/EnvFunctions.kt @@ -0,0 +1,29 @@ +package itest.integrate.oss.volcenginetos + +import io.github.truenine.composeserver.logger +import io.github.truenine.composeserver.oss.volcengine.properties.VolcengineTosProperties + +val log = logger() + +private val accessKey = System.getenv("VOLCENGINE_TOS_ACCESS_KEY")?.takeIf { it.isNotBlank() } +private val secretKey = System.getenv("VOLCENGINE_TOS_SECRET_KEY")?.takeIf { it.isNotBlank() } +private val hasCredentials = !accessKey.isNullOrBlank() && !secretKey.isNullOrBlank() + +data class OssAkSk(val ak: String, val sk: String, val endpoint: String = VolcengineTosProperties.DEFAULT_ENDPOINT) + +fun hasTosRequiredEnvironmentVariables(): Boolean { + if (!hasCredentials) { + log.warn("Skipping Volcengine TOS integration tests: missing required environment variables VOLCENGINE_TOS_ACCESS_KEY or VOLCENGINE_TOS_SECRET_KEY") + } else { + log.info("Detected Volcengine TOS credentials, will execute integration tests") + } + return hasCredentials +} + +fun getTosAkSk(): OssAkSk? { + return if (hasTosRequiredEnvironmentVariables()) { + OssAkSk(accessKey!!, secretKey!!) + } else { + null + } +} diff --git a/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/TestEntrance.kt b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/TestEntrance.kt new file mode 100644 index 000000000..1fc7655e9 --- /dev/null +++ b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/TestEntrance.kt @@ -0,0 +1,5 @@ +package itest.integrate.oss.volcenginetos + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication internal class TestEntrance diff --git a/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/VolcengineTosObjectStorageServiceIntegrationTest.kt b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/VolcengineTosObjectStorageServiceIntegrationTest.kt similarity index 59% rename from oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/VolcengineTosObjectStorageServiceIntegrationTest.kt rename to integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/VolcengineTosObjectStorageServiceIntegrationTest.kt index 5433228b3..15f25fb5b 100644 --- a/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/VolcengineTosObjectStorageServiceIntegrationTest.kt +++ b/integrate-test/oss/volcengine-tos/src/test/kotlin/itest/integrate/oss/volcenginetos/VolcengineTosObjectStorageServiceIntegrationTest.kt @@ -1,4 +1,4 @@ -package io.github.truenine.composeserver.oss.volcengine +package itest.integrate.oss.volcenginetos import com.volcengine.tos.TOSV2 import com.volcengine.tos.TOSV2ClientBuilder @@ -11,6 +11,7 @@ import io.github.truenine.composeserver.oss.InitiateMultipartUploadRequest import io.github.truenine.composeserver.oss.ListObjectsRequest import io.github.truenine.composeserver.oss.ShareLinkRequest import io.github.truenine.composeserver.oss.UploadPartRequest +import io.github.truenine.composeserver.oss.volcengine.VolcengineTosObjectStorageService import java.io.ByteArrayInputStream import java.time.Duration import kotlin.test.assertEquals @@ -24,13 +25,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledIf /** - * Volcengine TOS 对象存储服务集成测试 + * Volcengine TOS Object Storage Service Integration Tests * - * 该测试需要真实的 TOS 服务凭证,通过环境变量提供: - * - VOLCENGINE_TOS_ACCESS_KEY: TOS 访问密钥 - * - VOLCENGINE_TOS_SECRET_KEY: TOS 秘密密钥 + * These tests require real TOS service credentials provided through environment variables: + * - VOLCENGINE_TOS_ACCESS_KEY: TOS access key + * - VOLCENGINE_TOS_SECRET_KEY: TOS secret key * - * 如果环境变量不存在,测试将被跳过 + * If environment variables are not present, tests will be skipped * * @author TrueNine * @since 2025-08-05 @@ -41,22 +42,8 @@ class VolcengineTosObjectStorageServiceIntegrationTest { companion object { @JvmStatic private val log = logger() - /** 检查是否存在必需的环境变量 用于 JUnit5 的条件测试 */ - @JvmStatic - fun hasRequiredEnvironmentVariables(): Boolean { - val accessKey = System.getenv("VOLCENGINE_TOS_ACCESS_KEY") - val secretKey = System.getenv("VOLCENGINE_TOS_SECRET_KEY") - - val hasCredentials = !accessKey.isNullOrBlank() && !secretKey.isNullOrBlank() - - if (!hasCredentials) { - log.warn("跳过 Volcengine TOS 集成测试:缺少必需的环境变量 VOLCENGINE_TOS_ACCESS_KEY 或 VOLCENGINE_TOS_SECRET_KEY") - } else { - log.info("检测到 Volcengine TOS 凭证,将执行集成测试") - } - - return hasCredentials - } + /** Check if required environment variables exist for JUnit5 conditional testing */ + @JvmStatic fun hasRequiredEnvironmentVariables() = hasTosRequiredEnvironmentVariables() } private lateinit var service: VolcengineTosObjectStorageService @@ -67,39 +54,39 @@ class VolcengineTosObjectStorageServiceIntegrationTest { @BeforeEach fun setUp() { - // 读取环境变量 - accessKey = System.getenv("VOLCENGINE_TOS_ACCESS_KEY") ?: throw IllegalStateException("VOLCENGINE_TOS_ACCESS_KEY 环境变量未设置") - secretKey = System.getenv("VOLCENGINE_TOS_SECRET_KEY") ?: throw IllegalStateException("VOLCENGINE_TOS_SECRET_KEY 环境变量未设置") - - log.info("使用 Access Key: ${accessKey.take(8)}... 进行集成测试") + getTosAkSk()?.also { + accessKey = it.ak + secretKey = it.sk + log.info("Using Access Key: ${accessKey.take(8)}... for integration testing") - // 创建真实的 TOS 客户端 - tosClient = TOSV2ClientBuilder().build("cn-beijing", "https://tos-cn-beijing.volces.com", accessKey, secretKey) + // Create real TOS client + tosClient = TOSV2ClientBuilder().build("cn-beijing", it.endpoint, accessKey, secretKey) - // 创建服务实例 - service = VolcengineTosObjectStorageService(tosClient = tosClient, exposedBaseUrl = "https://tos-cn-beijing.volces.com") + // Create service instance + service = VolcengineTosObjectStorageService(tosClient = tosClient, exposedBaseUrl = it.endpoint) - log.info("TOS 客户端和服务实例创建完成") + log.info("TOS client and service instance created successfully") + } } @AfterEach fun tearDown() { - // 清理所有测试创建的存储桶 + // Clean up all test-created buckets testBuckets.forEach { bucketName -> try { runBlocking { - // 先删除桶中的所有对象 + // First delete all objects in the bucket val listResult = service.listObjects(ListObjectsRequest(bucketName = bucketName)) if (listResult.isSuccess) { val objects = listResult.getOrThrow().objects objects.forEach { obj -> service.deleteObject(bucketName, obj.objectName) } } - // 然后删除存储桶 + // Then delete the bucket service.deleteBucket(bucketName) } - log.info("清理测试存储桶: {}", bucketName) + log.info("Cleaned up test bucket: {}", bucketName) } catch (e: Exception) { - log.warn("清理存储桶失败: {} - {}", bucketName, e.message) + log.warn("Failed to clean up bucket: {} - {}", bucketName, e.message) } } testBuckets.clear() @@ -109,13 +96,13 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class HealthCheck { @Test - fun `测试健康检查`() = runBlocking { - log.info("开始健康检查测试") + fun `test health check`() = runBlocking { + log.info("Starting health check test") val result = service.isHealthy() - assertTrue(result, "TOS 服务健康检查应该成功") + assertTrue(result, "TOS service health check should succeed") - log.info("健康检查测试完成") + log.info("Health check test completed") } } @@ -123,45 +110,45 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class BucketOperations { @Test - fun `测试存储桶基本操作`() = runBlocking { + fun `test bucket basic operations`() = runBlocking { val testBucketName = "test-integration-bucket-${System.currentTimeMillis()}" testBuckets.add(testBucketName) - log.info("开始存储桶操作测试,桶名: {}", testBucketName) + log.info("Starting bucket operations test, bucket name: {}", testBucketName) - // 1. 创建存储桶 + // 1. Create bucket val createResult = service.createBucket(CreateBucketRequest(testBucketName)) - assertTrue(createResult.isSuccess, "创建存储桶应该成功") + assertTrue(createResult.isSuccess, "Creating bucket should succeed") val bucketInfo = createResult.getOrThrow() assertEquals(testBucketName, bucketInfo.name) - // 2. 检查存储桶是否存在 + // 2. Check if bucket exists val existsResult = service.bucketExists(testBucketName) - assertTrue(existsResult.isSuccess, "检查存储桶存在应该成功") - assertTrue(existsResult.getOrThrow(), "存储桶应该存在") + assertTrue(existsResult.isSuccess, "Checking bucket existence should succeed") + assertTrue(existsResult.getOrThrow(), "Bucket should exist") - // 3. 列出存储桶 + // 3. List buckets val listResult = service.listBuckets() - assertTrue(listResult.isSuccess, "列出存储桶应该成功") + assertTrue(listResult.isSuccess, "Listing buckets should succeed") val buckets = listResult.getOrThrow() - assertTrue(buckets.any { it.name == testBucketName }, "存储桶列表应该包含新创建的桶") + assertTrue(buckets.any { it.name == testBucketName }, "Bucket list should contain the newly created bucket") - log.info("存储桶操作测试完成") + log.info("Bucket operations test completed") } @Test - fun `测试存储桶权限设置`() = runBlocking { + fun `test bucket permission settings`() = runBlocking { val testBucketName = "test-integration-acl-bucket-${System.currentTimeMillis()}" testBuckets.add(testBucketName) - log.info("开始存储桶权限测试,桶名: {}", testBucketName) + log.info("Starting bucket permission test, bucket name: {}", testBucketName) - // 创建存储桶 + // Create bucket service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() - // 设置公共读权限 + // Set public read permission val aclResult = service.setBucketPublicRead(testBucketName) - assertTrue(aclResult.isSuccess, "设置存储桶公共读权限应该成功") + assertTrue(aclResult.isSuccess, "Setting bucket public read permission should succeed") - log.info("存储桶权限测试完成") + log.info("Bucket permission test completed") } } @@ -169,18 +156,18 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class ObjectOperations { @Test - fun `测试对象基本操作`() = runBlocking { + fun `test object basic operations`() = runBlocking { val testBucketName = "test-integration-object-bucket-${System.currentTimeMillis()}" val testObjectName = "test-object.txt" val testContent = "Hello, Volcengine TOS Integration Test!" testBuckets.add(testBucketName) - log.info("开始对象操作测试,桶名: {}, 对象名: {}", testBucketName, testObjectName) + log.info("Starting object operations test, bucket name: {}, object name: {}", testBucketName, testObjectName) - // 1. 创建存储桶 + // 1. Create bucket service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() - // 2. 上传对象 + // 2. Upload object val inputStream = ByteArrayInputStream(testContent.toByteArray()) val uploadResult = service.putObject( @@ -190,55 +177,55 @@ class VolcengineTosObjectStorageServiceIntegrationTest { size = testContent.length.toLong(), contentType = "text/plain", ) - assertTrue(uploadResult.isSuccess, "上传对象应该成功") + assertTrue(uploadResult.isSuccess, "Upload object should succeed") val objectInfo = uploadResult.getOrThrow() assertEquals(testBucketName, objectInfo.bucketName) assertEquals(testObjectName, objectInfo.objectName) - // 3. 检查对象是否存在 + // 3. Check if object exists val existsResult = service.objectExists(testBucketName, testObjectName) - assertTrue(existsResult.isSuccess, "检查对象存在应该成功") - assertTrue(existsResult.getOrThrow(), "对象应该存在") + assertTrue(existsResult.isSuccess, "Checking object existence should succeed") + assertTrue(existsResult.getOrThrow(), "Object should exist") - // 4. 获取对象信息 + // 4. Get object information val infoResult = service.getObjectInfo(testBucketName, testObjectName) - assertTrue(infoResult.isSuccess, "获取对象信息应该成功") + assertTrue(infoResult.isSuccess, "Getting object information should succeed") val retrievedInfo = infoResult.getOrThrow() assertEquals(testBucketName, retrievedInfo.bucketName) assertEquals(testObjectName, retrievedInfo.objectName) - // 5. 下载对象 + // 5. Download object val downloadResult = service.getObject(testBucketName, testObjectName) - assertTrue(downloadResult.isSuccess, "下载对象应该成功") + assertTrue(downloadResult.isSuccess, "Download object should succeed") val objectContent = downloadResult.getOrThrow() val downloadedContent = objectContent.inputStream.readBytes().toString(Charsets.UTF_8) - assertEquals(testContent, downloadedContent, "下载的内容应该与上传的内容一致") + assertEquals(testContent, downloadedContent, "Downloaded content should match uploaded content") - // 6. 列出对象 + // 6. List objects val listResult = service.listObjects(ListObjectsRequest(bucketName = testBucketName)) - assertTrue(listResult.isSuccess, "列出对象应该成功") + assertTrue(listResult.isSuccess, "List objects should succeed") val objects = listResult.getOrThrow().objects - assertTrue(objects.any { it.objectName == testObjectName }, "对象列表应该包含上传的对象") + assertTrue(objects.any { it.objectName == testObjectName }, "Object list should contain the uploaded object") - log.info("对象操作测试完成") + log.info("Object operations test completed") } @Test - fun `测试对象复制操作`() = runBlocking { + fun `test object copy operations`() = runBlocking { val testBucketName = "test-integration-copy-bucket-${System.currentTimeMillis()}" val sourceObjectName = "source-object.txt" val targetObjectName = "target-object.txt" val testContent = "Content for copy test" testBuckets.add(testBucketName) - log.info("开始对象复制测试") + log.info("Starting object copy test") - // 创建存储桶并上传源对象 + // Create bucket and upload source object service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() val inputStream = ByteArrayInputStream(testContent.toByteArray()) service.putObject(testBucketName, sourceObjectName, inputStream, testContent.length.toLong()).getOrThrow() - // 复制对象 + // Copy object val copyRequest = CopyObjectRequest( sourceBucketName = testBucketName, @@ -247,13 +234,13 @@ class VolcengineTosObjectStorageServiceIntegrationTest { destinationObjectName = targetObjectName, ) val copyResult = service.copyObject(copyRequest) - assertTrue(copyResult.isSuccess, "复制对象应该成功") + assertTrue(copyResult.isSuccess, "Copy object should succeed") - // 验证目标对象存在 + // Verify target object exists val existsResult = service.objectExists(testBucketName, targetObjectName) - assertTrue(existsResult.isSuccess && existsResult.getOrThrow(), "复制的对象应该存在") + assertTrue(existsResult.isSuccess && existsResult.getOrThrow(), "Copied object should exist") - log.info("对象复制测试完成") + log.info("Object copy test completed") } } @@ -261,27 +248,27 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class PresignedUrlOperations { @Test - fun `测试预签名URL生成`() = runBlocking { + fun `test presigned URL generation`() = runBlocking { val testBucketName = "test-integration-presigned-bucket-${System.currentTimeMillis()}" val testObjectName = "test-presigned-object.txt" testBuckets.add(testBucketName) - log.info("开始预签名URL测试,桶名: {}, 对象名: {}", testBucketName, testObjectName) + log.info("Starting presigned URL test, bucket name: {}, object name: {}", testBucketName, testObjectName) - // 1. 创建存储桶和对象 + // 1. Create bucket and object service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() val inputStream = ByteArrayInputStream("test content".toByteArray()) service.putObject(testBucketName, testObjectName, inputStream, 12L).getOrThrow() - // 2. 生成GET预签名URL + // 2. Generate GET presigned URL val getUrlResult = service.generatePresignedUrl(bucketName = testBucketName, objectName = testObjectName, expiration = Duration.ofHours(1), method = HttpMethod.GET) - assertTrue(getUrlResult.isSuccess, "生成GET预签名URL应该成功") + assertTrue(getUrlResult.isSuccess, "Generating GET presigned URL should succeed") val getPresignedUrl = getUrlResult.getOrThrow() - assertTrue(getPresignedUrl.isNotEmpty(), "GET预签名URL不应该为空") - assertTrue(getPresignedUrl.startsWith("https://"), "GET预签名URL应该是HTTPS协议") + assertTrue(getPresignedUrl.isNotEmpty(), "GET presigned URL should not be empty") + assertTrue(getPresignedUrl.startsWith("https://"), "GET presigned URL should use HTTPS protocol") - // 3. 生成PUT预签名URL + // 3. Generate PUT presigned URL val putUrlResult = service.generatePresignedUrl( bucketName = testBucketName, @@ -289,12 +276,12 @@ class VolcengineTosObjectStorageServiceIntegrationTest { expiration = Duration.ofMinutes(30), method = HttpMethod.PUT, ) - assertTrue(putUrlResult.isSuccess, "生成PUT预签名URL应该成功") + assertTrue(putUrlResult.isSuccess, "Generating PUT presigned URL should succeed") val putPresignedUrl = putUrlResult.getOrThrow() - assertTrue(putPresignedUrl.isNotEmpty(), "PUT预签名URL不应该为空") - assertTrue(putPresignedUrl.startsWith("https://"), "PUT预签名URL应该是HTTPS协议") + assertTrue(putPresignedUrl.isNotEmpty(), "PUT presigned URL should not be empty") + assertTrue(putPresignedUrl.startsWith("https://"), "PUT presigned URL should use HTTPS protocol") - log.info("预签名URL测试完成") + log.info("Presigned URL test completed") } } @@ -302,7 +289,7 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class MultipartUploadOperations { @Test - fun `测试多部分上传`() = runBlocking { + fun `test multipart upload`() = runBlocking { val testBucketName = "test-integration-multipart-bucket-${System.currentTimeMillis()}" val testObjectName = "test-multipart-object.txt" // TOS requires each part (except the last one) to be at least 5MB @@ -310,20 +297,20 @@ class VolcengineTosObjectStorageServiceIntegrationTest { val partContent2 = "B".repeat(5 * 1024 * 1024) // 5MB testBuckets.add(testBucketName) - log.info("开始多部分上传测试") + log.info("Starting multipart upload test") - // 创建存储桶 + // Create bucket service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() - // 1. 初始化多部分上传 + // 1. Initiate multipart upload val initiateRequest = InitiateMultipartUploadRequest(bucketName = testBucketName, objectName = testObjectName) val initiateResult = service.initiateMultipartUpload(initiateRequest) - assertTrue(initiateResult.isSuccess, "初始化多部分上传应该成功") + assertTrue(initiateResult.isSuccess, "Initiating multipart upload should succeed") val multipartUpload = initiateResult.getOrThrow() - assertNotNull(multipartUpload.uploadId, "上传ID不应该为空") + assertNotNull(multipartUpload.uploadId, "Upload ID should not be null") try { - // 2. 上传第一部分 + // 2. Upload first part val part1Request = UploadPartRequest( bucketName = testBucketName, @@ -334,10 +321,10 @@ class VolcengineTosObjectStorageServiceIntegrationTest { size = partContent1.length.toLong(), ) val part1Result = service.uploadPart(part1Request) - assertTrue(part1Result.isSuccess, "上传第一部分应该成功") + assertTrue(part1Result.isSuccess, "Uploading first part should succeed") val part1Info = part1Result.getOrThrow() - // 3. 上传第二部分 + // 3. Upload second part val part2Request = UploadPartRequest( bucketName = testBucketName, @@ -348,16 +335,16 @@ class VolcengineTosObjectStorageServiceIntegrationTest { size = partContent2.length.toLong(), ) val part2Result = service.uploadPart(part2Request) - assertTrue(part2Result.isSuccess, "上传第二部分应该成功") + assertTrue(part2Result.isSuccess, "Uploading second part should succeed") val part2Info = part2Result.getOrThrow() - // 4. 列出已上传的部分 + // 4. List uploaded parts val listPartsResult = service.listParts(multipartUpload.uploadId, testBucketName, testObjectName) - assertTrue(listPartsResult.isSuccess, "列出部分应该成功") + assertTrue(listPartsResult.isSuccess, "Listing parts should succeed") val parts = listPartsResult.getOrThrow() - assertEquals(2, parts.size, "应该有两个部分") + assertEquals(2, parts.size, "Should have two parts") - // 5. 完成多部分上传 + // 5. Complete multipart upload val completeRequest = CompleteMultipartUploadRequest( bucketName = testBucketName, @@ -366,15 +353,15 @@ class VolcengineTosObjectStorageServiceIntegrationTest { parts = listOf(part1Info, part2Info), ) val completeResult = service.completeMultipartUpload(completeRequest) - assertTrue(completeResult.isSuccess, "完成多部分上传应该成功") + assertTrue(completeResult.isSuccess, "Completing multipart upload should succeed") - // 6. 验证对象存在 + // 6. Verify object exists val existsResult = service.objectExists(testBucketName, testObjectName) - assertTrue(existsResult.isSuccess && existsResult.getOrThrow(), "多部分上传的对象应该存在") + assertTrue(existsResult.isSuccess && existsResult.getOrThrow(), "Multipart uploaded object should exist") - log.info("多部分上传测试完成") + log.info("Multipart upload test completed") } catch (e: Exception) { - // 如果出现异常,尝试中止上传 + // If an exception occurs, try to abort the upload service.abortMultipartUpload(multipartUpload.uploadId, testBucketName, testObjectName) throw e } @@ -385,31 +372,31 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class ShareLinkOperations { @Test - fun `测试分享链接生成和验证`() = runBlocking { + fun `test share link generation and validation`() = runBlocking { val testBucketName = "test-integration-share-bucket-${System.currentTimeMillis()}" val testObjectName = "test-share-object.txt" val testContent = "Content for share link test" testBuckets.add(testBucketName) - log.info("开始分享链接测试") + log.info("Starting share link test") - // 创建存储桶并上传对象 + // Create bucket and upload object service.createBucket(CreateBucketRequest(testBucketName)).getOrThrow() val inputStream = ByteArrayInputStream(testContent.toByteArray()) service.putObject(testBucketName, testObjectName, inputStream, testContent.length.toLong()).getOrThrow() - // 生成分享链接 + // Generate share link val shareRequest = ShareLinkRequest(bucketName = testBucketName, objectName = testObjectName, expiration = Duration.ofHours(2), method = HttpMethod.GET) val shareResult = service.generateShareLink(shareRequest) - assertTrue(shareResult.isSuccess, "生成分享链接应该成功") + assertTrue(shareResult.isSuccess, "Generating share link should succeed") val shareInfo = shareResult.getOrThrow() - assertTrue(shareInfo.shareUrl.isNotEmpty(), "分享链接不应该为空") + assertTrue(shareInfo.shareUrl.isNotEmpty(), "Share link should not be empty") - // 验证分享链接 + // Validate share link val validateResult = service.validateShareLink(shareInfo.shareUrl) - assertTrue(validateResult.isSuccess, "验证分享链接应该成功") + assertTrue(validateResult.isSuccess, "Validating share link should succeed") - log.info("分享链接测试完成") + log.info("Share link test completed") } } @@ -417,15 +404,15 @@ class VolcengineTosObjectStorageServiceIntegrationTest { inner class EnvironmentVariables { @Test - fun `测试环境变量读取`() { - log.info("验证环境变量读取") + fun `test environment variable reading`() { + log.info("Verifying environment variable reading") - assertNotNull(accessKey, "VOLCENGINE_TOS_ACCESS_KEY 应该不为空") - assertNotNull(secretKey, "VOLCENGINE_TOS_SECRET_KEY 应该不为空") - assertTrue(accessKey.isNotBlank(), "VOLCENGINE_TOS_ACCESS_KEY 应该不为空白") - assertTrue(secretKey.isNotBlank(), "VOLCENGINE_TOS_SECRET_KEY 应该不为空白") + assertNotNull(accessKey, "VOLCENGINE_TOS_ACCESS_KEY should not be null") + assertNotNull(secretKey, "VOLCENGINE_TOS_SECRET_KEY should not be null") + assertTrue(accessKey.isNotBlank(), "VOLCENGINE_TOS_ACCESS_KEY should not be blank") + assertTrue(secretKey.isNotBlank(), "VOLCENGINE_TOS_SECRET_KEY should not be blank") - log.info("环境变量验证完成") + log.info("Environment variable verification completed") } } } diff --git a/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfiguration.kt b/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfiguration.kt index 4f3ee2e76..3013110b3 100644 --- a/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfiguration.kt +++ b/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfiguration.kt @@ -124,28 +124,34 @@ class VolcengineTosAutoConfiguration { /** Resolve endpoint with detailed logging */ private fun resolveEndpoint(tosProperties: VolcengineTosProperties, ossProperties: OssProperties): String { val endpoint = tosProperties.endpoint ?: ossProperties.endpoint - log.debug("Resolved endpoint: $endpoint (from ${if (tosProperties.endpoint != null) "TOS properties" else "OSS properties"})") + log.debug("Resolved endpoint: {} (from {})", endpoint, if (tosProperties.endpoint != null) "TOS properties" else "OSS properties") return endpoint ?: throw IllegalArgumentException("TOS endpoint is required") } /** Resolve region with detailed logging */ private fun resolveRegion(tosProperties: VolcengineTosProperties, ossProperties: OssProperties): String { - val region = tosProperties.region ?: ossProperties.region - log.debug("Resolved region: $region (from ${if (tosProperties.region != null) "TOS properties" else "OSS properties"})") - return region ?: throw IllegalArgumentException("TOS region is required") + val region = + tosProperties.region + ?: ossProperties.region + ?: run { + log.warn("No region specified, using default region: cn-beijing") + "cn-beijing" + } + log.debug("Resolved region: {} (from {})", region, if (tosProperties.region != null) "TOS properties" else "OSS properties") + return region } /** Resolve access key with detailed logging */ private fun resolveAccessKey(tosProperties: VolcengineTosProperties, ossProperties: OssProperties): String { val accessKey = tosProperties.accessKey ?: ossProperties.accessKey - log.debug("Resolved access key: ${accessKey?.take(4)}*** (from ${if (tosProperties.accessKey != null) "TOS properties" else "OSS properties"})") + log.debug("Resolved access key: {}*** (from {})", accessKey?.take(4), if (tosProperties.accessKey != null) "TOS properties" else "OSS properties") return accessKey ?: throw IllegalArgumentException("TOS access key is required") } /** Resolve secret key with detailed logging */ private fun resolveSecretKey(tosProperties: VolcengineTosProperties, ossProperties: OssProperties): String { val secretKey = tosProperties.secretKey ?: ossProperties.secretKey - log.debug("Secret key resolved from ${if (tosProperties.secretKey != null) "TOS properties" else "OSS properties"}") + log.debug("Secret key resolved from {}", if (tosProperties.secretKey != null) "TOS properties" else "OSS properties") return secretKey ?: throw IllegalArgumentException("TOS secret key is required") } @@ -156,7 +162,7 @@ class VolcengineTosAutoConfiguration { require(accessKey.isNotBlank()) { "TOS access key cannot be blank" } require(secretKey.isNotBlank()) { "TOS secret key cannot be blank" } - log.debug("✅ All required parameters validated successfully") + log.debug("All required parameters validated successfully") } /** Log configuration summary without sensitive information */ @@ -255,7 +261,7 @@ class VolcengineTosAutoConfiguration { } if (tosProperties.userAgentCustomizedKeyValues.isNotEmpty()) { builder.userAgentCustomizedKeyValues(tosProperties.userAgentCustomizedKeyValues) - log.debug("🏷️ Custom User-Agent values: ${tosProperties.userAgentCustomizedKeyValues}") + log.debug("Custom User-Agent values: {}", tosProperties.userAgentCustomizedKeyValues) } val config = builder.build() @@ -271,7 +277,6 @@ class VolcengineTosAutoConfiguration { ?: tosProperties.customDomain ?: tosProperties.getEffectiveEndpoint() ?: ossProperties.getEffectiveEndpoint() - ?: throw IllegalArgumentException("Unable to determine exposed base URL") log.debug("Resolved exposed base URL: $exposedBaseUrl") return exposedBaseUrl diff --git a/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/properties/VolcengineTosProperties.kt b/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/properties/VolcengineTosProperties.kt index a571b71c9..4e619668d 100644 --- a/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/properties/VolcengineTosProperties.kt +++ b/oss/oss-volcengine-tos/src/main/kotlin/io/github/truenine/composeserver/oss/volcengine/properties/VolcengineTosProperties.kt @@ -43,10 +43,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = SpringBootConfigurationPropertiesPrefixes.OSS_VOLCENGINE_TOS) data class VolcengineTosProperties( /** Service endpoint URL */ - var endpoint: String? = null, + var endpoint: String? = DEFAULT_ENDPOINT, /** Service region */ - var region: String? = null, + var region: String? = "cn-beijing", /** Access key for authentication */ var accessKey: String? = null, @@ -58,7 +58,7 @@ data class VolcengineTosProperties( var sessionToken: String? = null, /** Public base URL for object access */ - var exposedBaseUrl: String? = null, + var exposedBaseUrl: String? = DEFAULT_EXPOSED_BASE_URL, /** Enable SSL/TLS connection */ var enableSsl: Boolean = true, @@ -166,6 +166,11 @@ data class VolcengineTosProperties( /** Check if proxy authentication is configured */ fun hasProxyAuthentication(): Boolean = hasProxyConfiguration() && !proxyUserName.isNullOrBlank() && !proxyPassword.isNullOrBlank() + companion object { + const val DEFAULT_ENDPOINT = "https://tos-cn-beijing.volces.com" + const val DEFAULT_EXPOSED_BASE_URL = "https://tos-cn-beijing.volces.com" + } + override fun toString(): String { return "VolcengineTosProperties(" + "endpoint='$endpoint', " + diff --git a/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/RegionResolutionTest.kt b/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/RegionResolutionTest.kt new file mode 100644 index 000000000..a0cf15f2a --- /dev/null +++ b/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/RegionResolutionTest.kt @@ -0,0 +1,52 @@ +package io.github.truenine.composeserver.oss.volcengine.autoconfig + +import io.github.truenine.composeserver.oss.properties.OssProperties +import io.github.truenine.composeserver.oss.volcengine.properties.VolcengineTosProperties +import kotlin.test.assertEquals +import org.junit.jupiter.api.Test + +/** Simple region resolution logic tests */ +class RegionResolutionTest { + + @Test + fun `resolveRegion should use TOS region when specified`() { + val config = VolcengineTosAutoConfiguration() + val tosProps = VolcengineTosProperties().apply { region = "cn-shanghai" } + val ossProps = OssProperties().apply { region = "cn-beijing" } + + // Use reflection to call private method + val method = VolcengineTosAutoConfiguration::class.java.getDeclaredMethod("resolveRegion", VolcengineTosProperties::class.java, OssProperties::class.java) + method.isAccessible = true + val result = method.invoke(config, tosProps, ossProps) as String + + assertEquals("cn-shanghai", result) + } + + @Test + fun `resolveRegion should use OSS region when TOS region is null`() { + val config = VolcengineTosAutoConfiguration() + val tosProps = VolcengineTosProperties().apply { region = null } + val ossProps = OssProperties().apply { region = "cn-guangzhou" } + + // Use reflection to call private method + val method = VolcengineTosAutoConfiguration::class.java.getDeclaredMethod("resolveRegion", VolcengineTosProperties::class.java, OssProperties::class.java) + method.isAccessible = true + val result = method.invoke(config, tosProps, ossProps) as String + + assertEquals("cn-guangzhou", result) + } + + @Test + fun `resolveRegion should use default cn-beijing when both regions are null`() { + val config = VolcengineTosAutoConfiguration() + val tosProps = VolcengineTosProperties().apply { region = null } + val ossProps = OssProperties().apply { region = null } + + // Use reflection to call private method + val method = VolcengineTosAutoConfiguration::class.java.getDeclaredMethod("resolveRegion", VolcengineTosProperties::class.java, OssProperties::class.java) + method.isAccessible = true + val result = method.invoke(config, tosProps, ossProps) as String + + assertEquals("cn-beijing", result) + } +} diff --git a/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfigurationTest.kt b/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfigurationTest.kt index 712aa50a3..2c89d32ae 100644 --- a/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfigurationTest.kt +++ b/oss/oss-volcengine-tos/src/test/kotlin/io/github/truenine/composeserver/oss/volcengine/autoconfig/VolcengineTosAutoConfigurationTest.kt @@ -7,8 +7,11 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.autoconfigure.AutoConfigurations import org.springframework.boot.test.context.runner.ApplicationContextRunner +import org.springframework.boot.test.system.CapturedOutput +import org.springframework.boot.test.system.OutputCaptureExtension /** * 测试 Volcengine TOS 自动配置机制 @@ -178,6 +181,171 @@ class VolcengineTosAutoConfigurationTest { } } + @Nested + inner class `默认区域配置测试` { + + @Test + fun `当未指定区域时应该使用默认区域 cn-beijing`() { + contextRunner + .withPropertyValues( + "compose.oss.volcengine-tos.endpoint=tos-cn-beijing.volces.com", + "compose.oss.volcengine-tos.access-key=testkey", + "compose.oss.volcengine-tos.secret-key=testsecret", + ) + .run { context -> + // 验证配置属性中region为null + val tosProperties = context.getBean(VolcengineTosProperties::class.java) + kotlin.test.assertNull(tosProperties.region) + + val ossProperties = context.getBean(OssProperties::class.java) + kotlin.test.assertNull(ossProperties.region) + + // 验证 TOS 客户端仍然被创建(使用默认区域) + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + assertTrue(context.containsBeanDefinition("volcengineTosObjectStorageService")) + } + } + + @Test + fun `TOS 专用区域配置应该优先于通用 OSS 区域配置`() { + contextRunner + .withPropertyValues( + "compose.oss.endpoint=tos-cn-beijing.volces.com", + "compose.oss.region=cn-shanghai", + "compose.oss.access-key=testkey", + "compose.oss.secret-key=testsecret", + "compose.oss.volcengine-tos.region=cn-guangzhou", + ) + .run { context -> + val tosProperties = context.getBean(VolcengineTosProperties::class.java) + val ossProperties = context.getBean(OssProperties::class.java) + + // 验证 TOS 专用配置优先 + kotlin.test.assertEquals("cn-guangzhou", tosProperties.region) + kotlin.test.assertEquals("cn-shanghai", ossProperties.region) + + // 验证 TOS 客户端被创建 + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + + @Test + fun `通用 OSS 区域配置应该作为 TOS 区域的后备选项`() { + contextRunner + .withPropertyValues( + "compose.oss.endpoint=tos-cn-beijing.volces.com", + "compose.oss.region=cn-hongkong", + "compose.oss.access-key=testkey", + "compose.oss.secret-key=testsecret", + ) + .run { context -> + val tosProperties = context.getBean(VolcengineTosProperties::class.java) + val ossProperties = context.getBean(OssProperties::class.java) + + // 验证 TOS 没有专用区域配置 + kotlin.test.assertNull(tosProperties.region) + // 验证通用配置存在 + kotlin.test.assertEquals("cn-hongkong", ossProperties.region) + + // 验证 TOS 客户端被创建(使用通用配置) + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + + @Test + fun `应该支持所有有效的火山引擎区域代码`() { + val validRegions = listOf("cn-beijing", "cn-shanghai", "cn-guangzhou", "cn-hongkong", "ap-southeast-1") + + validRegions.forEach { region -> + contextRunner + .withPropertyValues( + "compose.oss.volcengine-tos.endpoint=tos-$region.volces.com", + "compose.oss.volcengine-tos.region=$region", + "compose.oss.volcengine-tos.access-key=testkey", + "compose.oss.volcengine-tos.secret-key=testsecret", + ) + .run { context -> + val tosProperties = context.getBean(VolcengineTosProperties::class.java) + kotlin.test.assertEquals(region, tosProperties.region) + + // 验证 TOS 客户端被创建 + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + } + } + + @Nested + @ExtendWith(OutputCaptureExtension::class) + inner class `日志输出验证` { + + @Test + fun `当未指定区域时应该输出默认区域警告日志`(output: CapturedOutput) { + contextRunner + .withPropertyValues( + "compose.oss.volcengine-tos.endpoint=tos-cn-beijing.volces.com", + "compose.oss.volcengine-tos.access-key=testkey", + "compose.oss.volcengine-tos.secret-key=testsecret", + "spring.profiles.active=test", // 启用测试环境以跳过连接测试 + ) + .run { context -> + // 验证警告日志被输出 + kotlin.test.assertTrue( + output.out.contains("No region specified, using default region: cn-beijing") || + output.err.contains("No region specified, using default region: cn-beijing"), + "Expected warning log about default region not found in output: ${output.all}", + ) + + // 验证 TOS 客户端被创建 + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + + @Test + fun `当指定了区域时不应该输出默认区域警告日志`(output: CapturedOutput) { + contextRunner + .withPropertyValues( + "compose.oss.volcengine-tos.endpoint=tos-cn-beijing.volces.com", + "compose.oss.volcengine-tos.region=cn-shanghai", + "compose.oss.volcengine-tos.access-key=testkey", + "compose.oss.volcengine-tos.secret-key=testsecret", + "spring.profiles.active=test", // 启用测试环境以跳过连接测试 + ) + .run { context -> + // 验证没有默认区域警告日志 + kotlin.test.assertFalse( + output.out.contains("No region specified, using default region") || output.err.contains("No region specified, using default region"), + "Should not output default region warning when region is specified", + ) + + // 验证 TOS 客户端被创建 + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + + @Test + fun `当使用通用OSS区域配置时不应该输出默认区域警告日志`(output: CapturedOutput) { + contextRunner + .withPropertyValues( + "compose.oss.endpoint=tos-cn-beijing.volces.com", + "compose.oss.region=cn-guangzhou", + "compose.oss.access-key=testkey", + "compose.oss.secret-key=testsecret", + "spring.profiles.active=test", // 启用测试环境以跳过连接测试 + ) + .run { context -> + // 验证没有默认区域警告日志 + kotlin.test.assertFalse( + output.out.contains("No region specified, using default region") || output.err.contains("No region specified, using default region"), + "Should not output default region warning when OSS region is specified", + ) + + // 验证 TOS 客户端被创建 + assertTrue(context.containsBeanDefinition("volcengineTosClient")) + } + } + } + @Nested inner class `Bean 定义验证` { diff --git a/security/security-spring/build.gradle.kts b/security/security-spring/build.gradle.kts index d685e35b1..dfc054060 100644 --- a/security/security-spring/build.gradle.kts +++ b/security/security-spring/build.gradle.kts @@ -20,9 +20,7 @@ dependencies { implementation(libs.org.springframework.spring.webmvc) - implementation(libs.org.owasp.antisamy.antisamy) { - exclude(group = "org.htmlunit", module = "neko-htmlunit") - } + implementation(libs.org.owasp.antisamy.antisamy) { exclude(group = "org.htmlunit", module = "neko-htmlunit") } implementation(libs.org.htmlunit.neko.htmlunit) // implementation(project(":depend:depend-http-exchange")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 4091d6eb1..8053a12d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -119,4 +119,6 @@ include("integrate-test:depend:jackson") include("integrate-test:oss:minio") +include("integrate-test:oss:volcengine-tos") + include("integrate-test:cacheable")