177 lines
6.9 KiB
Go
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 := ¤tFakeOriginator{loaded: map[string]int64{"trunk-mock": 8}}
|
|
publisher := ¤tFakePublisher{}
|
|
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)
|
|
}
|
|
}
|