refactor: 重构trustlog-sdk目录结构到trustlog/go-trustlog

- 将所有trustlog-sdk文件移动到trustlog/go-trustlog/目录
- 更新README中所有import路径从trustlog-sdk改为go-trustlog
- 更新cookiecutter配置文件中的项目名称
- 更新根目录.lefthook.yml以引用新位置的配置
- 添加go.sum文件到版本控制
- 删除过时的示例文件

这次重构与trustlog-server保持一致的目录结构,
为未来支持多语言SDK(Python、Java等)预留空间。
This commit is contained in:
ryan
2025-12-22 13:37:57 +08:00
commit d313449c5c
87 changed files with 20622 additions and 0 deletions

156
api/highclient/client.go Normal file
View File

@@ -0,0 +1,156 @@
package highclient
import (
"errors"
"fmt"
"github.com/ThreeDotsLabs/watermill/message"
"go.yandata.net/iod/iod/trustlog-sdk/api/adapter"
"go.yandata.net/iod/iod/trustlog-sdk/api/logger"
"go.yandata.net/iod/iod/trustlog-sdk/api/model"
)
type Client struct {
publisher message.Publisher
logger logger.Logger
envelopeConfig model.EnvelopeConfig
}
// NewClient 创建HighClient,使用Envelope序列化方式.
// publisher可以使用任意包含forwarder创建的publisher,但是我们所有的订阅者必须可以处理Envelope格式的消息.
// 参数:
// - publisher: 消息发布器
// - logger: 日志记录器
// - envelopeConfig: SM2密钥配置,用于签名和序列化
func NewClient(publisher message.Publisher, logger logger.Logger, envelopeConfig model.EnvelopeConfig) *Client {
return &Client{
publisher: publisher,
logger: logger,
envelopeConfig: envelopeConfig,
}
}
func (c *Client) GetLow() message.Publisher {
return c.publisher
}
func (c *Client) OperationPublish(operation *model.Operation) error {
if operation == nil {
c.logger.Error("operation publish failed: operation is nil")
return errors.New("operation cannot be nil")
}
c.logger.Debug("publishing operation",
"opID", operation.OpID,
"opType", operation.OpType,
"doPrefix", operation.DoPrefix,
)
err := publish(operation, adapter.OperationTopic, c.publisher, c.envelopeConfig, c.logger)
if err != nil {
c.logger.Error("operation publish failed",
"opID", operation.OpID,
"error", err,
)
return err
}
c.logger.Info("operation published successfully",
"opID", operation.OpID,
"opType", operation.OpType,
)
return nil
}
func (c *Client) RecordPublish(record *model.Record) error {
if record == nil {
c.logger.Error("record publish failed: record is nil")
return errors.New("record cannot be nil")
}
c.logger.Debug("publishing record",
"recordID", record.ID,
"rcType", record.RCType,
"doPrefix", record.DoPrefix,
)
err := publish(record, adapter.RecordTopic, c.publisher, c.envelopeConfig, c.logger)
if err != nil {
c.logger.Error("record publish failed",
"recordID", record.ID,
"error", err,
)
return err
}
c.logger.Info("record published successfully",
"recordID", record.ID,
"rcType", record.RCType,
)
return nil
}
func (c *Client) Close() error {
c.logger.Info("closing high client")
err := c.publisher.Close()
if err != nil {
c.logger.Error("failed to close publisher", "error", err)
return err
}
c.logger.Info("high client closed successfully")
return nil
}
// publish 通用的发布函数,支持任何实现了 Trustlog 接口的类型。
// 使用 Envelope 格式序列化并发布到指定 topic。
func publish(
data model.Trustlog,
topic string,
publisher message.Publisher,
config model.EnvelopeConfig,
logger logger.Logger,
) error {
messageKey := data.Key()
logger.Debug("starting envelope serialization",
"messageKey", messageKey,
"topic", topic,
)
// 使用 Envelope 序列化MarshalTrustlog 会自动提取 producerID
envelopeData, err := model.MarshalTrustlog(data, config)
if err != nil {
logger.Error("envelope serialization failed",
"messageKey", messageKey,
"error", err,
)
return fmt.Errorf("failed to marshal envelope: %w", err)
}
logger.Debug("envelope serialized successfully",
"messageKey", messageKey,
"envelopeSize", len(envelopeData),
)
msg := message.NewMessage(messageKey, envelopeData)
logger.Debug("publishing message to topic",
"messageKey", messageKey,
"topic", topic,
)
if publishErr := publisher.Publish(topic, msg); publishErr != nil {
logger.Error("failed to publish to topic",
"messageKey", messageKey,
"topic", topic,
"error", publishErr,
)
return fmt.Errorf("failed to publish message to topic %s: %w", topic, publishErr)
}
logger.Debug("message published to topic successfully",
"messageKey", messageKey,
"topic", topic,
)
return nil
}

