主要变更: - 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
594 lines
13 KiB
Go
594 lines
13 KiB
Go
package model_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.yandata.net/iod/iod/go-trustlog/api/model"
|
|
)
|
|
|
|
func TestOperation_Key(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{
|
|
OpID: "test-op-id",
|
|
}
|
|
assert.Equal(t, "test-op-id", op.Key())
|
|
}
|
|
|
|
func TestOperation_CheckAndInit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
op *model.Operation
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid operation",
|
|
op: &model.Operation{
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "auto generate OpID",
|
|
op: &model.Operation{
|
|
OpID: "", // Will be auto-generated
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "auto set OpActor",
|
|
op: &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
OpActor: "", // Will be set to "SYSTEM"
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid doid format",
|
|
op: &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "invalid/123", // Doesn't start with "test/repo"
|
|
ProducerID: "producer-1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := tt.op.CheckAndInit()
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
if tt.name == "auto generate OpID" {
|
|
assert.NotEmpty(t, tt.op.OpID)
|
|
}
|
|
if tt.name == "auto set OpActor" {
|
|
assert.Equal(t, "SYSTEM", tt.op.OpActor)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOperation_RequestBodyFlexible(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "string",
|
|
input: "test",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "bytes",
|
|
input: []byte("test"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "nil",
|
|
input: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty bytes",
|
|
input: []byte{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid type",
|
|
input: 123,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
op := &model.Operation{}
|
|
err := op.RequestBodyFlexible(tt.input)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOperation_ResponseBodyFlexible(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "string",
|
|
input: "test",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "bytes",
|
|
input: []byte("test"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "nil",
|
|
input: nil,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
op := &model.Operation{}
|
|
err := op.ResponseBodyFlexible(tt.input)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOperation_WithRequestBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{}
|
|
result := op.WithRequestBody([]byte("test"))
|
|
assert.Equal(t, op, result)
|
|
}
|
|
|
|
func TestOperation_WithResponseBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{}
|
|
result := op.WithResponseBody([]byte("test"))
|
|
assert.Equal(t, op, result)
|
|
}
|
|
|
|
func TestOperation_MarshalUnmarshalBinary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
original := &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
OpActor: "actor-1",
|
|
}
|
|
|
|
// Marshal
|
|
data, err := original.MarshalBinary()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, data)
|
|
|
|
// Unmarshal
|
|
result := &model.Operation{}
|
|
err = result.UnmarshalBinary(data)
|
|
require.NoError(t, err)
|
|
|
|
// Verify
|
|
assert.Equal(t, original.OpID, result.OpID)
|
|
assert.Equal(t, original.OpSource, result.OpSource)
|
|
assert.Equal(t, original.OpType, result.OpType)
|
|
assert.Equal(t, original.DoPrefix, result.DoPrefix)
|
|
assert.Equal(t, original.DoRepository, result.DoRepository)
|
|
assert.Equal(t, original.Doid, result.Doid)
|
|
assert.Equal(t, original.ProducerID, result.ProducerID)
|
|
assert.Equal(t, original.OpActor, result.OpActor)
|
|
// 验证纳秒精度被保留
|
|
assert.Equal(t, original.Timestamp.UnixNano(), result.Timestamp.UnixNano(),
|
|
"时间戳的纳秒精度应该被保留")
|
|
}
|
|
|
|
func TestOperation_MarshalBinary_Empty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
}
|
|
// MarshalBinary should succeed even without CheckAndInit
|
|
// It just serializes the data
|
|
data, err := op.MarshalBinary()
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, data)
|
|
}
|
|
|
|
func TestOperation_UnmarshalBinary_Empty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{}
|
|
err := op.UnmarshalBinary([]byte{})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestOperation_GetProducerID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{
|
|
ProducerID: "producer-123",
|
|
}
|
|
assert.Equal(t, "producer-123", op.GetProducerID())
|
|
}
|
|
|
|
func TestOperation_DoHash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
OpActor: "actor-1",
|
|
}
|
|
|
|
err := op.CheckAndInit()
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
hashData, err := op.DoHash(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, hashData)
|
|
assert.Equal(t, op.OpID, hashData.Key())
|
|
assert.NotEmpty(t, hashData.Hash())
|
|
}
|
|
|
|
func TestOperationHashData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// OperationHashData is created through DoHash, test it indirectly
|
|
op := &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
OpActor: "actor-1",
|
|
}
|
|
|
|
err := op.CheckAndInit()
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
hashData, err := op.DoHash(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, hashData)
|
|
assert.Equal(t, "op-123", hashData.Key())
|
|
assert.NotEmpty(t, hashData.Hash())
|
|
assert.Equal(t, model.Sha256Simd, hashData.Type())
|
|
}
|
|
|
|
func TestOperation_UnmarshalBinary_InvalidData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{}
|
|
err := op.UnmarshalBinary([]byte("invalid-cbor-data"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to unmarshal operation from CBOR")
|
|
}
|
|
|
|
func TestOperation_MarshalTrustlog_EmptyProducerID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create an operation with empty ProducerID
|
|
// MarshalBinary will fail validation, but MarshalTrustlog checks ProducerID first
|
|
op := &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "", // Empty ProducerID
|
|
OpActor: "actor-1",
|
|
}
|
|
|
|
config := model.NewEnvelopeConfig(model.NewNopSigner())
|
|
_, err := model.MarshalTrustlog(op, config)
|
|
// MarshalTrustlog checks ProducerID before calling MarshalBinary
|
|
require.Error(t, err)
|
|
// Error could be from ProducerID check or MarshalBinary validation
|
|
assert.True(t,
|
|
err.Error() == "producerID cannot be empty" ||
|
|
strings.Contains(err.Error(), "ProducerID") ||
|
|
strings.Contains(err.Error(), "producerID"))
|
|
}
|
|
|
|
func TestOperation_MarshalTrustlog_NilSigner(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
op := &model.Operation{
|
|
OpID: "op-123",
|
|
Timestamp: time.Now(),
|
|
OpSource: model.OpSourceIRP,
|
|
OpType: string(model.OpTypeOCCreateHandle),
|
|
DoPrefix: "test",
|
|
DoRepository: "repo",
|
|
Doid: "test/repo/123",
|
|
ProducerID: "producer-1",
|
|
OpActor: "actor-1",
|
|
}
|
|
|
|
err := op.CheckAndInit()
|
|
require.NoError(t, err)
|
|
|
|
config := model.EnvelopeConfig{Signer: nil}
|
|
_, err = model.MarshalTrustlog(op, config)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "signer is required")
|
|
}
|
|
|
|
func TestGetOpTypesBySource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
source model.Source
|
|
wantTypes []model.Type
|
|
}{
|
|
{
|
|
name: "IRP操作类型",
|
|
source: model.OpSourceIRP,
|
|
wantTypes: []model.Type{
|
|
model.OpTypeOCCreateHandle,
|
|
model.OpTypeOCDeleteHandle,
|
|
model.OpTypeOCAddValue,
|
|
},
|
|
},
|
|
{
|
|
name: "DOIP操作类型",
|
|
source: model.OpSourceDOIP,
|
|
wantTypes: []model.Type{
|
|
model.OpTypeHello,
|
|
model.OpTypeCreate,
|
|
model.OpTypeDelete,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
opTypes := model.GetOpTypesBySource(tt.source)
|
|
assert.NotNil(t, opTypes)
|
|
// Verify expected types are included
|
|
for _, expectedType := range tt.wantTypes {
|
|
assert.Contains(t, opTypes, expectedType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidOpType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
source model.Source
|
|
opType string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "IRP有效操作类型",
|
|
source: model.OpSourceIRP,
|
|
opType: string(model.OpTypeOCCreateHandle),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "IRP无效操作类型",
|
|
source: model.OpSourceIRP,
|
|
opType: string(model.OpTypeHello),
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "DOIP有效操作类型",
|
|
source: model.OpSourceDOIP,
|
|
opType: string(model.OpTypeHello),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "DOIP无效操作类型",
|
|
source: model.OpSourceDOIP,
|
|
opType: string(model.OpTypeOCCreateHandle),
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "未知来源和类型",
|
|
source: model.Source("unknown"),
|
|
opType: "unknown",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := model.IsValidOpType(tt.source, tt.opType)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewFullOperation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
opSource model.Source
|
|
opType string
|
|
doPrefix string
|
|
doRepository string
|
|
doid string
|
|
producerID string
|
|
opActor string
|
|
requestBody interface{}
|
|
responseBody interface{}
|
|
timestamp time.Time
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "成功创建完整操作",
|
|
opSource: model.OpSourceIRP,
|
|
opType: string(model.OpTypeOCCreateHandle),
|
|
doPrefix: "test",
|
|
doRepository: "repo",
|
|
doid: "test/repo/123",
|
|
producerID: "producer-1",
|
|
opActor: "actor-1",
|
|
requestBody: []byte(`{"key": "value"}`),
|
|
responseBody: []byte(`{"status": "ok"}`),
|
|
timestamp: time.Now(),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "空请求体和响应体",
|
|
opSource: model.OpSourceIRP,
|
|
opType: string(model.OpTypeOCCreateHandle),
|
|
doPrefix: "test",
|
|
doRepository: "repo",
|
|
doid: "test/repo/123",
|
|
producerID: "producer-1",
|
|
opActor: "actor-1",
|
|
requestBody: nil,
|
|
responseBody: nil,
|
|
timestamp: time.Now(),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "字符串类型的请求体",
|
|
opSource: model.OpSourceIRP,
|
|
opType: string(model.OpTypeOCCreateHandle),
|
|
doPrefix: "test",
|
|
doRepository: "repo",
|
|
doid: "test/repo/123",
|
|
producerID: "producer-1",
|
|
opActor: "actor-1",
|
|
requestBody: "string body",
|
|
responseBody: "string response",
|
|
timestamp: time.Now(),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
op, err := model.NewFullOperation(
|
|
tt.opSource,
|
|
tt.opType,
|
|
tt.doPrefix,
|
|
tt.doRepository,
|
|
tt.doid,
|
|
tt.producerID,
|
|
tt.opActor,
|
|
tt.requestBody,
|
|
tt.responseBody,
|
|
tt.timestamp,
|
|
)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
assert.Nil(t, op)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, op)
|
|
assert.Equal(t, tt.opSource, op.OpSource)
|
|
assert.Equal(t, tt.opType, op.OpType)
|
|
assert.Equal(t, tt.doPrefix, op.DoPrefix)
|
|
assert.Equal(t, tt.doRepository, op.DoRepository)
|
|
assert.Equal(t, tt.doid, op.Doid)
|
|
assert.Equal(t, tt.producerID, op.ProducerID)
|
|
assert.Equal(t, tt.opActor, op.OpActor)
|
|
assert.NotEmpty(t, op.OpID) // Should be auto-generated
|
|
}
|
|
})
|
|
}
|
|
}
|