Files
go-trustlog/api/persistence/unit_test.go
ryan fb182adef4 feat: OpType重构为OpCode (int32) - 完整实现
🎯 核心变更:
- OpType (string) → OpCode (int32)
- 20+ OpCode枚举常量 (基于DOIP/IRP标准)
- 类型安全 + 性能优化

📊 影响范围:
- 核心模型: Operation结构体、CBOR序列化
- 数据库: schema.go + SQL DDL (PostgreSQL/MySQL/SQLite)
- 持久化: repository.go查询、cursor_worker.go
- API接口: Protobuf定义 + gRPC客户端
- 测试代码: 60+ 测试文件更新

 测试结果:
- 通过率: 100% (所有87个测试用例)
- 总体覆盖率: 53.7%
- 核心包覆盖率: logger(100%), highclient(95.3%), model(79.1%)

📝 文档:
- 精简README (1056行→489行,减少54%)
- 完整的OpCode枚举说明
- 三种持久化策略示例
- 数据库表结构和架构图

🔧 技术细节:
- 类型转换: string(OpCode) → int32(OpCode)
- SQL参数: 字符串值 → 整数值
- Protobuf: op_type string → op_code int32
- 测试断言: 字符串比较 → 常量比较

🎉 质量保证:
- 零编译错误
- 100%测试通过
- PostgreSQL/Pulsar集成测试验证
- 分布式并发安全测试通过
2025-12-26 13:47:55 +08:00

