163 lines
6.6 KiB
Go
163 lines
6.6 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/configread"
|
|
)
|
|
|
|
func preparedCurrentCallStore(t *testing.T) *CurrentStore {
|
|
t.Helper()
|
|
s, err := OpenCurrent(filepath.Join(t.TempDir(), "state.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
task := currentStoreSnapshot(t)
|
|
task.SIP.Trunks = []byte(strings.Replace(string(task.SIP.Trunks), `"max_concurrent_calls":null`, `"max_concurrent_calls":2`, 1))
|
|
if err := s.ApplyDiscoverySnapshot(currentDispatcherID, []configread.CurrentDiscoveredTask{{TaskID: task.Task.TaskID, TenantID: task.Task.TenantID, TaskRevision: task.Task.TaskRevision, Status: "running"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SaveSnapshot(task); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MarkReadyForSIP(currentDispatcherID, 8); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func currentCall(id string) CurrentExecuteCommand {
|
|
return CurrentExecuteCommand{DispatcherID: currentDispatcherID, EventID: id, TenantID: 1001, TaskID: "task-asr", Callee: "15003164745", IssuedAt: "2026-09-21T01:30:00Z"}
|
|
}
|
|
func currentReservation() CurrentCallReservation {
|
|
return CurrentCallReservation{TrunkID: "trunk-mock", SIPRevision: 8, CallerID: "BD00000000", DialedCallee: "15003164745", Deadline: time.Date(2026, 9, 21, 1, 32, 0, 0, time.UTC)}
|
|
}
|
|
|
|
func TestCurrentExecuteInboxPreventsDuplicateOriginationAfterRestart(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentCall("call-1")
|
|
first, created, err := s.RecordExecute(cmd)
|
|
if err != nil || !created || first.Status != "pending" {
|
|
t.Fatalf("first durable receipt: %+v created=%v err=%v", first, created, err)
|
|
}
|
|
second, created, err := s.RecordExecute(cmd)
|
|
if err != nil || created || second.Status != "pending" {
|
|
t.Fatalf("redelivery not deduplicated: %+v created=%v err=%v", second, created, err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, created, err := s.RecordExecute(cmd); err != nil || created {
|
|
t.Fatalf("reserved call was reaccepted: created=%v err=%v", created, err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)); err == nil {
|
|
t.Fatal("duplicate receipt reserved a second originate")
|
|
}
|
|
pending, err := s.ListPendingExecute(currentDispatcherID)
|
|
if err != nil || len(pending) != 0 {
|
|
t.Fatalf("unknown/reserved call retried: %+v %v", pending, err)
|
|
}
|
|
if err := s.MarkExecuteUnknown(cmd.DispatcherID, cmd.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, created, err := s.RecordExecute(cmd); err != nil || created {
|
|
t.Fatalf("unknown call reaccepted: created=%v err=%v", created, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExecuteRejectsOnlyOnceWithoutFinalResult(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentCall("call-invalid")
|
|
if _, _, err := s.RecordExecute(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.RejectExecute(cmd.DispatcherID, cmd.EventID, "callee not permitted"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outbox, err := s.ListPendingOutbox(currentDispatcherID)
|
|
if err != nil || len(outbox) != 1 || outbox[0].EventType != "call.execute" || !strings.Contains(string(outbox[0].Body), `"status":"rejected"`) {
|
|
t.Fatalf("rejection outbox: %+v %v", outbox, err)
|
|
}
|
|
if _, created, err := s.RecordExecute(cmd); err != nil || created {
|
|
t.Fatalf("rejected command was accepted twice: %v %v", created, err)
|
|
}
|
|
if err := s.RejectExecute(cmd.DispatcherID, cmd.EventID, "callee not permitted"); err == nil {
|
|
t.Fatal("duplicate rejection created another outbox event")
|
|
}
|
|
if err := s.MarkOutboxConfirmed(currentDispatcherID, outbox[0].EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pending, err := s.ListPendingOutbox(currentDispatcherID); err != nil || len(pending) != 0 {
|
|
t.Fatalf("confirmed outbox was republished: %+v %v", pending, err)
|
|
}
|
|
var total int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM dispatcher_outbox WHERE dispatcher_id=?`, currentDispatcherID).Scan(&total); err != nil || total != 1 {
|
|
t.Fatalf("confirmed outbox vanished before SaaS receipt: %d %v", total, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExecuteQuotaCountsUnknownAndReleasesOnlyConfirmedEnd(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
at := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
for _, id := range []string{"call-1", "call-2"} {
|
|
cmd := currentCall(id)
|
|
if _, _, err := s.RecordExecute(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), at); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
third := currentCall("call-3")
|
|
if _, _, err := s.RecordExecute(third); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(third.DispatcherID, third.EventID, currentReservation(), at); !errors.Is(err, ErrCurrentCapacity) {
|
|
t.Fatalf("third call exceeded task/trunk capacity: %v", err)
|
|
}
|
|
if err := s.MarkExecuteUnknown(currentDispatcherID, "call-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(third.DispatcherID, third.EventID, currentReservation(), at); !errors.Is(err, ErrCurrentCapacity) {
|
|
t.Fatalf("unknown call released occupancy: %v", err)
|
|
}
|
|
if err := s.MarkExecuteDispatched(currentDispatcherID, "call-2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.FinishExecute(currentDispatcherID, "call-2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(third.DispatcherID, third.EventID, currentReservation(), at); err != nil {
|
|
t.Fatalf("finished call failed to release capacity: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentExecuteOutboxFailureDoesNotMarkOriginatedCallDelivered(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
cmd := currentCall("call-fault")
|
|
if _, _, err := s.RecordExecute(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.db.Exec(`CREATE TRIGGER fail_outbox BEFORE INSERT ON dispatcher_outbox BEGIN SELECT RAISE(ABORT,'injected outbox failure'); END`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MarkExecuteDispatched(cmd.DispatcherID, cmd.EventID); err == nil {
|
|
t.Fatal("marked dispatch despite outbox failure")
|
|
}
|
|
var status string
|
|
if err := s.db.QueryRow(`SELECT status FROM dispatcher_inbox WHERE dispatcher_id=? AND event_id=?`, cmd.DispatcherID, cmd.EventID).Scan(&status); err != nil || status != "dispatching" {
|
|
t.Fatalf("lost pre-originate fence after outbox failure: %q %v", status, err)
|
|
}
|
|
if _, created, err := s.RecordExecute(cmd); err != nil || created {
|
|
t.Fatalf("originated command redialed after outbox failure: %v %v", created, err)
|
|
}
|
|
}
|