Files
go-sip/internal/dispatcher/current_execute_test.go
T

177 lines
6.9 KiB
Go

package dispatcher
import (
"context"
"encoding/json"
"errors"
"path/filepath"
"strings"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/contract"
"git.ipao.vip/rogee/go-sip/internal/store"
)
type currentFakeOriginator struct {
loaded map[string]int64
calls []CurrentCallSpec
err error
}
func (f *currentFakeOriginator) LoadedTrunks(context.Context) (map[string]int64, error) {
return f.loaded, nil
}
func (f *currentFakeOriginator) Originate(_ context.Context, spec CurrentCallSpec) error {
f.calls = append(f.calls, spec)
return f.err
}
type currentFakePublisher struct {
bodies [][]byte
err error
}
func (f *currentFakePublisher) Publish(_ context.Context, exchange, key string, body []byte) error {
if exchange != "agent-call.saas.v1" || !strings.HasSuffix(key, ".out") {
return errors.New("unexpected outbound MQ route")
}
if err := contract.ValidateCurrent("mq", body); err != nil {
return err
}
f.bodies = append(f.bodies, append([]byte(nil), body...))
return f.err
}
func newCurrentExecuteFixture(t *testing.T) (*CurrentExecuteController, *currentFakeOriginator, *currentFakePublisher, *store.CurrentStore) {
t.Helper()
s, err := store.OpenCurrent(filepath.Join(t.TempDir(), "state.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = s.Close() })
snapshot := currentPolicySnapshot(t)
if err := s.ApplyDiscoverySnapshot(snapshot.Task.DispatcherID, []configread.CurrentDiscoveredTask{{TaskID: snapshot.Task.TaskID, TenantID: snapshot.Task.TenantID, TaskRevision: snapshot.Task.TaskRevision, Status: "running"}}); err != nil {
t.Fatal(err)
}
if err := s.SaveSnapshot(snapshot); err != nil {
t.Fatal(err)
}
if err := s.MarkReadyForSIP(snapshot.Task.DispatcherID, snapshot.SIP.Revision); err != nil {
t.Fatal(err)
}
originator := &currentFakeOriginator{loaded: map[string]int64{"trunk-mock": 8}}
publisher := &currentFakePublisher{}
controller := &CurrentExecuteController{DispatcherID: snapshot.Task.DispatcherID, Store: s, Originator: originator, Publisher: publisher, Now: func() time.Time { return currentMonday(9, 30) }}
return controller, originator, publisher, s
}
func currentExecuteBody(t *testing.T, eventID, callee string) []byte {
t.Helper()
body := string(currentConfigExample(t, "mq-execute"))
body = strings.Replace(body, `"event_id":"call-example"`, `"event_id":"`+eventID+`"`, 1)
body = strings.Replace(body, `"callee":"15003164745"`, `"callee":"`+callee+`"`, 1)
return []byte(body)
}
func TestCurrentExecuteRejectsInvalidCalleeWithoutStoppingTask(t *testing.T) {
controller, originator, publisher, s := newCurrentExecuteFixture(t)
if err := controller.ProcessExecute(context.Background(), currentExecuteBody(t, "bad-1", "not-a-phone")); err != nil {
t.Fatal(err)
}
if len(originator.calls) != 0 {
t.Fatal("invalid number reached originator")
}
outbox, err := s.ListPendingOutbox(controller.DispatcherID)
if err != nil || len(outbox) != 1 || !strings.Contains(string(outbox[0].Body), `"status":"rejected"`) {
t.Fatalf("missing individual rejection: %+v %v", outbox, err)
}
if err := controller.ProcessExecute(context.Background(), currentExecuteBody(t, "good-1", "15003164745")); err != nil {
t.Fatal(err)
}
if len(originator.calls) != 1 {
t.Fatalf("invalid number paused entire task; originate calls=%d", len(originator.calls))
}
if err := controller.ProcessExecute(context.Background(), currentExecuteBody(t, "good-1", "15003164745")); err != nil || len(originator.calls) != 1 {
t.Fatalf("duplicate command reoriginated: %d %v", len(originator.calls), err)
}
if err := controller.FlushOutbox(context.Background()); err != nil {
t.Fatal(err)
}
if len(publisher.bodies) != 2 {
t.Fatalf("expected rejected and dispatched acknowledgments, got %d", len(publisher.bodies))
}
for _, body := range publisher.bodies {
var message struct {
EventType string `json:"event_type"`
Payload struct {
Status string `json:"status"`
} `json:"payload"`
}
if err := json.Unmarshal(body, &message); err != nil || message.EventType != "call.execute" || (message.Payload.Status != "rejected" && message.Payload.Status != "dispatched") {
t.Fatalf("unexpected outbound message: %s %v", body, err)
}
}
}
func TestCurrentExecuteWaitsForRulesThenDispatchesOriginalIdentity(t *testing.T) {
controller, originator, _, s := newCurrentExecuteFixture(t)
controller.Now = func() time.Time { return currentMonday(8, 59) }
body := currentExecuteBody(t, "waiting-1", "15003164745")
body = []byte(strings.Replace(string(body), `"issued_at":"2026-09-21T01:00:00Z"`, `"issued_at":"2026-09-20T00:00:00Z"`, 1))
if err := controller.ProcessExecute(context.Background(), body); err != nil {
t.Fatal(err)
}
if len(originator.calls) != 0 {
t.Fatal("dialed before task/trunk window")
}
pending, err := s.ListPendingExecute(controller.DispatcherID)
if err != nil || len(pending) != 1 || pending[0].EventID != "waiting-1" {
t.Fatalf("pending command was lost: %+v %v", pending, err)
}
controller.Now = func() time.Time { return currentMonday(9, 30) }
if err := controller.ProcessPending(context.Background()); err != nil || len(originator.calls) != 1 || originator.calls[0].EventID != "waiting-1" {
t.Fatalf("original command did not resume once: calls=%+v err=%v", originator.calls, err)
}
}
func TestCurrentExecuteTimeoutAndMQFailureDoNotRedial(t *testing.T) {
controller, originator, publisher, s := newCurrentExecuteFixture(t)
originator.err = context.DeadlineExceeded
body := currentExecuteBody(t, "unknown-1", "15003164745")
if err := controller.ProcessExecute(context.Background(), body); err == nil {
t.Fatal("originator timeout hidden")
}
if err := controller.ProcessExecute(context.Background(), body); err != nil || len(originator.calls) != 1 {
t.Fatalf("unknown call reoriginated: %d %v", len(originator.calls), err)
}
if err := controller.ProcessPending(context.Background()); err != nil || len(originator.calls) != 1 {
t.Fatalf("unknown call appeared pending: %d %v", len(originator.calls), err)
}
originator.err = nil
second := currentExecuteBody(t, "mq-loss-1", "15003164745")
if err := controller.ProcessExecute(context.Background(), second); err != nil {
t.Fatal(err)
}
publisher.err = errors.New("injected publisher confirm loss")
if err := controller.FlushOutbox(context.Background()); err == nil {
t.Fatal("MQ failure was hidden")
}
pending, err := s.ListPendingOutbox(controller.DispatcherID)
if err != nil || len(pending) != 1 {
t.Fatalf("outbox lost original ack: %+v %v", pending, err)
}
if err := controller.ProcessExecute(context.Background(), second); err != nil || len(originator.calls) != 2 {
t.Fatalf("MQ redelivery reoriginated: %d %v", len(originator.calls), err)
}
publisher.err = nil
if err := controller.FlushOutbox(context.Background()); err != nil {
t.Fatal(err)
}
pending, err = s.ListPendingOutbox(controller.DispatcherID)
if err != nil || len(pending) != 0 {
t.Fatalf("confirmed event not retained correctly: %+v %v", pending, err)
}
}