View File

@@ -0,0 +1,536 @@
package highclient_test
import (
"errors"
"fmt"
"testing"
"time"
"github.com/ThreeDotsLabs/watermill/message"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.yandata.net/iod/iod/trustlog-sdk/api/adapter"
"go.yandata.net/iod/iod/trustlog-sdk/api/highclient"
"go.yandata.net/iod/iod/trustlog-sdk/api/logger"
"go.yandata.net/iod/iod/trustlog-sdk/api/model"
)
// MockPublisher 模拟 message.Publisher.
type MockPublisher struct {
mock.Mock
}
func (m *MockPublisher) Publish(topic string, messages ...*message.Message) error {
args := m.Called(topic, messages)
return args.Error(0)
}
func (m *MockPublisher) Close() error {
args := m.Called()
return args.Error(0)
}
// generateTestKeys 生成测试用的SM2密钥对DER格式.
func generateTestKeys(t testing.TB) ([]byte, []byte) {
keyPair, err := model.GenerateSM2KeyPair()
if err != nil {
if t != nil {
require.NoError(t, err)
} else {
panic(err)
}
}
// 私钥DER编码
privateKeyDER, err := model.MarshalSM2PrivateDER(keyPair.Private)
if err != nil {
if t != nil {
require.NoError(t, err)
} else {
panic(err)
}
}
// 公钥DER编码
publicKeyDER, err := model.MarshalSM2PublicDER(keyPair.Public)
if err != nil {
if t != nil {
require.NoError(t, err)
} else {
panic(err)
}
}
return privateKeyDER, publicKeyDER
}
func TestNewClient(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
client := highclient.NewClient(mockPublisher, testLogger, config)
require.NotNil(t, client)
assert.Equal(t, mockPublisher, client.GetLow())
}
func TestClient_GetLow(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
client := highclient.NewClient(mockPublisher, testLogger, config)
lowLevelPublisher := client.GetLow()
assert.Equal(t, mockPublisher, lowLevelPublisher)
}
func TestClient_OperationPublish(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
tests := []struct {
name string
operation *model.Operation
setupMock func(*MockPublisher)
wantErr bool
errContains string
}{
{
name: "成功发布Operation",
operation: createTestOperation(t),
setupMock: func(mp *MockPublisher) {
mp.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).Return(nil).Once()
},
wantErr: false,
},
{
name: "发布失败",
operation: createTestOperation(t),
setupMock: func(mp *MockPublisher) {
mp.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).
Return(errors.New("publish failed")).
Once()
},
wantErr: true,
errContains: "publish failed",
},
{
name: "nil Operation应该失败",
operation: nil,
setupMock: func(_ *MockPublisher) {
// nil operation不会调用Publish因为会在之前失败
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
tt.setupMock(mockPublisher)
client := highclient.NewClient(mockPublisher, testLogger, config)
err := client.OperationPublish(tt.operation)
if tt.wantErr {
require.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
require.NoError(t, err)
}
mockPublisher.AssertExpectations(t)
})
}
}
func TestClient_RecordPublish(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
tests := []struct {
name string
record *model.Record
setupMock func(*MockPublisher)
wantErr bool
errContains string
}{
{
name: "成功发布Record",
record: createTestRecord(t),
setupMock: func(mp *MockPublisher) {
mp.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).Return(nil).Once()
},
wantErr: false,
},
{
name: "发布失败",
record: createTestRecord(t),
setupMock: func(mp *MockPublisher) {
mp.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).
Return(errors.New("record publish failed")).
Once()
},
wantErr: true,
errContains: "record publish failed",
},
{
name: "nil Record应该失败",
record: nil,
setupMock: func(_ *MockPublisher) {
// nil record不会调用Publish因为会在之前失败
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
tt.setupMock(mockPublisher)
client := highclient.NewClient(mockPublisher, testLogger, config)
err := client.RecordPublish(tt.record)
if tt.wantErr {
require.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
require.NoError(t, err)
}
mockPublisher.AssertExpectations(t)
})
}
}
func TestClient_Close(t *testing.T) {
tests := []struct {
name string
setupMock func(*MockPublisher)
wantErr bool
errContains string
}{
{
name: "成功关闭",
setupMock: func(mp *MockPublisher) {
mp.On("Close").Return(nil).Once()
},
wantErr: false,
},
{
name: "关闭失败",
setupMock: func(mp *MockPublisher) {
mp.On("Close").Return(errors.New("close failed")).Once()
},
wantErr: true,
errContains: "close failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
tt.setupMock(mockPublisher)
client := highclient.NewClient(mockPublisher, testLogger, config)
err := client.Close()
if tt.wantErr {
require.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
require.NoError(t, err)
}
mockPublisher.AssertExpectations(t)
})
}
}
func TestClient_MessageContent(t *testing.T) {
// 测试发布的消息内容是否正确
t.Run("Operation消息内容验证", func(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
operation := createTestOperation(t)
// 捕获发布的消息
var capturedMessages []*message.Message
mockPublisher.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).
Run(func(args mock.Arguments) {
messages, ok := args.Get(1).([]*message.Message)
if ok {
capturedMessages = messages
}
}).Return(nil).Once()
client := highclient.NewClient(mockPublisher, testLogger, config)
err := client.OperationPublish(operation)
require.NoError(t, err)
// 验证消息内容
require.Len(t, capturedMessages, 1)
msg := capturedMessages[0]
assert.Equal(t, operation.Key(), msg.UUID)
assert.NotEmpty(t, msg.Payload)
// 验证是Envelope格式可以反序列化
unmarshaledOp, err := model.UnmarshalOperation(msg.Payload)
require.NoError(t, err)
assert.Equal(t, operation.OpID, unmarshaledOp.OpID)
// 验证签名
verifyConfig := model.NewSM2VerifyConfig(publicKey)
verifiedEnv, err := model.VerifyEnvelopeWithConfig(msg.Payload, verifyConfig)
require.NoError(t, err)
assert.NotNil(t, verifiedEnv)
})
t.Run("Record消息内容验证", func(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
record := createTestRecord(t)
// 捕获发布的消息
var capturedMessages []*message.Message
mockPublisher.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).
Run(func(args mock.Arguments) {
messages, ok := args.Get(1).([]*message.Message)
if ok {
capturedMessages = messages
}
}).Return(nil).Once()
client := highclient.NewClient(mockPublisher, testLogger, config)
err := client.RecordPublish(record)
require.NoError(t, err)
// 验证消息内容
require.Len(t, capturedMessages, 1)
msg := capturedMessages[0]
assert.Equal(t, record.Key(), msg.UUID)
assert.NotEmpty(t, msg.Payload)
// 验证是Envelope格式可以反序列化
unmarshaledRecord, err := model.UnmarshalRecord(msg.Payload)
require.NoError(t, err)
assert.Equal(t, record.ID, unmarshaledRecord.ID)
// 验证签名
verifyConfig := model.NewSM2VerifyConfig(publicKey)
verifiedEnv, err := model.VerifyEnvelopeWithConfig(msg.Payload, verifyConfig)
require.NoError(t, err)
assert.NotNil(t, verifiedEnv)
})
}
func TestClient_ConcurrentPublish(t *testing.T) {
// 测试并发发布
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
// 设置期望的调用次数
publishCount := 100
mockPublisher.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).
Return(nil).Times(publishCount)
client := highclient.NewClient(mockPublisher, testLogger, config)
// 并发发布
errChan := make(chan error, publishCount)
for i := range publishCount {
go func(id int) {
//nolint:testifylint // 在goroutine中创建测试数据使用panic处理错误
operation := createTestOperationWithID(nil, fmt.Sprintf("concurrent-test-%d", id))
errChan <- client.OperationPublish(operation)
}(i)
}
// 收集结果
for range publishCount {
err := <-errChan
require.NoError(t, err)
}
mockPublisher.AssertExpectations(t)
}
func TestClient_EdgeCases(t *testing.T) {
t.Run("发布大型Operation", func(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
mockPublisher.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).
Return(nil).
Once()
client := highclient.NewClient(mockPublisher, testLogger, config)
// 创建包含大量数据的Operation
operation := createTestOperation(t)
operation.OpActor = string(make([]byte, 1000)) // 1KB数据
err := client.OperationPublish(operation)
require.NoError(t, err)
mockPublisher.AssertExpectations(t)
})
t.Run("发布大型Record", func(t *testing.T) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
mockPublisher.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).Return(nil).Once()
client := highclient.NewClient(mockPublisher, testLogger, config)
// 创建包含大量数据的Record
record := createTestRecord(t)
record.WithExtra(make([]byte, 500)) // 500字节的额外数据
err := client.RecordPublish(record)
require.NoError(t, err)
mockPublisher.AssertExpectations(t)
})
}
func TestClient_Integration(t *testing.T) {
// 集成测试 - 测试完整的工作流程
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(t)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
// 设置期望发布Operation -> 发布Record -> 关闭
mockPublisher.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).Return(nil).Once()
mockPublisher.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).Return(nil).Once()
mockPublisher.On("Close").Return(nil).Once()
client := highclient.NewClient(mockPublisher, testLogger, config)
// 发布Operation
operation := createTestOperation(t)
err := client.OperationPublish(operation)
require.NoError(t, err)
// 发布Record
record := createTestRecord(t)
err = client.RecordPublish(record)
require.NoError(t, err)
// 关闭客户端
err = client.Close()
require.NoError(t, err)
mockPublisher.AssertExpectations(t)
}
// 性能基准测试.
func BenchmarkClient_OperationPublish(b *testing.B) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(b)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
mockPublisher.On("Publish", adapter.OperationTopic, mock.AnythingOfType("[]*message.Message")).Return(nil)
client := highclient.NewClient(mockPublisher, testLogger, config)
operation := createTestOperation(b)
b.ResetTimer()
for range b.N {
err := client.OperationPublish(operation)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkClient_RecordPublish(b *testing.B) {
mockPublisher := &MockPublisher{}
testLogger := logger.NewLogger(logr.Discard())
privateKey, publicKey := generateTestKeys(b)
config := model.NewSM2EnvelopeConfig(privateKey, publicKey)
mockPublisher.On("Publish", adapter.RecordTopic, mock.AnythingOfType("[]*message.Message")).Return(nil)
client := highclient.NewClient(mockPublisher, testLogger, config)
record := createTestRecord(b)
b.ResetTimer()
for range b.N {
err := client.RecordPublish(record)
if err != nil {
b.Fatal(err)
}
}
}
// 测试辅助函数.
func createTestOperation(t testing.TB) *model.Operation {
return createTestOperationWithID(t, "test-operation-001")
}
func createTestOperationWithID(t testing.TB, id string) *model.Operation {
// 在并发测试中t可能为nil这是正常的
errorHandler := func(err error) {
if t != nil {
require.NoError(t, err)
} else if err != nil {
panic(err)
}
}
operation, err := model.NewFullOperation(
model.OpSourceDOIP,
model.OpTypeRetrieve,
"test-prefix",
"test-repo",
"test-prefix/test-repo/test-object",
"test-producer-id",
"test-actor",
"test request body",
"test response body",
time.Now(),
)
errorHandler(err)
operation.OpID = id // 设置自定义ID
return operation
}
func createTestRecord(t testing.TB) *model.Record {
record, err := model.NewFullRecord(
"test-prefix",
"test-producer-id",
time.Now(),
"test-operator",
[]byte("test extra data"),
"test-type",
)
require.NoError(t, err)
return record
}