153 lines
6.6 KiB
Go
153 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"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/asterisk"
|
|
"git.ipao.vip/rogee/go-sip/internal/rpc"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
)
|
|
|
|
type definiteEndClient struct {
|
|
agentpb.AgentControlServiceClient
|
|
calls int
|
|
fail error
|
|
}
|
|
|
|
func (c *definiteEndClient) ReportCallEnded(_ context.Context, req *agentpb.ReportCallEndedRequest, _ ...grpc.CallOption) (*agentpb.ReportCallEndedResponse, error) {
|
|
c.calls++
|
|
if c.fail != nil {
|
|
return nil, c.fail
|
|
}
|
|
return &agentpb.ReportCallEndedResponse{Receipt: &agentpb.OperationReceipt{Result: agentpb.ResultCode_RESULT_CODE_APPLIED, FactId: req.SourceEventId}}, nil
|
|
}
|
|
|
|
func TestOnlyDefinitelyUnissuedRealCallReportsConfirmedEnd(t *testing.T) {
|
|
backend := &definiteEndClient{}
|
|
client := agent.RecordingClient{Client: backend, DispatcherID: "dispatcher-test", TenantID: 1, SourceEventID: "call-pre-dial", Session: func(context.Context) (*agentpb.RequestMeta, error) {
|
|
return &agentpb.RequestMeta{AgentId: "agent-test", CellId: "cell-test", BootId: "boot-test", DispatcherEpoch: "epoch-test", SessionGeneration: 1}, nil
|
|
}}
|
|
before := &asterisk.NativeCallFailure{Phase: "rtp_listen", Cause: errors.New("no RTP socket")}
|
|
if err := reportDefiniteNonDialFailure(context.Background(), before, client); err != nil || backend.calls != 1 {
|
|
t.Fatalf("definite pre-dial failure was not reported: calls=%d err=%v", backend.calls, err)
|
|
}
|
|
unknown := &asterisk.NativeCallFailure{Phase: "originate", Cause: errors.New("response lost")}
|
|
if err := reportDefiniteNonDialFailure(context.Background(), unknown, client); err != nil || backend.calls != 1 {
|
|
t.Fatalf("unknown ARI outcome must stay occupied: calls=%d err=%v", backend.calls, err)
|
|
}
|
|
backend.fail = errors.New("no authenticated end receipt")
|
|
if err := reportDefiniteNonDialFailure(context.Background(), before, client); err == nil {
|
|
t.Fatal("failed end reporting must not pretend capacity was released")
|
|
}
|
|
}
|
|
|
|
func TestRealAgentCommandStartsPinnedServerWithoutMockFixturesOrDial(t *testing.T) {
|
|
ca, agentCert, agentKey, dispatcherCert, dispatcherKey, dispatcherLeaf := localCommandCertificates(t)
|
|
settings, _ := currentAgentSetupFixture(t)
|
|
for path, data := range map[string][]byte{settings.CAFile: ca, settings.CertFile: agentCert, settings.KeyFile: agentKey} {
|
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listen := listener.Addr().String()
|
|
listener.Close()
|
|
for name, value := range map[string]string{
|
|
"AGENT_ID": settings.AgentID, "CELL_ID": settings.CellID,
|
|
"AGENT_GRPC_LISTEN": listen, "AGENT_SESSION_PATH": settings.SessionPath,
|
|
"AGENT_RECOVERY_ROOT": settings.RecoveryRoot, "DISPATCHER_ID": settings.DispatcherID,
|
|
"DISPATCHER_GRPC_ENDPOINT": settings.DispatcherEndpoint, "DISPATCHER_GRPC_SERVER_NAME": settings.DispatcherServerName,
|
|
"MTLS_CA_FILE": settings.CAFile, "MTLS_CERT_FILE": settings.CertFile, "MTLS_KEY_FILE": settings.KeyFile,
|
|
"MTLS_PEER_CERT_FINGERPRINTS": rpc.CertificateFingerprint(dispatcherLeaf),
|
|
"ASTERISK_CONFIG_DIR": filepath.Join(settings.RecoveryRoot, "asterisk"),
|
|
"ASTERISK_BIN": "/usr/sbin/asterisk", "ASTERISK_LIBRARY_DIR": "/usr/lib/asterisk/modules",
|
|
"AGENT_EVIDENCE_ROOT": filepath.Join(settings.RecoveryRoot, "evidence"),
|
|
"AGENT_OSS_ALLOWED_HOST": "bucket.oss-cn-beijing.aliyuncs.com",
|
|
"AGENT_MOCK_SCENARIO_FILE": "", "AGENT_MOCK_APPLIED_SIP_FILE": "",
|
|
} {
|
|
t.Setenv(name, value)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cmd := newAgentCommand()
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
cmd.SetContext(ctx)
|
|
cmd.SetArgs([]string{"--mode", "nonprod-real"})
|
|
finished := make(chan error, 1)
|
|
go func() { finished <- cmd.Execute() }()
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
select {
|
|
case err := <-finished:
|
|
if err != nil {
|
|
t.Errorf("real Agent shutdown failed: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("real Agent did not stop after cancellation")
|
|
}
|
|
})
|
|
tlsConfig, err := rpc.NewClientTLSConfig(ca, dispatcherCert, dispatcherKey, "agent.local")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dialCtx, stop := context.WithTimeout(context.Background(), 6*time.Second)
|
|
defer stop()
|
|
conn, err := grpc.DialContext(dialCtx, listen, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), grpc.WithBlock())
|
|
if err != nil {
|
|
select {
|
|
case startupErr := <-finished:
|
|
t.Fatalf("nonprod-real startup failed: %v (dial: %v)", startupErr, err)
|
|
default:
|
|
t.Fatalf("nonprod-real pinned server unavailable: %v", err)
|
|
}
|
|
}
|
|
defer conn.Close()
|
|
status, err := agentpb.NewAgentControlServiceClient(conn).GetAgentStatus(dialCtx, &agentpb.GetAgentStatusRequest{
|
|
Meta: &agentpb.RequestMeta{ProtocolVersion: "agent.v1", RequestId: "status", TraceId: "status", OperationId: "status", AgentId: settings.AgentID, CellId: settings.CellID},
|
|
Target: &agentpb.AgentBinding{AgentId: settings.AgentID, CellId: settings.CellID},
|
|
})
|
|
if err != nil || status.GetStatus().GetAgentId() != settings.AgentID {
|
|
t.Fatalf("nonprod-real Agent did not expose its pinned status: status=%v err=%v", status, err)
|
|
}
|
|
}
|
|
|
|
func TestRealAgentServerRequiresBoundNativeSIPAndNoMock(t *testing.T) {
|
|
settings, _ := currentAgentSetupFixture(t)
|
|
settings.Mode = "nonprod-real"
|
|
settings.AsteriskConfigDir = filepath.Join(settings.RecoveryRoot, "asterisk")
|
|
settings.AsteriskBin = "/usr/sbin/asterisk"
|
|
settings.AsteriskLibraryDir = "/usr/lib/asterisk/modules"
|
|
settings.EvidenceRoot = filepath.Join(settings.RecoveryRoot, "evidence")
|
|
settings.OSSAllowedHost = "bucket.oss-cn-beijing.aliyuncs.com"
|
|
client := &isolatedAgentRecordingClient{}
|
|
server, err := newRealAgentServer(context.Background(), settings, client)
|
|
if err != nil || server == nil {
|
|
t.Fatalf("real Agent must use the native SIP and non-Mock delivery boundary: %v", err)
|
|
}
|
|
if _, err := os.Lstat(settings.SessionPath); !os.IsNotExist(err) {
|
|
t.Fatalf("server assembly must not touch the existing session: %v", err)
|
|
}
|
|
var typedNil *isolatedAgentRecordingClient
|
|
var nilClient agentpb.AgentControlServiceClient = typedNil
|
|
if server, err := newRealAgentServer(context.Background(), settings, nilClient); err == nil || server != nil {
|
|
t.Fatal("typed-nil Dispatcher must block real Agent startup")
|
|
}
|
|
settings.MockScenarioFile = "old-mock.json"
|
|
if server, err := newRealAgentServer(context.Background(), settings, client); err == nil || server != nil {
|
|
t.Fatal("real Agent must not accept isolated Mock fixtures")
|
|
}
|
|
}
|