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)
836 lines
23 KiB
V
836 lines
23 KiB
V
module main
|
|
|
|
import rand
|
|
import time
|
|
import os
|
|
import db.sqlite
|
|
import crypto.rand as crand
|
|
|
|
// ============================================================================
|
|
// Property 2: Metadata Round Trip
|
|
// Feature: v-hono-upload-integration, Property 2: Metadata Round Trip
|
|
// Validates: Requirements 1.5, 8.1, 8.2
|
|
//
|
|
// *For any* file with metadata, storing the file and then retrieving its metadata
|
|
// should return all original metadata fields (uuid, hash, name, size, type, timestamps)
|
|
// unchanged.
|
|
// ============================================================================
|
|
|
|
const test_iterations = 100
|
|
const test_db_path = './test_metadata_roundtrip.db'
|
|
|
|
// ============================================================================
|
|
// Type definitions (copied from database.v for standalone testing)
|
|
// ============================================================================
|
|
|
|
struct FileInfo {
|
|
pub:
|
|
id int
|
|
file_uuid string
|
|
file_hash string
|
|
file_name string
|
|
file_size i64
|
|
file_type string
|
|
storage_type string
|
|
bucket string
|
|
object_key string
|
|
created_at i64
|
|
updated_at i64
|
|
metadata string
|
|
}
|
|
|
|
struct FileListOptions {
|
|
pub:
|
|
bucket string
|
|
prefix string
|
|
storage_type string
|
|
limit int = 100
|
|
offset int
|
|
order_by string = 'created_at'
|
|
order_desc bool = true
|
|
}
|
|
|
|
struct FileListResult {
|
|
pub:
|
|
files []FileInfo
|
|
total_count int
|
|
has_more bool
|
|
}
|
|
|
|
// 数据库管理器
|
|
struct DatabaseManager {
|
|
mut:
|
|
db sqlite.DB
|
|
}
|
|
|
|
// 创建数据库管理器
|
|
fn new_database_manager(db_path string) !DatabaseManager {
|
|
mut db := sqlite.connect(db_path) or {
|
|
return error('Failed to connect to database: ${err}')
|
|
}
|
|
|
|
// 创建文件信息表
|
|
db.exec('CREATE TABLE IF NOT EXISTS file_info (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
file_uuid TEXT UNIQUE NOT NULL,
|
|
file_hash TEXT NOT NULL,
|
|
file_name TEXT NOT NULL,
|
|
file_size INTEGER NOT NULL,
|
|
file_type TEXT NOT NULL,
|
|
storage_type TEXT NOT NULL DEFAULT "local",
|
|
bucket TEXT NOT NULL DEFAULT "default",
|
|
object_key TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
metadata TEXT
|
|
);') or { return error('Failed to create file_info table: ${err}') }
|
|
|
|
// 创建索引
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_file_hash ON file_info(file_hash);') or {
|
|
return error('Failed to create idx_file_hash index: ${err}')
|
|
}
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_file_uuid ON file_info(file_uuid);') or {
|
|
return error('Failed to create idx_file_uuid index: ${err}')
|
|
}
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_bucket_key ON file_info(bucket, object_key);') or {
|
|
return error('Failed to create idx_bucket_key index: ${err}')
|
|
}
|
|
|
|
return DatabaseManager{
|
|
db: db
|
|
}
|
|
}
|
|
|
|
// 生成文件UUID
|
|
fn generate_file_uuid() string {
|
|
random_bytes := crand.bytes(16) or { return '' }
|
|
mut uuid := ''
|
|
for i, b in random_bytes {
|
|
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
uuid += '-'
|
|
}
|
|
uuid += '${b:02x}'
|
|
}
|
|
return uuid
|
|
}
|
|
|
|
// SQL 字符串转义
|
|
fn escape_sql(s string) string {
|
|
return s.replace("'", "''")
|
|
}
|
|
|
|
// 插入文件信息
|
|
fn (mut dm DatabaseManager) insert_file(file FileInfo) !FileInfo {
|
|
now := time.now().unix()
|
|
file_uuid := if file.file_uuid != '' { file.file_uuid } else { generate_file_uuid() }
|
|
|
|
dm.db.exec("INSERT INTO file_info (file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata) VALUES ('${file_uuid}', '${file.file_hash}', '${escape_sql(file.file_name)}', ${file.file_size}, '${file.file_type}', '${file.storage_type}', '${file.bucket}', '${escape_sql(file.object_key)}', ${now}, ${now}, '${escape_sql(file.metadata)}')") or {
|
|
return error('Failed to insert file info: ${err}')
|
|
}
|
|
|
|
return FileInfo{
|
|
id: 0
|
|
file_uuid: file_uuid
|
|
file_hash: file.file_hash
|
|
file_name: file.file_name
|
|
file_size: file.file_size
|
|
file_type: file.file_type
|
|
storage_type: file.storage_type
|
|
bucket: file.bucket
|
|
object_key: file.object_key
|
|
created_at: now
|
|
updated_at: now
|
|
metadata: file.metadata
|
|
}
|
|
}
|
|
|
|
// 根据UUID获取文件信息
|
|
fn (dm DatabaseManager) get_file_by_uuid(file_uuid string) !FileInfo {
|
|
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE file_uuid = '${file_uuid}'") or {
|
|
return error('Failed to query file info: ${err}')
|
|
}
|
|
|
|
if rows.len > 0 {
|
|
row := rows[0]
|
|
return FileInfo{
|
|
id: row.vals[0].int()
|
|
file_uuid: row.vals[1]
|
|
file_hash: row.vals[2]
|
|
file_name: row.vals[3]
|
|
file_size: row.vals[4].i64()
|
|
file_type: row.vals[5]
|
|
storage_type: row.vals[6]
|
|
bucket: row.vals[7]
|
|
object_key: row.vals[8]
|
|
created_at: row.vals[9].i64()
|
|
updated_at: row.vals[10].i64()
|
|
metadata: row.vals[11]
|
|
}
|
|
}
|
|
|
|
return error('File not found')
|
|
}
|
|
|
|
// 根据hash获取文件信息
|
|
fn (dm DatabaseManager) get_file_by_hash(file_hash string) !FileInfo {
|
|
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE file_hash = '${file_hash}'") or {
|
|
return error('Failed to query file info: ${err}')
|
|
}
|
|
|
|
if rows.len > 0 {
|
|
row := rows[0]
|
|
return FileInfo{
|
|
id: row.vals[0].int()
|
|
file_uuid: row.vals[1]
|
|
file_hash: row.vals[2]
|
|
file_name: row.vals[3]
|
|
file_size: row.vals[4].i64()
|
|
file_type: row.vals[5]
|
|
storage_type: row.vals[6]
|
|
bucket: row.vals[7]
|
|
object_key: row.vals[8]
|
|
created_at: row.vals[9].i64()
|
|
updated_at: row.vals[10].i64()
|
|
metadata: row.vals[11]
|
|
}
|
|
}
|
|
|
|
return error('File not found')
|
|
}
|
|
|
|
// 更新文件信息
|
|
fn (mut dm DatabaseManager) update_file(file_uuid string, file_name string, file_type string, metadata string) !FileInfo {
|
|
now := time.now().unix()
|
|
|
|
dm.db.exec("UPDATE file_info SET file_name = '${escape_sql(file_name)}', file_type = '${file_type}', metadata = '${escape_sql(metadata)}', updated_at = ${now} WHERE file_uuid = '${file_uuid}'") or {
|
|
return error('Failed to update file info: ${err}')
|
|
}
|
|
|
|
return dm.get_file_by_uuid(file_uuid)
|
|
}
|
|
|
|
// 删除文件信息
|
|
fn (mut dm DatabaseManager) delete_file(file_uuid string) ! {
|
|
dm.db.exec("DELETE FROM file_info WHERE file_uuid = '${file_uuid}'") or {
|
|
return error('Failed to delete file info: ${err}')
|
|
}
|
|
}
|
|
|
|
// 检查文件是否存在
|
|
fn (dm DatabaseManager) file_exists(file_uuid string) bool {
|
|
rows := dm.db.exec("SELECT 1 FROM file_info WHERE file_uuid = '${file_uuid}' LIMIT 1") or {
|
|
return false
|
|
}
|
|
return rows.len > 0
|
|
}
|
|
|
|
// 根据 bucket 和 object_key 获取文件
|
|
fn (dm DatabaseManager) get_file_by_key(bucket string, object_key string) !FileInfo {
|
|
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE bucket = '${bucket}' AND object_key = '${escape_sql(object_key)}'") or {
|
|
return error('Failed to query file info: ${err}')
|
|
}
|
|
|
|
if rows.len > 0 {
|
|
row := rows[0]
|
|
return FileInfo{
|
|
id: row.vals[0].int()
|
|
file_uuid: row.vals[1]
|
|
file_hash: row.vals[2]
|
|
file_name: row.vals[3]
|
|
file_size: row.vals[4].i64()
|
|
file_type: row.vals[5]
|
|
storage_type: row.vals[6]
|
|
bucket: row.vals[7]
|
|
object_key: row.vals[8]
|
|
created_at: row.vals[9].i64()
|
|
updated_at: row.vals[10].i64()
|
|
metadata: row.vals[11]
|
|
}
|
|
}
|
|
|
|
return error('File not found')
|
|
}
|
|
|
|
// 关闭数据库连接
|
|
fn (mut dm DatabaseManager) close() {
|
|
dm.db.close() or {}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// 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=== Metadata Round Trip 属性测试总结 ===')
|
|
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_filename() string {
|
|
name := generate_random_string(5, 20)
|
|
extensions := ['.txt', '.bin', '.dat', '.json', '.xml', '.png', '.jpg']
|
|
ext_idx := rand.int_in_range(0, extensions.len) or { 0 }
|
|
return name + extensions[ext_idx]
|
|
}
|
|
|
|
// 生成随机 hash (模拟 MD5)
|
|
fn generate_random_hash() string {
|
|
mut hash := ''
|
|
for _ in 0 .. 32 {
|
|
idx := rand.int_in_range(0, 16) or { 0 }
|
|
hash += '0123456789abcdef'[idx].ascii_str()
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// 生成随机 content_type
|
|
fn generate_random_content_type() string {
|
|
types := [
|
|
'text/plain',
|
|
'application/octet-stream',
|
|
'application/json',
|
|
'image/png',
|
|
'image/jpeg',
|
|
'application/xml',
|
|
'text/html',
|
|
]
|
|
idx := rand.int_in_range(0, types.len) or { 0 }
|
|
return types[idx]
|
|
}
|
|
|
|
// 生成随机存储类型
|
|
fn generate_random_storage_type() string {
|
|
types := ['local', 's3', 'aliyun_oss', 'tencent_cos']
|
|
idx := rand.int_in_range(0, types.len) or { 0 }
|
|
return types[idx]
|
|
}
|
|
|
|
// 生成随机 bucket 名称
|
|
fn generate_random_bucket() string {
|
|
return 'bucket-' + generate_random_string(5, 10)
|
|
}
|
|
|
|
// 生成随机 object key
|
|
fn generate_random_object_key() string {
|
|
depth := rand.int_in_range(0, 4) or { 1 }
|
|
mut parts := []string{}
|
|
for _ in 0 .. depth {
|
|
parts << generate_random_string(3, 10)
|
|
}
|
|
parts << generate_random_filename()
|
|
return parts.join('/')
|
|
}
|
|
|
|
// 生成随机文件大小
|
|
fn generate_random_file_size() i64 {
|
|
return rand.i64_in_range(0, 10_000_000) or { 1000 }
|
|
}
|
|
|
|
// 生成随机 JSON 元数据
|
|
fn generate_random_metadata() string {
|
|
key := generate_random_string(3, 10)
|
|
value := generate_random_string(5, 20)
|
|
return '{"${key}": "${value}"}'
|
|
}
|
|
|
|
// 清理测试数据库
|
|
fn cleanup_test_db() {
|
|
os.rm(test_db_path) or {}
|
|
}
|
|
|
|
// 创建测试数据库管理器
|
|
fn create_test_db() !DatabaseManager {
|
|
return new_database_manager(test_db_path)
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 2.1: Basic Metadata Round Trip
|
|
// For any file metadata, inserting and retrieving by UUID should return identical fields
|
|
// ============================================================================
|
|
fn test_property_2_1_basic_metadata_roundtrip() bool {
|
|
cleanup_test_db()
|
|
defer {
|
|
cleanup_test_db()
|
|
}
|
|
|
|
mut db := create_test_db() or {
|
|
println(' Failed to create database: ${err}')
|
|
return false
|
|
}
|
|
defer {
|
|
db.close()
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate random file info
|
|
file_hash := generate_random_hash()
|
|
file_name := generate_random_filename()
|
|
file_size := generate_random_file_size()
|
|
file_type := generate_random_content_type()
|
|
storage_type := generate_random_storage_type()
|
|
bucket := generate_random_bucket()
|
|
object_key := generate_random_object_key()
|
|
metadata := generate_random_metadata()
|
|
|
|
original := FileInfo{
|
|
file_hash: file_hash
|
|
file_name: file_name
|
|
file_size: file_size
|
|
file_type: file_type
|
|
storage_type: storage_type
|
|
bucket: bucket
|
|
object_key: object_key
|
|
metadata: metadata
|
|
}
|
|
|
|
// Insert
|
|
inserted := db.insert_file(original) or {
|
|
println(' Iteration ${i}: Insert failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Retrieve by UUID
|
|
retrieved := db.get_file_by_uuid(inserted.file_uuid) or {
|
|
println(' Iteration ${i}: Get by UUID failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify all fields match
|
|
if retrieved.file_uuid != inserted.file_uuid {
|
|
println(' Iteration ${i}: UUID mismatch: ${retrieved.file_uuid} vs ${inserted.file_uuid}')
|
|
return false
|
|
}
|
|
if retrieved.file_hash != file_hash {
|
|
println(' Iteration ${i}: Hash mismatch: ${retrieved.file_hash} vs ${file_hash}')
|
|
return false
|
|
}
|
|
if retrieved.file_name != file_name {
|
|
println(' Iteration ${i}: Name mismatch: ${retrieved.file_name} vs ${file_name}')
|
|
return false
|
|
}
|
|
if retrieved.file_size != file_size {
|
|
println(' Iteration ${i}: Size mismatch: ${retrieved.file_size} vs ${file_size}')
|
|
return false
|
|
}
|
|
if retrieved.file_type != file_type {
|
|
println(' Iteration ${i}: Type mismatch: ${retrieved.file_type} vs ${file_type}')
|
|
return false
|
|
}
|
|
if retrieved.storage_type != storage_type {
|
|
println(' Iteration ${i}: Storage type mismatch: ${retrieved.storage_type} vs ${storage_type}')
|
|
return false
|
|
}
|
|
if retrieved.bucket != bucket {
|
|
println(' Iteration ${i}: Bucket mismatch: ${retrieved.bucket} vs ${bucket}')
|
|
return false
|
|
}
|
|
if retrieved.object_key != object_key {
|
|
println(' Iteration ${i}: Object key mismatch: ${retrieved.object_key} vs ${object_key}')
|
|
return false
|
|
}
|
|
if retrieved.metadata != metadata {
|
|
println(' Iteration ${i}: Metadata mismatch: ${retrieved.metadata} vs ${metadata}')
|
|
return false
|
|
}
|
|
|
|
// Verify timestamps are set
|
|
if retrieved.created_at <= 0 {
|
|
println(' Iteration ${i}: created_at not set')
|
|
return false
|
|
}
|
|
if retrieved.updated_at <= 0 {
|
|
println(' Iteration ${i}: updated_at not set')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 2.2: Hash Query Round Trip
|
|
// For any file metadata, inserting and retrieving by hash should return the file
|
|
// ============================================================================
|
|
fn test_property_2_2_hash_query_roundtrip() bool {
|
|
cleanup_test_db()
|
|
defer {
|
|
cleanup_test_db()
|
|
}
|
|
|
|
mut db := create_test_db() or {
|
|
println(' Failed to create database: ${err}')
|
|
return false
|
|
}
|
|
defer {
|
|
db.close()
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate random file info
|
|
file_hash := generate_random_hash()
|
|
file_name := generate_random_filename()
|
|
file_size := generate_random_file_size()
|
|
file_type := generate_random_content_type()
|
|
storage_type := generate_random_storage_type()
|
|
bucket := generate_random_bucket()
|
|
object_key := generate_random_object_key()
|
|
|
|
original := FileInfo{
|
|
file_hash: file_hash
|
|
file_name: file_name
|
|
file_size: file_size
|
|
file_type: file_type
|
|
storage_type: storage_type
|
|
bucket: bucket
|
|
object_key: object_key
|
|
metadata: ''
|
|
}
|
|
|
|
// Insert
|
|
inserted := db.insert_file(original) or {
|
|
println(' Iteration ${i}: Insert failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Retrieve by hash
|
|
retrieved := db.get_file_by_hash(file_hash) or {
|
|
println(' Iteration ${i}: Get by hash failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify UUID matches
|
|
if retrieved.file_uuid != inserted.file_uuid {
|
|
println(' Iteration ${i}: UUID mismatch when querying by hash')
|
|
return false
|
|
}
|
|
|
|
// Verify hash matches
|
|
if retrieved.file_hash != file_hash {
|
|
println(' Iteration ${i}: Hash mismatch: ${retrieved.file_hash} vs ${file_hash}')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Property 2.3: Update Preserves Immutable Fields
|
|
// For any file, updating metadata should preserve immutable fields (uuid, hash, size, timestamps)
|
|
// ============================================================================
|
|
fn test_property_2_3_update_preserves_immutable_fields() bool {
|
|
cleanup_test_db()
|
|
defer {
|
|
cleanup_test_db()
|
|
}
|
|
|
|
mut db := create_test_db() or {
|
|
println(' Failed to create database: ${err}')
|
|
return false
|
|
}
|
|
defer {
|
|
db.close()
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate and insert original file info
|
|
file_hash := generate_random_hash()
|
|
file_name := generate_random_filename()
|
|
file_size := generate_random_file_size()
|
|
file_type := generate_random_content_type()
|
|
storage_type := generate_random_storage_type()
|
|
bucket := generate_random_bucket()
|
|
object_key := generate_random_object_key()
|
|
metadata := generate_random_metadata()
|
|
|
|
original := FileInfo{
|
|
file_hash: file_hash
|
|
file_name: file_name
|
|
file_size: file_size
|
|
file_type: file_type
|
|
storage_type: storage_type
|
|
bucket: bucket
|
|
object_key: object_key
|
|
metadata: metadata
|
|
}
|
|
|
|
inserted := db.insert_file(original) or {
|
|
println(' Iteration ${i}: Insert failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Generate new values for mutable fields
|
|
new_file_name := generate_random_filename()
|
|
new_file_type := generate_random_content_type()
|
|
new_metadata := generate_random_metadata()
|
|
|
|
// Update
|
|
updated := db.update_file(inserted.file_uuid, new_file_name, new_file_type, new_metadata) or {
|
|
println(' Iteration ${i}: Update failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify immutable fields are preserved
|
|
if updated.file_uuid != inserted.file_uuid {
|
|
println(' Iteration ${i}: UUID changed after update')
|
|
return false
|
|
}
|
|
if updated.file_hash != file_hash {
|
|
println(' Iteration ${i}: Hash changed after update')
|
|
return false
|
|
}
|
|
if updated.file_size != file_size {
|
|
println(' Iteration ${i}: Size changed after update')
|
|
return false
|
|
}
|
|
if updated.storage_type != storage_type {
|
|
println(' Iteration ${i}: Storage type changed after update')
|
|
return false
|
|
}
|
|
if updated.bucket != bucket {
|
|
println(' Iteration ${i}: Bucket changed after update')
|
|
return false
|
|
}
|
|
if updated.object_key != object_key {
|
|
println(' Iteration ${i}: Object key changed after update')
|
|
return false
|
|
}
|
|
if updated.created_at != inserted.created_at {
|
|
println(' Iteration ${i}: created_at changed after update')
|
|
return false
|
|
}
|
|
|
|
// Verify mutable fields are updated
|
|
if updated.file_name != new_file_name {
|
|
println(' Iteration ${i}: Name not updated: ${updated.file_name} vs ${new_file_name}')
|
|
return false
|
|
}
|
|
if updated.file_type != new_file_type {
|
|
println(' Iteration ${i}: Type not updated: ${updated.file_type} vs ${new_file_type}')
|
|
return false
|
|
}
|
|
if updated.metadata != new_metadata {
|
|
println(' Iteration ${i}: Metadata not updated: ${updated.metadata} vs ${new_metadata}')
|
|
return false
|
|
}
|
|
|
|
// Verify updated_at is changed
|
|
if updated.updated_at < inserted.updated_at {
|
|
println(' Iteration ${i}: updated_at not updated')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 2.4: Delete Removes File Metadata
|
|
// For any file, deleting should make it unretrievable
|
|
// ============================================================================
|
|
fn test_property_2_4_delete_removes_file() bool {
|
|
cleanup_test_db()
|
|
defer {
|
|
cleanup_test_db()
|
|
}
|
|
|
|
mut db := create_test_db() or {
|
|
println(' Failed to create database: ${err}')
|
|
return false
|
|
}
|
|
defer {
|
|
db.close()
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate and insert file info
|
|
original := FileInfo{
|
|
file_hash: generate_random_hash()
|
|
file_name: generate_random_filename()
|
|
file_size: generate_random_file_size()
|
|
file_type: generate_random_content_type()
|
|
storage_type: generate_random_storage_type()
|
|
bucket: generate_random_bucket()
|
|
object_key: generate_random_object_key()
|
|
metadata: ''
|
|
}
|
|
|
|
inserted := db.insert_file(original) or {
|
|
println(' Iteration ${i}: Insert failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify file exists
|
|
if !db.file_exists(inserted.file_uuid) {
|
|
println(' Iteration ${i}: File should exist after insert')
|
|
return false
|
|
}
|
|
|
|
// Delete
|
|
db.delete_file(inserted.file_uuid) or {
|
|
println(' Iteration ${i}: Delete failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify file no longer exists
|
|
if db.file_exists(inserted.file_uuid) {
|
|
println(' Iteration ${i}: File should not exist after delete')
|
|
return false
|
|
}
|
|
|
|
// Verify get_file_by_uuid returns error
|
|
_ := db.get_file_by_uuid(inserted.file_uuid) or {
|
|
// Expected error
|
|
continue
|
|
}
|
|
println(' Iteration ${i}: get_file_by_uuid should return error after delete')
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ============================================================================
|
|
// Property 2.5: Bucket and Key Query Round Trip
|
|
// For any file, querying by bucket and object_key should return the correct file
|
|
// ============================================================================
|
|
fn test_property_2_5_bucket_key_query_roundtrip() bool {
|
|
cleanup_test_db()
|
|
defer {
|
|
cleanup_test_db()
|
|
}
|
|
|
|
mut db := create_test_db() or {
|
|
println(' Failed to create database: ${err}')
|
|
return false
|
|
}
|
|
defer {
|
|
db.close()
|
|
}
|
|
|
|
for i in 0 .. test_iterations {
|
|
// Generate random file info
|
|
file_hash := generate_random_hash()
|
|
file_name := generate_random_filename()
|
|
file_size := generate_random_file_size()
|
|
file_type := generate_random_content_type()
|
|
storage_type := generate_random_storage_type()
|
|
bucket := generate_random_bucket()
|
|
object_key := generate_random_object_key()
|
|
|
|
original := FileInfo{
|
|
file_hash: file_hash
|
|
file_name: file_name
|
|
file_size: file_size
|
|
file_type: file_type
|
|
storage_type: storage_type
|
|
bucket: bucket
|
|
object_key: object_key
|
|
metadata: ''
|
|
}
|
|
|
|
// Insert
|
|
inserted := db.insert_file(original) or {
|
|
println(' Iteration ${i}: Insert failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Retrieve by bucket and key
|
|
retrieved := db.get_file_by_key(bucket, object_key) or {
|
|
println(' Iteration ${i}: Get by key failed: ${err}')
|
|
return false
|
|
}
|
|
|
|
// Verify UUID matches
|
|
if retrieved.file_uuid != inserted.file_uuid {
|
|
println(' Iteration ${i}: UUID mismatch when querying by bucket/key')
|
|
return false
|
|
}
|
|
|
|
// Verify bucket and key match
|
|
if retrieved.bucket != bucket {
|
|
println(' Iteration ${i}: Bucket mismatch: ${retrieved.bucket} vs ${bucket}')
|
|
return false
|
|
}
|
|
if retrieved.object_key != object_key {
|
|
println(' Iteration ${i}: Object key mismatch: ${retrieved.object_key} vs ${object_key}')
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
|
|
fn main() {
|
|
println('🚀 开始 Metadata Round Trip 属性测试...')
|
|
println('Feature: v-hono-upload-integration, Property 2: Metadata Round Trip')
|
|
println('Validates: Requirements 1.5, 8.1, 8.2')
|
|
println('每个属性测试运行 ${test_iterations} 次迭代\n')
|
|
|
|
rand.seed([u32(time.now().unix()), u32(12345)])
|
|
|
|
mut stats := PropertyTestStats{}
|
|
|
|
// Run property tests
|
|
stats.run_property_test('Property 2.1: Basic Metadata Round Trip', test_property_2_1_basic_metadata_roundtrip)
|
|
stats.run_property_test('Property 2.2: Hash Query Round Trip', test_property_2_2_hash_query_roundtrip)
|
|
stats.run_property_test('Property 2.3: Update Preserves Immutable Fields', test_property_2_3_update_preserves_immutable_fields)
|
|
stats.run_property_test('Property 2.4: Delete Removes File Metadata', test_property_2_4_delete_removes_file)
|
|
stats.run_property_test('Property 2.5: Bucket and Key Query Round Trip', test_property_2_5_bucket_key_query_roundtrip)
|
|
|
|
// Print summary
|
|
stats.print_summary()
|
|
|
|
// Cleanup
|
|
cleanup_test_db()
|
|
|
|
// Exit with error code if any tests failed
|
|
if stats.failed_tests > 0 {
|
|
exit(1)
|
|
}
|
|
}
|