Add built-in storage support from v-hono-storage module: - StorageProvider interface with 4 backends - FileService with HTTP handlers - Multipart upload support - 86 property-based tests - Retry mechanism with exponential backoff Breaking: None (backward compatible)
874 lines
25 KiB
V
874 lines
25 KiB
V
module main
|
|
|
|
import rand
|
|
import time
|
|
import crypto.sha1
|
|
import encoding.base64
|
|
import net.urllib
|
|
|
|
// ============================================================================
|
|
// Property 1: Upload-Download Round Trip (OSS)
|
|
// Property 4: Presigned URL Validity (OSS)
|
|
// Feature: v-hono-upload-integration, Property 1 & 4
|
|
// Validates: Requirements 4.4, 4.5
|
|
//
|
|
// Since we cannot test against a real Aliyun OSS server without credentials,
|
|
// these tests verify:
|
|
// 1. Aliyun OSS storage provider creation and configuration validation
|
|
// 2. Presigned URL generation format and structure
|
|
// 3. Aliyun OSS signing algorithm correctness
|
|
// 4. Internal/External endpoint switching
|
|
// ============================================================================
|
|
|
|
const test_iterations = 100
|
|
|
|
// ============================================================================
|
|
// Type definitions (copied from storage modules for standalone testing)
|
|
// ============================================================================
|
|
|
|
struct StorageResult {
|
|
pub:
|
|
success bool
|
|
object_key string
|
|
etag string
|
|
size i64
|
|
error_msg string
|
|
}
|
|
|
|
struct ObjectInfo {
|
|
pub:
|
|
key string
|
|
size i64
|
|
etag string
|
|
content_type string
|
|
last_modified i64
|
|
metadata map[string]string
|
|
}
|
|
|
|
struct ListOptions {
|
|
pub:
|
|
prefix string
|
|
delimiter string
|
|
max_keys int = 1000
|
|
start_after string
|
|
}
|
|
|
|
struct ListResult {
|
|
pub:
|
|
objects []ObjectInfo
|
|
common_prefixes []string
|
|
is_truncated bool
|
|
next_marker string
|
|
}
|
|
|
|
struct PresignOptions {
|
|
pub:
|
|
expires_in int = 3600
|
|
method string = 'GET'
|
|
content_type string
|
|
}
|
|
|
|
struct PartInfo {
|
|
pub:
|
|
part_number int
|
|
etag string
|
|
size i64
|
|
}
|
|
|
|
struct AliyunOSSConfig {
|
|
pub:
|
|
endpoint string
|
|
access_key_id string
|
|
access_key_secret string
|
|
internal_endpoint string
|
|
default_bucket string
|
|
}
|
|
|
|
struct ConfigValidationResult {
|
|
pub:
|
|
valid bool
|
|
missing_fields []string
|
|
error_message string
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// AliyunOSS implementation (minimal for testing)
|
|
// ============================================================================
|
|
|
|
struct AliyunOSS {
|
|
config AliyunOSSConfig
|
|
mut:
|
|
multipart_uploads map[string]OSSMultipartUploadState
|
|
use_internal bool
|
|
}
|
|
|
|
struct OSSMultipartUploadState {
|
|
mut:
|
|
bucket string
|
|
key string
|
|
upload_id string
|
|
content_type string
|
|
parts map[int]PartInfo
|
|
created_at i64
|
|
}
|
|
|
|
fn validate_aliyun_oss_config(config AliyunOSSConfig) ConfigValidationResult {
|
|
mut missing := []string{}
|
|
|
|
if config.endpoint == '' {
|
|
missing << 'aliyun_oss.endpoint'
|
|
}
|
|
if config.access_key_id == '' {
|
|
missing << 'aliyun_oss.access_key_id'
|
|
}
|
|
if config.access_key_secret == '' {
|
|
missing << 'aliyun_oss.access_key_secret'
|
|
}
|
|
if config.default_bucket == '' {
|
|
missing << 'aliyun_oss.default_bucket'
|
|
}
|
|
|
|
if missing.len > 0 {
|
|
return ConfigValidationResult{
|
|
valid: false
|
|
missing_fields: missing
|
|
error_message: 'Missing required fields: ${missing.join(", ")}'
|
|
}
|
|
}
|
|
|
|
return ConfigValidationResult{
|
|
valid: true
|
|
missing_fields: []string{}
|
|
error_message: ''
|
|
}
|
|
}
|
|
|
|
fn new_aliyun_oss(config AliyunOSSConfig) !AliyunOSS {
|
|
validation := validate_aliyun_oss_config(config)
|
|
if !validation.valid {
|
|
return error(validation.error_message)
|
|
}
|
|
|
|
return AliyunOSS{
|
|
config: config
|
|
multipart_uploads: map[string]OSSMultipartUploadState{}
|
|
use_internal: false
|
|
}
|
|
}
|
|
|
|
fn new_aliyun_oss_internal(config AliyunOSSConfig) !AliyunOSS {
|
|
validation := validate_aliyun_oss_config(config)
|
|
if !validation.valid {
|
|
return error(validation.error_message)
|
|
}
|
|
|
|
if config.internal_endpoint == '' {
|
|
return error('Internal endpoint is not configured')
|
|
}
|
|
|
|
return AliyunOSS{
|
|
config: config
|
|
multipart_uploads: map[string]OSSMultipartUploadState{}
|
|
use_internal: true
|
|
}
|
|
}
|
|
|
|
fn (o AliyunOSS) provider_name() string {
|
|
return 'aliyun_oss'
|
|
}
|
|
|
|
fn (o AliyunOSS) get_current_endpoint() string {
|
|
if o.use_internal && o.config.internal_endpoint != '' {
|
|
return o.config.internal_endpoint
|
|
}
|
|
return o.config.endpoint
|
|
}
|
|
|
|
fn (mut o AliyunOSS) switch_to_internal() ! {
|
|
if o.config.internal_endpoint == '' {
|
|
return error('Internal endpoint is not configured')
|
|
}
|
|
o.use_internal = true
|
|
}
|
|
|
|
fn (mut o AliyunOSS) switch_to_external() {
|
|
o.use_internal = false
|
|
}
|
|
|
|
fn (o AliyunOSS) get_host(bucket string) string {
|
|
endpoint := o.get_current_endpoint()
|
|
if bucket != '' {
|
|
return '${bucket}.${endpoint}'
|
|
}
|
|
return endpoint
|
|
}
|
|
|
|
|
|
// URL encoding for key (preserving /)
|
|
fn (o AliyunOSS) encode_key(key string) string {
|
|
parts := key.split('/')
|
|
mut encoded_parts := []string{}
|
|
for part in parts {
|
|
encoded_parts << urllib.query_escape(part)
|
|
}
|
|
return encoded_parts.join('/')
|
|
}
|
|
|
|
// Build canonicalized resource
|
|
fn (o AliyunOSS) build_canonicalized_resource(bucket string, key string, query_params map[string]string) string {
|
|
mut resource := ''
|
|
|
|
if bucket != '' {
|
|
resource = '/${bucket}'
|
|
}
|
|
|
|
if key != '' {
|
|
resource += '/${key}'
|
|
} else if bucket != '' {
|
|
resource += '/'
|
|
} else {
|
|
resource = '/'
|
|
}
|
|
|
|
sub_resources := [
|
|
'acl', 'uploads', 'location', 'cors', 'logging', 'website', 'referer',
|
|
'lifecycle', 'delete', 'append', 'tagging', 'objectMeta', 'uploadId',
|
|
'partNumber', 'security-token', 'position', 'img', 'style', 'styleName',
|
|
'replication', 'replicationProgress', 'replicationLocation', 'cname',
|
|
'bucketInfo', 'comp', 'qos', 'live', 'status', 'vod', 'startTime',
|
|
'endTime', 'symlink', 'x-oss-process', 'response-content-type',
|
|
'response-content-language', 'response-expires', 'response-cache-control',
|
|
'response-content-disposition', 'response-content-encoding',
|
|
]
|
|
|
|
mut sub_params := []string{}
|
|
for param in sub_resources {
|
|
if param in query_params {
|
|
value := query_params[param]
|
|
if value != '' {
|
|
sub_params << '${param}=${value}'
|
|
} else {
|
|
sub_params << param
|
|
}
|
|
}
|
|
}
|
|
|
|
if sub_params.len > 0 {
|
|
resource += '?' + sub_params.join('&')
|
|
}
|
|
|
|
return resource
|
|
}
|
|
|
|
// HMAC-SHA1 implementation
|
|
fn oss_test_hmac_sha1(key []u8, data []u8) []u8 {
|
|
block_size := 64
|
|
mut k := key.clone()
|
|
|
|
if k.len > block_size {
|
|
k = sha1.sum(k)
|
|
}
|
|
|
|
for k.len < block_size {
|
|
k << u8(0)
|
|
}
|
|
|
|
mut i_pad := []u8{len: block_size}
|
|
mut o_pad := []u8{len: block_size}
|
|
for i in 0 .. block_size {
|
|
i_pad[i] = k[i] ^ u8(0x36)
|
|
o_pad[i] = k[i] ^ u8(0x5c)
|
|
}
|
|
|
|
mut inner_data := i_pad.clone()
|
|
inner_data << data
|
|
inner_hash := sha1.sum(inner_data)
|
|
|
|
mut outer_data := o_pad.clone()
|
|
outer_data << inner_hash
|
|
return sha1.sum(outer_data)
|
|
}
|
|
|
|
fn (o AliyunOSS) calculate_signature(string_to_sign string) string {
|
|
signature := oss_test_hmac_sha1(o.config.access_key_secret.bytes(), string_to_sign.bytes())
|
|
return base64.encode(signature)
|
|
}
|
|
|
|
|
|
fn (o AliyunOSS) presign_url(bucket string, key string, options PresignOptions) !string {
|
|
now := time.utc()
|
|
expires := now.unix() + i64(options.expires_in)
|
|
|
|
mut content_type := ''
|
|
if options.content_type != '' {
|
|
content_type = options.content_type
|
|
}
|
|
|
|
canonicalized_resource := o.build_canonicalized_resource(bucket, key, map[string]string{})
|
|
|
|
string_to_sign := '${options.method}\n\n${content_type}\n${expires}\n${canonicalized_resource}'
|
|
|
|
signature := o.calculate_signature(string_to_sign)
|
|
|
|
host := o.get_host(bucket)
|
|
encoded_key := o.encode_key(key)
|
|
|
|
mut url := 'https://${host}/${encoded_key}'
|
|
url += '?OSSAccessKeyId=${urllib.query_escape(o.config.access_key_id)}'
|
|
url += '&Expires=${expires}'
|
|
url += '&Signature=${urllib.query_escape(signature)}'
|
|
|
|
return url
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test infrastructure
|
|
// ============================================================================
|
|
|
|
struct PropertyTestStats {
|
|
mut:
|
|
total_tests int
|
|
passed_tests int
|
|
failed_tests int
|
|
}
|
|
|
|
fn (mut stats PropertyTestStats) run_property_test(test_name string, test_func fn () bool) {
|
|
stats.total_tests++
|
|
print('🔬 ${test_name}... ')
|
|
|
|
if test_func() {
|
|
stats.passed_tests++
|
|
println('✅')
|
|
} else {
|
|
stats.failed_tests++
|
|
println('❌')
|
|
}
|
|
}
|
|
|
|
fn (stats PropertyTestStats) print_summary() {
|
|
println('\n=== Aliyun OSS Storage 属性测试总结 ===')
|
|
println('总测试数: ${stats.total_tests}')
|
|
println('通过: ${stats.passed_tests}')
|
|
println('失败: ${stats.failed_tests}')
|
|
|
|
if stats.failed_tests == 0 {
|
|
println('🎉 所有属性测试通过!')
|
|
} else {
|
|
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
|
|
}
|
|
}
|
|
|
|
fn generate_random_string(min_len int, max_len int) string {
|
|
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
len := rand.int_in_range(min_len, max_len) or { min_len }
|
|
mut result := ''
|
|
for _ in 0 .. len {
|
|
idx := rand.int_in_range(0, chars.len) or { 0 }
|
|
result += chars[idx].ascii_str()
|
|
}
|
|
return result
|
|
}
|
|
|
|
fn generate_random_oss_config() AliyunOSSConfig {
|
|
regions := ['cn-hangzhou', 'cn-shanghai', 'cn-beijing', 'cn-shenzhen', 'cn-hongkong']
|
|
region_idx := rand.int_in_range(0, regions.len) or { 0 }
|
|
region := regions[region_idx]
|
|
|
|
return AliyunOSSConfig{
|
|
endpoint: 'oss-${region}.aliyuncs.com'
|
|
access_key_id: generate_random_string(16, 24)
|
|
access_key_secret: generate_random_string(30, 40)
|
|
internal_endpoint: 'oss-${region}-internal.aliyuncs.com'
|
|
default_bucket: generate_random_string(5, 15).to_lower()
|
|
}
|
|
}
|
|
|
|
fn generate_random_oss_config_no_internal() AliyunOSSConfig {
|
|
regions := ['cn-hangzhou', 'cn-shanghai', 'cn-beijing', 'cn-shenzhen']
|
|
region_idx := rand.int_in_range(0, regions.len) or { 0 }
|
|
region := regions[region_idx]
|
|
|
|
return AliyunOSSConfig{
|
|
endpoint: 'oss-${region}.aliyuncs.com'
|
|
access_key_id: generate_random_string(16, 24)
|
|
access_key_secret: generate_random_string(30, 40)
|
|
internal_endpoint: ''
|
|
default_bucket: generate_random_string(5, 15).to_lower()
|
|
}
|
|
}
|
|
|
|
fn generate_random_filename() string {
|
|
chars := 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
len := rand.int_in_range(5, 20) or { 10 }
|
|
mut result := ''
|
|
for _ in 0 .. len {
|
|
idx := rand.int_in_range(0, chars.len) or { 0 }
|
|
result += chars[idx].ascii_str()
|
|
}
|
|
extensions := ['.txt', '.bin', '.dat', '.json', '.xml', '.png', '.jpg']
|
|
ext_idx := rand.int_in_range(0, extensions.len) or { 0 }
|
|
return result + extensions[ext_idx]
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 1: Aliyun OSS Storage Provider Creation
|
|
// For any valid Aliyun OSS configuration, creating a storage provider should succeed
|
|
// Validates: Requirements 4.1, 4.2
|
|
// ============================================================================
|
|
fn test_property_1_oss_provider_creation() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config()
|
|
|
|
storage := new_aliyun_oss(config) or {
|
|
println(' Iteration ${i}: Failed to create Aliyun OSS storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
if storage.provider_name() != 'aliyun_oss' {
|
|
println(' Iteration ${i}: Provider name mismatch. Expected: aliyun_oss, Got: ${storage.provider_name()}')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 2: Invalid Configuration Rejection
|
|
// For any Aliyun OSS configuration with missing required fields, creation should fail
|
|
// Validates: Requirements 4.2
|
|
// ============================================================================
|
|
fn test_property_2_invalid_config_rejection() bool {
|
|
// Test missing endpoint
|
|
for _ in 0 .. 10 {
|
|
config := AliyunOSSConfig{
|
|
endpoint: ''
|
|
access_key_id: generate_random_string(16, 24)
|
|
access_key_secret: generate_random_string(30, 40)
|
|
default_bucket: generate_random_string(5, 15)
|
|
}
|
|
|
|
_ := new_aliyun_oss(config) or {
|
|
continue
|
|
}
|
|
println(' Missing endpoint should have failed')
|
|
return false
|
|
}
|
|
|
|
// Test missing access_key_id
|
|
for _ in 0 .. 10 {
|
|
config := AliyunOSSConfig{
|
|
endpoint: 'oss-cn-hangzhou.aliyuncs.com'
|
|
access_key_id: ''
|
|
access_key_secret: generate_random_string(30, 40)
|
|
default_bucket: generate_random_string(5, 15)
|
|
}
|
|
|
|
_ := new_aliyun_oss(config) or {
|
|
continue
|
|
}
|
|
println(' Missing access_key_id should have failed')
|
|
return false
|
|
}
|
|
|
|
// Test missing access_key_secret
|
|
for _ in 0 .. 10 {
|
|
config := AliyunOSSConfig{
|
|
endpoint: 'oss-cn-hangzhou.aliyuncs.com'
|
|
access_key_id: generate_random_string(16, 24)
|
|
access_key_secret: ''
|
|
default_bucket: generate_random_string(5, 15)
|
|
}
|
|
|
|
_ := new_aliyun_oss(config) or {
|
|
continue
|
|
}
|
|
println(' Missing access_key_secret should have failed')
|
|
return false
|
|
}
|
|
|
|
// Test missing default_bucket
|
|
for _ in 0 .. 10 {
|
|
config := AliyunOSSConfig{
|
|
endpoint: 'oss-cn-hangzhou.aliyuncs.com'
|
|
access_key_id: generate_random_string(16, 24)
|
|
access_key_secret: generate_random_string(30, 40)
|
|
default_bucket: ''
|
|
}
|
|
|
|
_ := new_aliyun_oss(config) or {
|
|
continue
|
|
}
|
|
println(' Missing default_bucket should have failed')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 3: Internal/External Endpoint Switching
|
|
// For any Aliyun OSS configuration with internal endpoint, switching should work correctly
|
|
// Validates: Requirements 4.3
|
|
// ============================================================================
|
|
fn test_property_3_endpoint_switching() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config()
|
|
|
|
mut storage := new_aliyun_oss(config) or {
|
|
println(' Iteration ${i}: Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify initial endpoint is external
|
|
initial_endpoint := storage.get_current_endpoint()
|
|
if initial_endpoint != config.endpoint {
|
|
println(' Iteration ${i}: Initial endpoint mismatch. Expected: ${config.endpoint}, Got: ${initial_endpoint}')
|
|
return false
|
|
}
|
|
|
|
// Switch to internal endpoint
|
|
storage.switch_to_internal() or {
|
|
println(' Iteration ${i}: Failed to switch to internal endpoint: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify endpoint is now internal
|
|
internal_endpoint := storage.get_current_endpoint()
|
|
if internal_endpoint != config.internal_endpoint {
|
|
println(' Iteration ${i}: Internal endpoint mismatch. Expected: ${config.internal_endpoint}, Got: ${internal_endpoint}')
|
|
return false
|
|
}
|
|
|
|
// Switch back to external endpoint
|
|
storage.switch_to_external()
|
|
|
|
// Verify endpoint is back to external
|
|
external_endpoint := storage.get_current_endpoint()
|
|
if external_endpoint != config.endpoint {
|
|
println(' Iteration ${i}: External endpoint mismatch after switch back. Expected: ${config.endpoint}, Got: ${external_endpoint}')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 4: Internal Endpoint Switching Without Config Should Fail
|
|
// For any Aliyun OSS configuration without internal endpoint, switching should fail
|
|
// Validates: Requirements 4.3
|
|
// ============================================================================
|
|
fn test_property_4_internal_switch_without_config() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config_no_internal()
|
|
|
|
mut storage := new_aliyun_oss(config) or {
|
|
println(' Iteration ${i}: Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Attempt to switch to internal endpoint should fail
|
|
storage.switch_to_internal() or {
|
|
continue
|
|
}
|
|
println(' Iteration ${i}: Switch to internal should have failed without internal_endpoint configured')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 5: Presigned URL Format Validity
|
|
// For any valid Aliyun OSS configuration and object key, the presigned URL should:
|
|
// - Contain the correct scheme (https)
|
|
// - Contain the correct host (bucket.endpoint)
|
|
// - Contain the object key
|
|
// - Contain required OSS signature parameters
|
|
// Validates: Requirements 4.4, 4.5
|
|
// ============================================================================
|
|
fn test_property_5_presigned_url_format() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config()
|
|
bucket := config.default_bucket
|
|
key := generate_random_filename()
|
|
|
|
storage := new_aliyun_oss(config) or {
|
|
println(' Iteration ${i}: Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
options := PresignOptions{
|
|
expires_in: 3600
|
|
method: 'GET'
|
|
}
|
|
|
|
url := storage.presign_url(bucket, key, options) or {
|
|
println(' Iteration ${i}: Failed to generate presigned URL: ${err}')
|
|
return false
|
|
}
|
|
|
|
// 1. Should start with https://
|
|
if !url.starts_with('https://') {
|
|
println(' Iteration ${i}: URL should start with https://, got: ${url}')
|
|
return false
|
|
}
|
|
|
|
// 2. Should contain bucket in host (virtual-hosted-style)
|
|
expected_host := '${bucket}.${config.endpoint}'
|
|
if !url.contains(expected_host) {
|
|
println(' Iteration ${i}: URL should contain host "${expected_host}", got: ${url}')
|
|
return false
|
|
}
|
|
|
|
// 3. Should contain the object key
|
|
if !url.contains(key) {
|
|
println(' Iteration ${i}: URL should contain key "${key}", got: ${url}')
|
|
return false
|
|
}
|
|
|
|
// 4. Should contain OSS signature parameters
|
|
required_params := ['OSSAccessKeyId', 'Expires', 'Signature']
|
|
for param in required_params {
|
|
if !url.contains(param) {
|
|
println(' Iteration ${i}: URL missing required parameter "${param}", got: ${url}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 6: Presigned URL Expiration Parameter
|
|
// For any expiration time, the presigned URL should contain the correct Expires value
|
|
// Validates: Requirements 4.4
|
|
// ============================================================================
|
|
fn test_property_6_presigned_url_expiration() bool {
|
|
config := generate_random_oss_config()
|
|
bucket := config.default_bucket
|
|
key := generate_random_filename()
|
|
|
|
storage := new_aliyun_oss(config) or {
|
|
println(' Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
expiration_times := [60, 300, 900, 1800, 3600, 7200, 86400]
|
|
|
|
for expires_in in expiration_times {
|
|
options := PresignOptions{
|
|
expires_in: expires_in
|
|
method: 'GET'
|
|
}
|
|
|
|
url := storage.presign_url(bucket, key, options) or {
|
|
println(' Failed to generate presigned URL with expires_in=${expires_in}: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify URL contains Expires parameter
|
|
if !url.contains('Expires=') {
|
|
println(' URL should contain Expires parameter, got: ${url}')
|
|
return false
|
|
}
|
|
|
|
// The Expires value should be a Unix timestamp (current time + expires_in)
|
|
if expires_idx := url.index('Expires=') {
|
|
expires_start := expires_idx + 8
|
|
mut expires_end := expires_start
|
|
for expires_end < url.len && url[expires_end] != `&` {
|
|
expires_end++
|
|
}
|
|
expires_str := url[expires_start..expires_end]
|
|
expires_value := expires_str.i64()
|
|
|
|
// Should be a reasonable Unix timestamp (after year 2020)
|
|
if expires_value < 1577836800 {
|
|
println(' Expires value ${expires_value} seems too small')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 7: Presigned URL Method Parameter
|
|
// For different HTTP methods, the presigned URL should be generated correctly
|
|
// Validates: Requirements 4.4
|
|
// ============================================================================
|
|
fn test_property_7_presigned_url_methods() bool {
|
|
config := generate_random_oss_config()
|
|
bucket := config.default_bucket
|
|
key := generate_random_filename()
|
|
|
|
storage := new_aliyun_oss(config) or {
|
|
println(' Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
methods := ['GET', 'PUT', 'DELETE', 'HEAD']
|
|
|
|
for method in methods {
|
|
options := PresignOptions{
|
|
expires_in: 3600
|
|
method: method
|
|
}
|
|
|
|
url := storage.presign_url(bucket, key, options) or {
|
|
println(' Failed to generate presigned URL for method ${method}: ${err}')
|
|
return false
|
|
}
|
|
|
|
if url == '' {
|
|
println(' Empty URL generated for method ${method}')
|
|
return false
|
|
}
|
|
|
|
if !url.contains('Signature=') {
|
|
println(' URL for method ${method} missing signature')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 8: Nested Path Key Handling
|
|
// For keys with nested paths, the presigned URL should handle them correctly
|
|
// Validates: Requirements 4.4
|
|
// ============================================================================
|
|
fn test_property_8_nested_path_handling() bool {
|
|
config := generate_random_oss_config()
|
|
bucket := config.default_bucket
|
|
|
|
storage := new_aliyun_oss(config) or {
|
|
println(' Failed to create storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate nested path
|
|
depth := rand.int_in_range(1, 5) or { 2 }
|
|
mut path_parts := []string{}
|
|
for _ in 0 .. depth {
|
|
part_len := rand.int_in_range(3, 10) or { 5 }
|
|
mut part := ''
|
|
for _ in 0 .. part_len {
|
|
idx := rand.int_in_range(0, 26) or { 0 }
|
|
part += ('a'[0] + u8(idx)).ascii_str()
|
|
}
|
|
path_parts << part
|
|
}
|
|
path_parts << generate_random_filename()
|
|
key := path_parts.join('/')
|
|
|
|
options := PresignOptions{
|
|
expires_in: 3600
|
|
method: 'GET'
|
|
}
|
|
|
|
url := storage.presign_url(bucket, key, options) or {
|
|
println(' Iteration ${i}: Failed to generate presigned URL for nested key "${key}": ${err}')
|
|
return false
|
|
}
|
|
|
|
// URL should contain the key parts
|
|
for part in path_parts {
|
|
if !url.contains(part) {
|
|
println(' Iteration ${i}: URL should contain path part "${part}", got: ${url}')
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 9: Internal Endpoint Provider Creation
|
|
// For any valid Aliyun OSS configuration with internal endpoint, creating an internal provider should succeed
|
|
// Validates: Requirements 4.3
|
|
// ============================================================================
|
|
fn test_property_9_internal_provider_creation() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config()
|
|
|
|
storage := new_aliyun_oss_internal(config) or {
|
|
println(' Iteration ${i}: Failed to create internal Aliyun OSS storage: ${err}')
|
|
return false
|
|
}
|
|
|
|
if storage.provider_name() != 'aliyun_oss' {
|
|
println(' Iteration ${i}: Provider name mismatch. Expected: aliyun_oss, Got: ${storage.provider_name()}')
|
|
return false
|
|
}
|
|
|
|
current_endpoint := storage.get_current_endpoint()
|
|
if current_endpoint != config.internal_endpoint {
|
|
println(' Iteration ${i}: Endpoint should be internal. Expected: ${config.internal_endpoint}, Got: ${current_endpoint}')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 10: Internal Provider Creation Without Config Should Fail
|
|
// For any Aliyun OSS configuration without internal endpoint, creating an internal provider should fail
|
|
// Validates: Requirements 4.3
|
|
// ============================================================================
|
|
fn test_property_10_internal_provider_without_config() bool {
|
|
for i in 0 .. test_iterations {
|
|
config := generate_random_oss_config_no_internal()
|
|
|
|
_ := new_aliyun_oss_internal(config) or {
|
|
continue
|
|
}
|
|
println(' Iteration ${i}: Creating internal provider should have failed without internal_endpoint configured')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
fn main() {
|
|
println('🚀 开始 Aliyun OSS Storage 属性测试...')
|
|
println('Feature: v-hono-upload-integration, Property 1 & 4: Upload-Download Round Trip (OSS) & Presigned URL Validity')
|
|
println('Validates: Requirements 4.4, 4.5')
|
|
println('每个属性测试运行 ${test_iterations} 次迭代\n')
|
|
|
|
rand.seed([u32(time.now().unix()), u32(67890)])
|
|
|
|
mut stats := PropertyTestStats{}
|
|
|
|
// Run property tests
|
|
stats.run_property_test('Property 1: Aliyun OSS Storage Provider Creation', test_property_1_oss_provider_creation)
|
|
stats.run_property_test('Property 2: Invalid Configuration Rejection', test_property_2_invalid_config_rejection)
|
|
stats.run_property_test('Property 3: Internal/External Endpoint Switching', test_property_3_endpoint_switching)
|
|
stats.run_property_test('Property 4: Internal Switch Without Config Should Fail', test_property_4_internal_switch_without_config)
|
|
stats.run_property_test('Property 5: Presigned URL Format Validity', test_property_5_presigned_url_format)
|
|
stats.run_property_test('Property 6: Presigned URL Expiration Parameter', test_property_6_presigned_url_expiration)
|
|
stats.run_property_test('Property 7: Presigned URL Method Parameter', test_property_7_presigned_url_methods)
|
|
stats.run_property_test('Property 8: Nested Path Key Handling', test_property_8_nested_path_handling)
|
|
stats.run_property_test('Property 9: Internal Endpoint Provider Creation', test_property_9_internal_provider_creation)
|
|
stats.run_property_test('Property 10: Internal Provider Without Config Should Fail', test_property_10_internal_provider_without_config)
|
|
|
|
stats.print_summary()
|
|
|
|
if stats.failed_tests > 0 {
|
|
exit(1)
|
|
}
|
|
}
|