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:
441
api/queryclient/client.go
Normal file
441
api/queryclient/client.go
Normal file
@@ -0,0 +1,441 @@
|
||||
package queryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/grpc/pb"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/logger"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/model"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/internal/grpcclient"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultChannelBuffer 是channel的默认缓冲区大小.
|
||||
defaultChannelBuffer = 10
|
||||
)
|
||||
|
||||
// serverClients 封装单个服务器的两种服务客户端.
|
||||
type serverClients struct {
|
||||
opClient pb.OperationValidationServiceClient
|
||||
recClient pb.RecordValidationServiceClient
|
||||
}
|
||||
|
||||
// Client 查询客户端,包装gRPC客户端提供操作和记录的查询及验证功能.
|
||||
type Client struct {
|
||||
connLB *grpcclient.LoadBalancer[*serverClients]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// ClientConfig 客户端配置.
|
||||
type ClientConfig = grpcclient.Config
|
||||
|
||||
// NewClient 创建新的查询客户端.
|
||||
func NewClient(config ClientConfig, logger logger.Logger) (*Client, error) {
|
||||
// 获取服务器地址列表
|
||||
addrs, err := config.GetAddrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建连接负载均衡器,每个连接同时创建两种服务的客户端
|
||||
connLB, err := grpcclient.NewLoadBalancer(
|
||||
addrs,
|
||||
config.DialOptions,
|
||||
func(conn grpc.ClientConnInterface) *serverClients {
|
||||
return &serverClients{
|
||||
opClient: pb.NewOperationValidationServiceClient(conn),
|
||||
recClient: pb.NewRecordValidationServiceClient(conn),
|
||||
}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("Query client initialized", "serverCount", len(addrs))
|
||||
|
||||
return &Client{
|
||||
connLB: connLB,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListOperationsRequest 列表查询请求参数.
|
||||
type ListOperationsRequest struct {
|
||||
// 分页参数
|
||||
PageSize uint64 // 页面大小
|
||||
PreTime time.Time // 上一页最后一个时间(用于游标分页)
|
||||
|
||||
// 可选过滤条件
|
||||
Timestamp *time.Time // 操作时间戳
|
||||
OpSource model.Source // 操作来源
|
||||
OpType model.Type // 操作类型
|
||||
DoPrefix string // 数据前缀
|
||||
DoRepository string // 数据仓库
|
||||
}
|
||||
|
||||
// ListOperationsResponse 列表查询响应.
|
||||
type ListOperationsResponse struct {
|
||||
Count int64 // 数据总量
|
||||
Data []*model.Operation // 操作列表
|
||||
}
|
||||
|
||||
// ListOperations 查询操作列表.
|
||||
func (c *Client) ListOperations(ctx context.Context, req ListOperationsRequest) (*ListOperationsResponse, error) {
|
||||
c.logger.DebugContext(ctx, "Querying operations list", "pageSize", req.PageSize)
|
||||
|
||||
// 使用负载均衡器获取客户端
|
||||
clients := c.connLB.Next()
|
||||
client := clients.opClient
|
||||
|
||||
// 构建protobuf请求
|
||||
pbReq := &pb.ListOperationReq{
|
||||
PageSize: req.PageSize,
|
||||
OpSource: string(req.OpSource),
|
||||
OpType: string(req.OpType),
|
||||
DoPrefix: req.DoPrefix,
|
||||
DoRepository: req.DoRepository,
|
||||
}
|
||||
|
||||
// 设置可选参数
|
||||
if !req.PreTime.IsZero() {
|
||||
pbReq.PreTime = timestamppb.New(req.PreTime)
|
||||
}
|
||||
if req.Timestamp != nil {
|
||||
pbReq.Timestamp = timestamppb.New(*req.Timestamp)
|
||||
}
|
||||
|
||||
// 调用gRPC
|
||||
pbRes, err := client.ListOperations(ctx, pbReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list operations: %w", err)
|
||||
}
|
||||
|
||||
// 转换响应
|
||||
operations := make([]*model.Operation, 0, len(pbRes.GetData()))
|
||||
for _, pbOp := range pbRes.GetData() {
|
||||
op, convertErr := model.FromProtobuf(pbOp)
|
||||
if convertErr != nil {
|
||||
c.logger.ErrorContext(ctx, "Failed to convert operation", "error", convertErr)
|
||||
continue
|
||||
}
|
||||
operations = append(operations, op)
|
||||
}
|
||||
|
||||
return &ListOperationsResponse{
|
||||
Count: pbRes.GetCount(),
|
||||
Data: operations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidationRequest 取证验证请求参数.
|
||||
type ValidationRequest struct {
|
||||
Time time.Time // 操作时间戳
|
||||
OpID string // 操作唯一标识符
|
||||
OpType string // 操作类型
|
||||
DoRepository string // 数据仓库标识
|
||||
}
|
||||
|
||||
// ValidateOperation 执行操作取证验证,返回流式结果通道
|
||||
// 该方法会启动一个goroutine接收流式响应,通过返回的channel发送结果
|
||||
// 当流结束或发生错误时,channel会被关闭.
|
||||
//
|
||||
//nolint:dupl // 与 ValidateRecord 有相似逻辑,但处理不同的数据类型和 gRPC 服务
|
||||
func (c *Client) ValidateOperation(ctx context.Context, req ValidationRequest) (<-chan *model.ValidationResult, error) {
|
||||
c.logger.InfoContext(ctx, "Starting validation for operation", "opID", req.OpID)
|
||||
|
||||
// 使用负载均衡器获取客户端
|
||||
clients := c.connLB.Next()
|
||||
client := clients.opClient
|
||||
|
||||
// 构建protobuf请求
|
||||
pbReq := &pb.ValidationReq{
|
||||
Time: timestamppb.New(req.Time),
|
||||
OpId: req.OpID,
|
||||
OpType: req.OpType,
|
||||
DoRepository: req.DoRepository,
|
||||
}
|
||||
|
||||
// 调用gRPC流式方法
|
||||
stream, err := client.ValidateOperation(ctx, pbReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start validation: %w", err)
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *model.ValidationResult, defaultChannelBuffer)
|
||||
|
||||
// 启动goroutine接收流式响应
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
|
||||
for {
|
||||
pbRes, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if errors.Is(recvErr, io.EOF) {
|
||||
// 流正常结束
|
||||
c.logger.DebugContext(ctx, "Validation stream completed", "opID", req.OpID)
|
||||
return
|
||||
}
|
||||
// 发生错误
|
||||
c.logger.ErrorContext(ctx, "Error receiving validation result", "error", recvErr)
|
||||
// 发送错误结果
|
||||
resultChan <- &model.ValidationResult{
|
||||
Code: model.ValidationCodeFailed,
|
||||
Msg: fmt.Sprintf("Stream error: %v", recvErr),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 转换并发送结果
|
||||
result, convertErr := model.FromProtobufValidationResult(pbRes)
|
||||
if convertErr != nil {
|
||||
c.logger.ErrorContext(ctx, "Failed to convert validation result", "error", convertErr)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case resultChan <- result:
|
||||
c.logger.DebugContext(ctx, "Sent validation result", "code", result.Code, "progress", result.Progress)
|
||||
case <-ctx.Done():
|
||||
c.logger.InfoContext(ctx, "Context cancelled, stopping validation stream")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return resultChan, nil
|
||||
}
|
||||
|
||||
// ValidateOperationSync 同步执行操作取证验证,阻塞直到获得最终结果
|
||||
// 该方法会处理所有中间进度,只返回最终的完成结果.
|
||||
func (c *Client) ValidateOperationSync(
|
||||
ctx context.Context,
|
||||
req ValidationRequest,
|
||||
progressCallback func(*model.ValidationResult),
|
||||
) (*model.ValidationResult, error) {
|
||||
resultChan, err := c.ValidateOperation(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalResult *model.ValidationResult
|
||||
for result := range resultChan {
|
||||
if result.IsCompleted() || result.IsFailed() {
|
||||
finalResult = result
|
||||
break
|
||||
}
|
||||
|
||||
// 如果提供了进度回调,则调用
|
||||
if progressCallback != nil {
|
||||
progressCallback(result)
|
||||
}
|
||||
}
|
||||
|
||||
if finalResult == nil {
|
||||
return nil, errors.New("validation completed without final result")
|
||||
}
|
||||
|
||||
return finalResult, nil
|
||||
}
|
||||
|
||||
// ListRecordsRequest 列表查询请求参数.
|
||||
type ListRecordsRequest struct {
|
||||
// 分页参数
|
||||
PageSize uint64 // 页面大小
|
||||
PreTime time.Time // 上一页最后一个时间(用于游标分页)
|
||||
|
||||
// 可选过滤条件
|
||||
DoPrefix string // 数据前缀
|
||||
RCType string // 记录类型
|
||||
}
|
||||
|
||||
// ListRecordsResponse 列表查询响应.
|
||||
type ListRecordsResponse struct {
|
||||
Count int64 // 数据总量
|
||||
Data []*model.Record // 记录列表
|
||||
}
|
||||
|
||||
// ListRecords 查询记录列表.
|
||||
func (c *Client) ListRecords(ctx context.Context, req ListRecordsRequest) (*ListRecordsResponse, error) {
|
||||
c.logger.DebugContext(ctx, "Querying records list", "pageSize", req.PageSize)
|
||||
|
||||
// 使用负载均衡器获取客户端
|
||||
clients := c.connLB.Next()
|
||||
client := clients.recClient
|
||||
|
||||
// 构建protobuf请求
|
||||
pbReq := &pb.ListRecordReq{
|
||||
PageSize: req.PageSize,
|
||||
DoPrefix: req.DoPrefix,
|
||||
RcType: req.RCType,
|
||||
}
|
||||
|
||||
// 设置可选参数
|
||||
if !req.PreTime.IsZero() {
|
||||
pbReq.PreTime = timestamppb.New(req.PreTime)
|
||||
}
|
||||
|
||||
// 调用gRPC
|
||||
pbRes, err := client.ListRecords(ctx, pbReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list records: %w", err)
|
||||
}
|
||||
|
||||
// 转换响应
|
||||
records := make([]*model.Record, 0, len(pbRes.GetData()))
|
||||
for _, pbRec := range pbRes.GetData() {
|
||||
rec, convertErr := model.RecordFromProtobuf(pbRec)
|
||||
if convertErr != nil {
|
||||
c.logger.ErrorContext(ctx, "Failed to convert record", "error", convertErr)
|
||||
continue
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
|
||||
return &ListRecordsResponse{
|
||||
Count: pbRes.GetCount(),
|
||||
Data: records,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RecordValidationRequest 记录验证请求参数.
|
||||
type RecordValidationRequest struct {
|
||||
Timestamp time.Time // 记录时间戳
|
||||
RecordID string // 要验证的记录ID
|
||||
DoPrefix string // 数据前缀(可选)
|
||||
RCType string // 记录类型
|
||||
}
|
||||
|
||||
// ValidateRecord 执行记录验证,返回流式结果通道
|
||||
// 该方法会启动一个goroutine接收流式响应,通过返回的channel发送结果
|
||||
// 当流结束或发生错误时,channel会被关闭.
|
||||
//
|
||||
//nolint:dupl // 与 ValidateOperation 有相似逻辑,但处理不同的数据类型和 gRPC 服务
|
||||
func (c *Client) ValidateRecord(
|
||||
ctx context.Context,
|
||||
req RecordValidationRequest,
|
||||
) (<-chan *model.RecordValidationResult, error) {
|
||||
c.logger.InfoContext(ctx, "Starting validation for record", "recordID", req.RecordID)
|
||||
|
||||
// 使用负载均衡器获取客户端
|
||||
clients := c.connLB.Next()
|
||||
client := clients.recClient
|
||||
|
||||
// 构建protobuf请求
|
||||
pbReq := &pb.RecordValidationReq{
|
||||
Timestamp: timestamppb.New(req.Timestamp),
|
||||
RecordId: req.RecordID,
|
||||
DoPrefix: req.DoPrefix,
|
||||
RcType: req.RCType,
|
||||
}
|
||||
|
||||
// 调用gRPC流式方法
|
||||
stream, err := client.ValidateRecord(ctx, pbReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start validation: %w", err)
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *model.RecordValidationResult, defaultChannelBuffer)
|
||||
|
||||
// 启动goroutine接收流式响应
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
|
||||
for {
|
||||
pbRes, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if errors.Is(recvErr, io.EOF) {
|
||||
// 流正常结束
|
||||
c.logger.DebugContext(ctx, "Validation stream completed", "recordID", req.RecordID)
|
||||
return
|
||||
}
|
||||
// 发生错误
|
||||
c.logger.ErrorContext(ctx, "Error receiving validation result", "error", recvErr)
|
||||
// 发送错误结果
|
||||
resultChan <- &model.RecordValidationResult{
|
||||
Code: model.ValidationCodeFailed,
|
||||
Msg: fmt.Sprintf("Stream error: %v", recvErr),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 转换并发送结果
|
||||
result, convertErr := model.RecordFromProtobufValidationResult(pbRes)
|
||||
if convertErr != nil {
|
||||
c.logger.ErrorContext(ctx, "Failed to convert validation result", "error", convertErr)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case resultChan <- result:
|
||||
c.logger.DebugContext(ctx, "Sent validation result", "code", result.Code, "progress", result.Progress)
|
||||
case <-ctx.Done():
|
||||
c.logger.InfoContext(ctx, "Context cancelled, stopping validation stream")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return resultChan, nil
|
||||
}
|
||||
|
||||
// ValidateRecordSync 同步执行记录验证,阻塞直到获得最终结果
|
||||
// 该方法会处理所有中间进度,只返回最终的完成结果.
|
||||
func (c *Client) ValidateRecordSync(
|
||||
ctx context.Context,
|
||||
req RecordValidationRequest,
|
||||
progressCallback func(*model.RecordValidationResult),
|
||||
) (*model.RecordValidationResult, error) {
|
||||
resultChan, err := c.ValidateRecord(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalResult *model.RecordValidationResult
|
||||
for result := range resultChan {
|
||||
if result.IsCompleted() || result.IsFailed() {
|
||||
finalResult = result
|
||||
break
|
||||
}
|
||||
|
||||
// 如果提供了进度回调,则调用
|
||||
if progressCallback != nil {
|
||||
progressCallback(result)
|
||||
}
|
||||
}
|
||||
|
||||
if finalResult == nil {
|
||||
return nil, errors.New("validation completed without final result")
|
||||
}
|
||||
|
||||
return finalResult, nil
|
||||
}
|
||||
|
||||
// Close 关闭客户端连接.
|
||||
func (c *Client) Close() error {
|
||||
if c.connLB != nil {
|
||||
return c.connLB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLowLevelOperationClient 获取底层的操作gRPC客户端(用于高级用户自定义操作)
|
||||
// 注意:使用负载均衡时,每次调用此方法将返回轮询的下一个客户端.
|
||||
func (c *Client) GetLowLevelOperationClient() pb.OperationValidationServiceClient {
|
||||
return c.connLB.Next().opClient
|
||||
}
|
||||
|
||||
// GetLowLevelRecordClient 获取底层的记录gRPC客户端(用于高级用户自定义操作)
|
||||
// 注意:使用负载均衡时,每次调用此方法将返回轮询的下一个客户端.
|
||||
func (c *Client) GetLowLevelRecordClient() pb.RecordValidationServiceClient {
|
||||
return c.connLB.Next().recClient
|
||||
}
|
||||
627
api/queryclient/client_test.go
Normal file
627
api/queryclient/client_test.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package queryclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/grpc/pb"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/logger"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/model"
|
||||
"go.yandata.net/iod/iod/trustlog-sdk/api/queryclient"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 1024
|
||||
|
||||
//nolint:gochecknoglobals // 测试文件中的全局变量是可接受的
|
||||
var testLogger = logger.NewLogger(logr.Discard())
|
||||
|
||||
// mockOperationServer 模拟操作验证服务.
|
||||
type mockOperationServer struct {
|
||||
pb.UnimplementedOperationValidationServiceServer
|
||||
}
|
||||
|
||||
func (s *mockOperationServer) ListOperations(
|
||||
_ context.Context,
|
||||
_ *pb.ListOperationReq,
|
||||
) (*pb.ListOperationRes, error) {
|
||||
return &pb.ListOperationRes{
|
||||
Count: 2,
|
||||
Data: []*pb.OperationData{
|
||||
{
|
||||
OpId: "op-1",
|
||||
Timestamp: timestamppb.Now(),
|
||||
OpSource: "test",
|
||||
OpType: "create",
|
||||
DoPrefix: "test",
|
||||
DoRepository: "repo",
|
||||
Doid: "test/repo/123",
|
||||
ProducerId: "producer-1",
|
||||
OpActor: "tester",
|
||||
},
|
||||
{
|
||||
OpId: "op-2",
|
||||
Timestamp: timestamppb.Now(),
|
||||
OpSource: "test",
|
||||
OpType: "update",
|
||||
DoPrefix: "test",
|
||||
DoRepository: "repo",
|
||||
Doid: "test/repo/456",
|
||||
ProducerId: "producer-1",
|
||||
OpActor: "tester",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *mockOperationServer) ValidateOperation(
|
||||
req *pb.ValidationReq,
|
||||
stream pb.OperationValidationService_ValidateOperationServer,
|
||||
) error {
|
||||
// 发送进度消息
|
||||
_ = stream.Send(&pb.ValidationStreamRes{
|
||||
Code: 100,
|
||||
Msg: "Processing",
|
||||
Progress: "50%",
|
||||
})
|
||||
|
||||
// 发送完成消息
|
||||
_ = stream.Send(&pb.ValidationStreamRes{
|
||||
Code: 200,
|
||||
Msg: "Completed",
|
||||
Progress: "100%",
|
||||
Data: &pb.OperationData{
|
||||
OpId: req.GetOpId(),
|
||||
Timestamp: req.GetTime(),
|
||||
OpSource: "test",
|
||||
OpType: req.GetOpType(),
|
||||
DoPrefix: "test",
|
||||
DoRepository: req.GetDoRepository(),
|
||||
Doid: "test/repo/123",
|
||||
ProducerId: "producer-1",
|
||||
OpActor: "tester",
|
||||
},
|
||||
Proof: &pb.Proof{
|
||||
ColItems: []*pb.MerkleTreeProofItem{
|
||||
{Floor: 1, Hash: "hash1", Left: true},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockRecordServer 模拟记录验证服务.
|
||||
type mockRecordServer struct {
|
||||
pb.UnimplementedRecordValidationServiceServer
|
||||
}
|
||||
|
||||
func (s *mockRecordServer) ListRecords(
|
||||
_ context.Context,
|
||||
_ *pb.ListRecordReq,
|
||||
) (*pb.ListRecordRes, error) {
|
||||
return &pb.ListRecordRes{
|
||||
Count: 2,
|
||||
Data: []*pb.RecordData{
|
||||
{
|
||||
Id: "rec-1",
|
||||
DoPrefix: "test",
|
||||
ProducerId: "producer-1",
|
||||
Timestamp: timestamppb.Now(),
|
||||
Operator: "tester",
|
||||
RcType: "log",
|
||||
},
|
||||
{
|
||||
Id: "rec-2",
|
||||
DoPrefix: "test",
|
||||
ProducerId: "producer-1",
|
||||
Timestamp: timestamppb.Now(),
|
||||
Operator: "tester",
|
||||
RcType: "log",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *mockRecordServer) ValidateRecord(
|
||||
req *pb.RecordValidationReq,
|
||||
stream pb.RecordValidationService_ValidateRecordServer,
|
||||
) error {
|
||||
// 发送进度消息
|
||||
_ = stream.Send(&pb.RecordValidationStreamRes{
|
||||
Code: 100,
|
||||
Msg: "Processing",
|
||||
Progress: "50%",
|
||||
})
|
||||
|
||||
// 发送完成消息
|
||||
_ = stream.Send(&pb.RecordValidationStreamRes{
|
||||
Code: 200,
|
||||
Msg: "Completed",
|
||||
Progress: "100%",
|
||||
Result: &pb.RecordData{
|
||||
Id: req.GetRecordId(),
|
||||
DoPrefix: req.GetDoPrefix(),
|
||||
ProducerId: "producer-1",
|
||||
Timestamp: req.GetTimestamp(),
|
||||
Operator: "tester",
|
||||
RcType: req.GetRcType(),
|
||||
},
|
||||
Proof: &pb.Proof{
|
||||
ColItems: []*pb.MerkleTreeProofItem{
|
||||
{Floor: 1, Hash: "hash1", Left: true},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupTestServer 创建测试用的 gRPC server.
|
||||
func setupTestServer(t *testing.T) (*grpc.Server, *bufconn.Listener) {
|
||||
lis := bufconn.Listen(bufSize)
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterOperationValidationServiceServer(s, &mockOperationServer{})
|
||||
pb.RegisterRecordValidationServiceServer(s, &mockRecordServer{})
|
||||
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
t.Logf("Server exited with error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return s, lis
|
||||
}
|
||||
|
||||
// createTestClient 创建用于测试的客户端.
|
||||
//
|
||||
//nolint:unparam // 集成测试暂时跳过,返回值始终为 nil
|
||||
func createTestClient(t *testing.T, _ *bufconn.Listener) *queryclient.Client {
|
||||
// 使用 bufconn 的特殊方式创建客户端
|
||||
// 由于我们不能直接注入连接,需要通过地址的方式
|
||||
// 这里我们使用一个变通的方法:直接构建客户端结构(不推荐生产使用)
|
||||
// 更好的方法是提供一个可注入连接的构造函数
|
||||
|
||||
// 暂时使用真实的地址测试配置验证
|
||||
client, err := queryclient.NewClient(queryclient.ClientConfig{
|
||||
ServerAddr: "bufnet",
|
||||
}, testLogger)
|
||||
|
||||
// 对于这个测试,我们关闭它并使用 mock 方式
|
||||
if client != nil {
|
||||
_ = client.Close()
|
||||
}
|
||||
|
||||
// 检查 err 避免未使用的警告
|
||||
_ = err
|
||||
|
||||
// 返回 nil,让调用者知道需要用其他方式测试
|
||||
t.Skip("Skipping integration test - requires real gRPC server setup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config queryclient.ClientConfig
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "使用ServerAddr成功创建客户端",
|
||||
config: queryclient.ClientConfig{
|
||||
ServerAddr: "localhost:9090",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "使用ServerAddrs成功创建客户端",
|
||||
config: queryclient.ClientConfig{
|
||||
ServerAddrs: []string{"localhost:9090", "localhost:9091"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "没有提供地址应该失败",
|
||||
config: queryclient.ClientConfig{},
|
||||
wantErr: true,
|
||||
errMsg: "at least one server address is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := queryclient.NewClient(tt.config, testLogger)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
assert.Nil(t, client)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
// 清理
|
||||
if client != nil {
|
||||
_ = client.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConfig_GetAddrs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config queryclient.ClientConfig
|
||||
wantAddrs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ServerAddrs优先",
|
||||
config: queryclient.ClientConfig{
|
||||
ServerAddrs: []string{"addr1:9090", "addr2:9090"},
|
||||
ServerAddr: "addr3:9090",
|
||||
},
|
||||
wantAddrs: []string{"addr1:9090", "addr2:9090"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "使用ServerAddr作为后备",
|
||||
config: queryclient.ClientConfig{
|
||||
ServerAddr: "addr1:9090",
|
||||
},
|
||||
wantAddrs: []string{"addr1:9090"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "没有地址应该返回错误",
|
||||
config: queryclient.ClientConfig{},
|
||||
wantAddrs: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
addrs, err := tt.config.GetAddrs()
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantAddrs, addrs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListOperationsRequest(t *testing.T) {
|
||||
// 测试请求结构的创建
|
||||
now := time.Now()
|
||||
req := queryclient.ListOperationsRequest{
|
||||
PageSize: 10,
|
||||
PreTime: now,
|
||||
Timestamp: &now,
|
||||
OpSource: model.Source("test"),
|
||||
OpType: model.Type("create"),
|
||||
}
|
||||
|
||||
assert.Equal(t, uint64(10), req.PageSize)
|
||||
assert.Equal(t, now, req.PreTime)
|
||||
assert.NotNil(t, req.Timestamp)
|
||||
assert.Equal(t, "test", string(req.OpSource))
|
||||
assert.Equal(t, "create", string(req.OpType))
|
||||
}
|
||||
|
||||
func TestValidationRequest(t *testing.T) {
|
||||
// 测试验证请求结构
|
||||
now := time.Now()
|
||||
req := queryclient.ValidationRequest{
|
||||
Time: now,
|
||||
OpID: "op-123",
|
||||
OpType: "create",
|
||||
DoRepository: "repo",
|
||||
}
|
||||
|
||||
assert.Equal(t, now, req.Time)
|
||||
assert.Equal(t, "op-123", req.OpID)
|
||||
assert.Equal(t, "create", req.OpType)
|
||||
assert.Equal(t, "repo", req.DoRepository)
|
||||
}
|
||||
|
||||
func TestListRecordsRequest(t *testing.T) {
|
||||
// 测试记录列表请求结构
|
||||
now := time.Now()
|
||||
|
||||
req := queryclient.ListRecordsRequest{
|
||||
PageSize: 20,
|
||||
PreTime: now,
|
||||
DoPrefix: "test",
|
||||
RCType: "log",
|
||||
}
|
||||
|
||||
assert.Equal(t, uint64(20), req.PageSize)
|
||||
assert.Equal(t, now, req.PreTime)
|
||||
assert.Equal(t, "test", req.DoPrefix)
|
||||
assert.Equal(t, "log", req.RCType)
|
||||
}
|
||||
|
||||
func TestRecordValidationRequest(t *testing.T) {
|
||||
// 测试记录验证请求结构
|
||||
now := time.Now()
|
||||
req := queryclient.RecordValidationRequest{
|
||||
Timestamp: now,
|
||||
RecordID: "rec-123",
|
||||
DoPrefix: "test",
|
||||
RCType: "log",
|
||||
}
|
||||
|
||||
assert.Equal(t, now, req.Timestamp)
|
||||
assert.Equal(t, "rec-123", req.RecordID)
|
||||
assert.Equal(t, "test", req.DoPrefix)
|
||||
assert.Equal(t, "log", req.RCType)
|
||||
}
|
||||
|
||||
// 集成测试部分(需要真实的 gRPC server).
|
||||
func TestIntegration_ListOperations(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ListOperations(ctx, queryclient.ListOperationsRequest{
|
||||
PageSize: 10,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, int64(2), resp.Count)
|
||||
assert.Len(t, resp.Data, 2)
|
||||
assert.Equal(t, "op-1", resp.Data[0].OpID)
|
||||
}
|
||||
|
||||
func TestIntegration_ValidateOperation(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resultChan, err := client.ValidateOperation(ctx, queryclient.ValidationRequest{
|
||||
Time: time.Now(),
|
||||
OpID: "op-test",
|
||||
OpType: "create",
|
||||
DoRepository: "repo",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resultChan)
|
||||
|
||||
results := []int32{}
|
||||
for result := range resultChan {
|
||||
results = append(results, result.Code)
|
||||
if result.IsCompleted() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Contains(t, results, int32(100)) // Processing
|
||||
assert.Contains(t, results, int32(200)) // Completed
|
||||
}
|
||||
|
||||
func TestIntegration_ValidateOperationSync(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
progressCount := 0
|
||||
result, err := client.ValidateOperationSync(
|
||||
ctx,
|
||||
queryclient.ValidationRequest{
|
||||
Time: time.Now(),
|
||||
OpID: "op-test",
|
||||
OpType: "create",
|
||||
DoRepository: "repo",
|
||||
},
|
||||
func(r *model.ValidationResult) {
|
||||
progressCount++
|
||||
assert.Equal(t, int32(100), r.Code)
|
||||
},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, int32(200), result.Code)
|
||||
assert.True(t, result.IsCompleted())
|
||||
assert.Positive(t, progressCount)
|
||||
}
|
||||
|
||||
func TestIntegration_ListRecords(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ListRecords(ctx, queryclient.ListRecordsRequest{
|
||||
PageSize: 10,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, int64(2), resp.Count)
|
||||
assert.Len(t, resp.Data, 2)
|
||||
assert.Equal(t, "rec-1", resp.Data[0].ID)
|
||||
}
|
||||
|
||||
func TestIntegration_ValidateRecord(t *testing.T) { //nolint:dupl // 测试代码中的重复模式是合理的
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resultChan, err := client.ValidateRecord(ctx, queryclient.RecordValidationRequest{
|
||||
Timestamp: time.Now(),
|
||||
RecordID: "rec-test",
|
||||
DoPrefix: "test",
|
||||
RCType: "log",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resultChan)
|
||||
|
||||
results := []int32{}
|
||||
for result := range resultChan {
|
||||
results = append(results, result.Code)
|
||||
if result.IsCompleted() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Contains(t, results, int32(100)) // Processing
|
||||
assert.Contains(t, results, int32(200)) // Completed
|
||||
}
|
||||
|
||||
func TestIntegration_ValidateRecordSync(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
progressCount := 0
|
||||
result, err := client.ValidateRecordSync(
|
||||
ctx,
|
||||
queryclient.RecordValidationRequest{
|
||||
Timestamp: time.Now(),
|
||||
RecordID: "rec-test",
|
||||
DoPrefix: "test",
|
||||
RCType: "log",
|
||||
},
|
||||
func(r *model.RecordValidationResult) {
|
||||
progressCount++
|
||||
assert.Equal(t, int32(100), r.Code)
|
||||
},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, int32(200), result.Code)
|
||||
assert.True(t, result.IsCompleted())
|
||||
assert.Positive(t, progressCount)
|
||||
}
|
||||
|
||||
func TestClient_GetLowLevelClients(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
opClient := client.GetLowLevelOperationClient()
|
||||
assert.NotNil(t, opClient)
|
||||
|
||||
recClient := client.GetLowLevelRecordClient()
|
||||
assert.NotNil(t, recClient)
|
||||
}
|
||||
|
||||
func TestClient_Close(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
server, lis := setupTestServer(t)
|
||||
defer server.Stop()
|
||||
|
||||
client := createTestClient(t, lis)
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := client.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 再次关闭应该不会报错
|
||||
err = client.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user