Files
go-sip/internal/rpc/recording_transport_test.go
T

99 lines
4.3 KiB
Go

package rpc
import (
"context"
"net"
"sync/atomic"
"testing"
"time"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"git.ipao.vip/rogee/go-sip/internal/agent"
"git.ipao.vip/rogee/go-sip/internal/session"
"git.ipao.vip/rogee/go-sip/internal/store"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/test/bufconn"
"google.golang.org/protobuf/proto"
)
func TestRecordingFactsCrossRealMutualTLSUnaryTransportWithoutExternalOSS(t *testing.T) {
server, database, _, request, snapshot := recordingRPCFixture(t)
caPEM, caCert, caKey := testCertificate(t, nil, nil, true, nil, nil)
serverPEM, _, _ := testCertificate(t, caCert, caKey, false, []string{"dispatcher.local"}, nil)
clientPEM, clientCert, _ := testCertificate(t, caCert, caKey, false, []string{"agent.local"}, nil)
serverTLS, err := NewServerTLSConfig(caPEM.certPEM, serverPEM.certPEM, serverPEM.keyPEM)
if err != nil {
t.Fatal(err)
}
clientTLS, err := NewClientTLSConfig(caPEM.certPEM, clientPEM.certPEM, clientPEM.keyPEM, "dispatcher.local")
if err != nil {
t.Fatal(err)
}
server.TrustedFingerprints = map[string]struct{}{CertificateFingerprint(clientCert): {}}
var agentGeneration, activeGeneration atomic.Uint64
var fencedReports atomic.Int32
agentGeneration.Store(request.Meta.SessionGeneration)
activeGeneration.Store(request.Meta.SessionGeneration + 1)
server.AuthorizeSession = func(meta *agentpb.RequestMeta) error {
active := activeGeneration.Load()
if meta.SessionGeneration == active {
return nil
}
if meta.SessionGeneration > 0 && meta.SessionGeneration < active && active-meta.SessionGeneration == 1 {
fencedReports.Add(1)
agentGeneration.Store(active)
return session.ErrGenerationTransition
}
return store.ErrCommandConflict
}
listener := bufconn.Listen(1 << 20)
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLS)))
agentpb.RegisterAgentControlServiceServer(grpcServer, server)
go func() { _ = grpcServer.Serve(listener) }()
t.Cleanup(func() { grpcServer.Stop(); _ = listener.Close() })
conn, err := grpc.NewClient("bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
grpc.WithTransportCredentials(credentials.NewTLS(clientTLS)),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := agent.RecordingClient{
Client: agentpb.NewAgentControlServiceClient(conn), DispatcherID: request.DispatcherId, TenantID: request.TenantId, SourceEventID: request.SourceEventId,
Session: func(context.Context) (*agentpb.RequestMeta, error) {
meta := proto.Clone(request.Meta).(*agentpb.RequestMeta)
meta.SessionGeneration = agentGeneration.Load()
return meta, nil
},
}
grant, err := client.RequestUpload(ctx, request.Asset, request.UploadId)
if err != nil || grant.GetBucket() != "mock-bucket" {
t.Fatalf("mTLS Agent request did not bind Dispatcher-owned bucket: bucket=%q err=%v", grant.GetBucket(), err)
}
activeGeneration.Add(1)
if err := client.ReportEnded(ctx); err != nil {
t.Fatalf("mTLS Agent end did not durably acknowledge original execution: %v", err)
}
observation := &agentpb.UploadObservation{UploadId: request.UploadId, RecordingId: request.Asset.AssetId, PutStatusCode: 200, SizeBytes: request.Asset.SizeBytes, ChecksumSha256: request.Asset.ChecksumSha256}
activeGeneration.Add(1)
resultID, err := client.ReportFinal(ctx, recordingResultPayload(t, snapshot, grant), observation)
if err != nil || resultID == "" {
t.Fatalf("mTLS Agent final result not persisted: fact=%q err=%v", resultID, err)
}
if fencedReports.Load() != 3 || agentGeneration.Load() != activeGeneration.Load() {
t.Fatalf("three mTLS recording facts did not cross the verified Agent session cutovers: fenced=%d current=%d", fencedReports.Load(), agentGeneration.Load())
}
if request.Meta.OperationId != "request-1" || request.Meta.IdempotencyKey != "request-1" {
t.Fatal("concurrent Agent session metadata was mutated by the recording client")
}
outbox, err := database.ListPendingOutbox(request.DispatcherId)
if err != nil || len(outbox) != 2 || outbox[0].EventType != "call.execute" || outbox[1].EventType != "call.execute.result" {
t.Fatalf("gRPC facts did not produce one ACK and one result: outbox=%+v err=%v", outbox, err)
}
}