主要变更: - Operation.OpType: Type → string - NewFullOperation 参数: opType Type → opType string - IsValidOpType 参数: opType Type → opType string - operationMeta.OpType: *Type → *string - queryclient.ListRequest.OpType: model.Type → string 优点: - 更灵活,支持动态扩展操作类型 - 不再受限于预定义的枚举常量 - 简化类型转换逻辑 兼容性: - Type 常量定义保持不变 (OpTypeCreate, OpTypeUpdate 等) - 使用时需要 string() 转换: string(model.OpTypeCreate) - 所有单元测试已更新并通过 (100%) 测试结果: ✅ api/adapter - PASS ✅ api/highclient - PASS ✅ api/logger - PASS ✅ api/model - PASS ✅ api/persistence - PASS ✅ api/queryclient - PASS ✅ internal/* - PASS
537 lines
15 KiB
Go
537 lines
15 KiB
Go
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/go-trustlog/api/adapter"
|
||
"go.yandata.net/iod/iod/go-trustlog/api/highclient"
|
||
"go.yandata.net/iod/iod/go-trustlog/api/logger"
|
||
"go.yandata.net/iod/iod/go-trustlog/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,
|
||
string(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
|
||
}
|