365 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// +build unit
package persistence
import (
"context"
"database/sql"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
)
// 这个文件包含不依赖外部复杂模块的纯单元测试
// TestDBConfigCreation 测试数据库配置创建
func TestDBConfigCreation(t *testing.T) {
t.Run("default config", func(t *testing.T) {
cfg := DefaultDBConfig("postgres", "test-dsn")
assertEqual(t, cfg.DriverName, "postgres")
assertEqual(t, cfg.DSN, "test-dsn")
assertEqual(t, cfg.MaxOpenConns, 25)
assertEqual(t, cfg.MaxIdleConns, 5)
assertEqual(t, cfg.ConnMaxLifetime, time.Hour)
assertEqual(t, cfg.ConnMaxIdleTime, 10*time.Minute)
})
t.Run("custom config", func(t *testing.T) {
cfg := DBConfig{
DriverName: "mysql",
DSN: "user:pass@/db",
MaxOpenConns: 50,
MaxIdleConns: 10,
ConnMaxLifetime: 2 * time.Hour,
ConnMaxIdleTime: 20 * time.Minute,
}
assertEqual(t, cfg.DriverName, "mysql")
assertEqual(t, cfg.MaxOpenConns, 50)
})
}
// TestTrustlogStatusEnum 测试存证状态枚举
func TestTrustlogStatusEnum(t *testing.T) {
tests := []struct {
status TrustlogStatus
expected string
}{
{StatusNotTrustlogged, "NOT_TRUSTLOGGED"},
{StatusTrustlogged, "TRUSTLOGGED"},
}
for _, tt := range tests {
assertEqual(t, string(tt.status), tt.expected)
}
}
// TestRetryStatusEnum 测试重试状态枚举
func TestRetryStatusEnum(t *testing.T) {
tests := []struct {
status RetryStatus
expected string
}{
{RetryStatusPending, "PENDING"},
{RetryStatusRetrying, "RETRYING"},
{RetryStatusDeadLetter, "DEAD_LETTER"},
}
for _, tt := range tests {
assertEqual(t, string(tt.status), tt.expected)
}
}
// TestPersistenceStrategyEnum 测试持久化策略枚举
func TestPersistenceStrategyEnum(t *testing.T) {
tests := []struct {
strategy PersistenceStrategy
expected string
}{
{StrategyDBOnly, "DB_ONLY"},
{StrategyDBAndTrustlog, "DB_AND_TRUSTLOG"},
{StrategyTrustlogOnly, "TRUSTLOG_ONLY"},
{PersistenceStrategy(999), "UNKNOWN"},
}
for _, tt := range tests {
assertEqual(t, tt.strategy.String(), tt.expected)
}
}
// TestDefaultPersistenceConfig 测试默认持久化配置
func TestDefaultPersistenceConfig(t *testing.T) {
cfg := DefaultPersistenceConfig(StrategyDBAndTrustlog)
assertEqual(t, cfg.Strategy, StrategyDBAndTrustlog)
assertEqual(t, cfg.EnableRetry, true)
assertEqual(t, cfg.MaxRetryCount, 5)
assertEqual(t, cfg.RetryBatchSize, 100)
}
// TestDefaultRetryWorkerConfig 测试默认重试工作器配置
func TestDefaultRetryWorkerConfig(t *testing.T) {
cfg := DefaultRetryWorkerConfig()
assertEqual(t, cfg.RetryInterval, 30*time.Second)
assertEqual(t, cfg.MaxRetryCount, 5)
assertEqual(t, cfg.BatchSize, 100)
assertEqual(t, cfg.BackoffMultiplier, 2.0)
assertEqual(t, cfg.InitialBackoff, 1*time.Minute)
}
// TestGetDialectDDL 测试不同数据库的 DDL 生成
func TestGetDialectDDL(t *testing.T) {
drivers := []string{"postgres", "mysql", "sqlite3", "sqlite", "unknown"}
for _, driver := range drivers {
t.Run(driver, func(t *testing.T) {
opDDL, cursorDDL, retryDDL, err := GetDialectDDL(driver)
if err != nil {
t.Fatalf("GetDialectDDL(%s) returned error: %v", driver, err)
}
if opDDL == "" {
t.Error("operation DDL should not be empty")
}
if cursorDDL == "" {
t.Error("cursor DDL should not be empty")
}
if retryDDL == "" {
t.Error("retry DDL should not be empty")
}
})
}
}
// TestDatabaseConnection 测试数据库连接(使用 SQLite 内存数据库)
func TestDatabaseConnection(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
err = db.Ping()
if err != nil {
t.Fatalf("failed to ping database: %v", err)
}
}
// TestDDLExecution 测试 DDL 执行
func TestDDLExecution(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
opDDL, cursorDDL, retryDDL, err := GetDialectDDL("sqlite3")
if err != nil {
t.Fatalf("failed to get DDL: %v", err)
}
// 执行 operation 表 DDL
_, err = db.Exec(opDDL)
if err != nil {
t.Fatalf("failed to execute operation DDL: %v", err)
}
// 执行 cursor 表 DDL
_, err = db.Exec(cursorDDL)
if err != nil {
t.Fatalf("failed to execute cursor DDL: %v", err)
}
// 执行 retry 表 DDL
_, err = db.Exec(retryDDL)
if err != nil {
t.Fatalf("failed to execute retry DDL: %v", err)
}
// 验证表是否创建成功
tables := []string{"operation", "trustlog_cursor", "trustlog_retry"}
for _, table := range tables {
var name string
err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
if err != nil {
t.Errorf("table %s was not created: %v", table, err)
}
}
}
// TestOperationTableStructure 测试 operation 表结构
func TestOperationTableStructure(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
opDDL, _, _, _ := GetDialectDDL("sqlite3")
_, err = db.Exec(opDDL)
if err != nil {
t.Fatalf("failed to create operation table: %v", err)
}
// 验证关键字段存在
requiredFields := []string{
"op_id", "op_actor", "doid", "producer_id",
"request_body_hash", "response_body_hash",
"client_ip", "server_ip", "trustlog_status",
}
rows, err := db.Query("PRAGMA table_info(operation)")
if err != nil {
t.Fatalf("failed to get table info: %v", err)
}
defer rows.Close()
foundFields := make(map[string]bool)
for rows.Next() {
var cid int
var name, typ string
var notnull, pk int
var dfltValue sql.NullString
err = rows.Scan(&cid, &name, &typ, &notnull, &dfltValue, &pk)
if err != nil {
t.Fatalf("failed to scan row: %v", err)
}
foundFields[name] = true
}
for _, field := range requiredFields {
if !foundFields[field] {
t.Errorf("required field %s not found in operation table", field)
}
}
}
// TestCursorTableInitialization 测试游标表初始化
func TestCursorTableInitialization(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
_, cursorDDL, _, _ := GetDialectDDL("sqlite3")
_, err = db.Exec(cursorDDL)
if err != nil {
t.Fatalf("failed to create cursor table: %v", err)
}
// 验证初始记录存在
var count int
err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor WHERE id = 1").Scan(&count)
if err != nil {
t.Fatalf("failed to count cursor records: %v", err)
}
if count != 1 {
t.Errorf("expected 1 cursor record, got %d", count)
}
}
// TestRetryTableIndexes 测试重试表索引
func TestRetryTableIndexes(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
_, _, retryDDL, _ := GetDialectDDL("sqlite3")
_, err = db.Exec(retryDDL)
if err != nil {
t.Fatalf("failed to create retry table: %v", err)
}
// 验证索引存在
expectedIndexes := []string{
"idx_retry_status",
"idx_retry_next_retry_at",
}
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='trustlog_retry'")
if err != nil {
t.Fatalf("failed to get indexes: %v", err)
}
defer rows.Close()
foundIndexes := make(map[string]bool)
for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
t.Fatalf("failed to scan row: %v", err)
}
foundIndexes[name] = true
}
for _, idx := range expectedIndexes {
if !foundIndexes[idx] {
t.Errorf("expected index %s not found", idx)
}
}
}
// TestNullableFields 测试可空字段
func TestNullableFields(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
opDDL, _, _, _ := GetDialectDDL("sqlite3")
_, err = db.Exec(opDDL)
if err != nil {
t.Fatalf("failed to create operation table: %v", err)
}
// 插入测试数据IP 字段为 NULL
ctx := context.Background()
_, err = db.ExecContext(ctx, `
INSERT INTO operation (
op_id, doid, producer_id, op_source, op_code,
do_prefix, do_repository, trustlog_status, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", 100,
"10.1000", "repo", "NOT_TRUSTLOGGED", time.Now())
if err != nil {
t.Fatalf("failed to insert record with null IPs: %v", err)
}
// 查询验证
var clientIP, serverIP sql.NullString
err = db.QueryRowContext(ctx, "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", "test-001").
Scan(&clientIP, &serverIP)
if err != nil {
t.Fatalf("failed to query record: %v", err)
}
if clientIP.Valid {
t.Error("clientIP should be NULL")
}
if serverIP.Valid {
t.Error("serverIP should be NULL")
}
}
// 辅助函数
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}