211 lines
8.9 KiB
Go
211 lines
8.9 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
"git.ipao.vip/rogee/go-sip/internal/agent"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestApprovedAgentServerRegistersCallsBeforeExecuteAckAndDrainsOnControl(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
req := approvedTestRequest(t, now)
|
|
process, stopProcess := context.WithCancel(context.Background())
|
|
defer stopProcess()
|
|
calls := &agent.TaskCalls{}
|
|
started := make(chan context.Context, 1)
|
|
hangupFinished := make(chan struct{})
|
|
defer func() {
|
|
select {
|
|
case <-hangupFinished:
|
|
default:
|
|
close(hangupFinished)
|
|
}
|
|
}()
|
|
failures := make(chan error, 1)
|
|
worker := &ApprovedCallWorker{Lifecycle: process, Calls: calls, Now: func() time.Time { return now },
|
|
Prepare: prepareWorkerRun(func(ctx context.Context, _ ApprovedExecution) error {
|
|
started <- ctx
|
|
<-ctx.Done()
|
|
<-hangupFinished
|
|
return nil
|
|
}),
|
|
OnFailure: func(_ ApprovedExecution, err error) error { failures <- err; return nil },
|
|
}
|
|
server, err := NewApprovedAgentServer(ServerOptions{
|
|
Mode: "mock", StatePath: filepath.Join(t.TempDir(), "agent-session.json"),
|
|
ApprovedDispatcherID: req.DispatcherId, Now: func() time.Time { return now },
|
|
Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) {
|
|
return map[string]int64{req.SelectedTrunkId: req.SipRevision}, nil
|
|
},
|
|
}, worker)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := server.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{
|
|
Meta: testMeta("activate-approved", "", 0),
|
|
Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-1", ExpectedBootId: "boot-1",
|
|
DispatcherEpoch: "epoch-1", SessionGeneration: 1, DispatcherId: req.DispatcherId},
|
|
ActivationOperationId: "activate-approved",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request, closeRequest := context.WithCancel(context.Background())
|
|
response, err := server.ExecuteApproved(request, req)
|
|
if err != nil || response == nil || !response.Accepted {
|
|
t.Fatalf("registered call did not receive dispatch ACK: response=%v err=%v", response, err)
|
|
}
|
|
closeRequest() // the outbound gRPC request lifetime has ended
|
|
var callContext context.Context
|
|
select {
|
|
case callContext = <-started:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("approved worker did not begin after ACK")
|
|
}
|
|
if callContext.Err() != nil {
|
|
t.Fatal("active call inherited the completed unary context")
|
|
}
|
|
control := &agentpb.ApplyApprovedTaskControlRequest{
|
|
Meta: testMeta("control", "", 1), DispatcherId: req.DispatcherId,
|
|
TenantId: req.TenantId, TaskId: req.TaskId,
|
|
Action: agentpb.ControlAction_CONTROL_ACTION_STOP, ActiveCallPolicy: agentpb.ActiveCallPolicy_ACTIVE_CALL_POLICY_HANGUP,
|
|
}
|
|
controlResult := make(chan error, 1)
|
|
go func() { _, err := server.ApplyApprovedTaskControl(context.Background(), control); controlResult <- err }()
|
|
select {
|
|
case <-callContext.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("stop did not reach the registered call")
|
|
}
|
|
select {
|
|
case err := <-controlResult:
|
|
t.Fatalf("stop was acknowledged before media hangup: %v", err)
|
|
default:
|
|
}
|
|
close(hangupFinished)
|
|
if err := <-controlResult; err != nil {
|
|
t.Fatalf("stop did not wait for the actual runner: %v", err)
|
|
}
|
|
if _, err := server.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("completed or unknown durable identity was redialed: %v", err)
|
|
}
|
|
select {
|
|
case err := <-failures:
|
|
t.Fatalf("successful controlled hangup fabricated an execution failure: %v", err)
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestApprovedAgentServerRefusesMissingOrCompetingAdapters(t *testing.T) {
|
|
process := context.Background()
|
|
calls := &agent.TaskCalls{}
|
|
worker := &ApprovedCallWorker{Lifecycle: process, Calls: calls,
|
|
Prepare: prepareWorkerRun(func(context.Context, ApprovedExecution) error { return nil }),
|
|
OnFailure: func(ApprovedExecution, error) error { return nil },
|
|
}
|
|
valid := ServerOptions{Mode: "mock", StatePath: filepath.Join(t.TempDir(), "agent-session.json"),
|
|
ApprovedDispatcherID: "dispatcher-1", Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) { return map[string]int64{"trunk-mock": 8}, nil },
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
edit func(*ServerOptions, *ApprovedCallWorker)
|
|
}{
|
|
{"missing state", func(o *ServerOptions, _ *ApprovedCallWorker) { o.StatePath = "" }},
|
|
{"non-Mock mode", func(o *ServerOptions, _ *ApprovedCallWorker) { o.Mode = "real" }},
|
|
{"implicit Mock default", func(o *ServerOptions, _ *ApprovedCallWorker) { o.Mode = "" }},
|
|
{"missing SIP observation", func(o *ServerOptions, _ *ApprovedCallWorker) { o.LoadedSIP = nil }},
|
|
{"missing Agent identity", func(o *ServerOptions, _ *ApprovedCallWorker) { o.Status = nil }},
|
|
{"missing authorized Dispatcher", func(o *ServerOptions, _ *ApprovedCallWorker) { o.ApprovedDispatcherID = "" }},
|
|
{"competing originator", func(o *ServerOptions, _ *ApprovedCallWorker) {
|
|
o.MockApprovedOriginate = func(context.Context, ApprovedExecution) error { return nil }
|
|
}},
|
|
{"competing control registry", func(o *ServerOptions, _ *ApprovedCallWorker) { o.ApprovedTaskCalls = &agent.TaskCalls{} }},
|
|
{"missing worker process", func(_ *ServerOptions, w *ApprovedCallWorker) { w.Lifecycle = nil }},
|
|
{"missing failure handler", func(_ *ServerOptions, w *ApprovedCallWorker) { w.OnFailure = nil }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
options, isolated := valid, *worker
|
|
tc.edit(&options, &isolated)
|
|
if server, err := NewApprovedAgentServer(options, &isolated); err == nil || server != nil {
|
|
t.Fatalf("unsafe approved server setup was admitted: %v", err)
|
|
}
|
|
})
|
|
}
|
|
if server, err := NewApprovedAgentServer(valid, nil); err == nil || server != nil || !errors.Is(err, ErrApprovedWorkerRequired) {
|
|
t.Fatalf("missing approved call worker was admitted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApprovedAgentServerRejectsSelfReportedDispatcherBeforeSessionWrite(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
req := approvedTestRequest(t, now)
|
|
state := filepath.Join(t.TempDir(), "agent-session.json")
|
|
worker := &ApprovedCallWorker{Lifecycle: context.Background(), Calls: &agent.TaskCalls{},
|
|
Prepare: prepareWorkerRun(func(context.Context, ApprovedExecution) error { return nil }),
|
|
OnFailure: func(ApprovedExecution, error) error { return nil },
|
|
}
|
|
server, err := NewApprovedAgentServer(ServerOptions{
|
|
Mode: "mock", StatePath: state, ApprovedDispatcherID: req.DispatcherId,
|
|
Now: func() time.Time { return now },
|
|
Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) {
|
|
return map[string]int64{req.SelectedTrunkId: req.SipRevision}, nil
|
|
},
|
|
}, worker)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
activation := &agentpb.ActivateAgentRequest{
|
|
Meta: testMeta("activate-unapproved-d", "", 0),
|
|
Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-1", ExpectedBootId: "boot-1",
|
|
DispatcherEpoch: "epoch-1", DispatcherId: req.DispatcherId + "-other"},
|
|
ActivationOperationId: "activate-unapproved-d",
|
|
}
|
|
if _, err := server.ActivateAgent(context.Background(), activation); status.Code(err) != codes.PermissionDenied {
|
|
t.Fatalf("pinned peer could claim a different Dispatcher identity: %v", err)
|
|
}
|
|
if _, err := os.Stat(state); !os.IsNotExist(err) {
|
|
t.Fatalf("rejected identity persisted a session: %v", err)
|
|
}
|
|
activation.Binding.DispatcherId = req.DispatcherId
|
|
if _, err := server.ActivateAgent(context.Background(), activation); err != nil {
|
|
t.Fatalf("approved Dispatcher could not activate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteApprovedReturnsDefinitiveNoDialRefusalWithoutRetry(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
for _, tc := range []struct {
|
|
name string
|
|
cause error
|
|
want codes.Code
|
|
}{
|
|
{"paused task", agent.ErrTaskAdmissionClosed, codes.FailedPrecondition},
|
|
{"stopped task", agent.ErrTaskStopped, codes.FailedPrecondition},
|
|
{"expired before dispatch", ErrApprovedDialExpired, codes.DeadlineExceeded},
|
|
{"invalid per-call preparation", ErrApprovedCallPreparation, codes.FailedPrecondition},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := approvedTestRequest(t, now)
|
|
attempts := 0
|
|
server := activatedApprovedServer(t, now, filepath.Join(t.TempDir(), "agent-session.json"), req.DispatcherId, 1,
|
|
func(context.Context, ApprovedExecution) error { attempts++; return tc.cause })
|
|
if _, err := server.ExecuteApproved(context.Background(), req); status.Code(err) != tc.want {
|
|
t.Fatalf("known no-dial was treated as ambiguous: %v", err)
|
|
}
|
|
if _, err := server.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 1 {
|
|
t.Fatalf("durable no-redial guard was bypassed: attempts=%d err=%v", attempts, err)
|
|
}
|
|
})
|
|
}
|
|
}
|