191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
package dispatcher
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/contracts"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
"git.ipao.vip/rogee/go-sip/internal/mq"
|
|
"git.ipao.vip/rogee/go-sip/internal/store"
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
)
|
|
|
|
func TestMQCommandQueryPersistsBeforeAcceptance(t *testing.T) {
|
|
s, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
const id = "c046b893-8628-4589-ae50-619d049248a6"
|
|
if err := s.BindDispatcherID(id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, err := New(s, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := contracts.Files.ReadFile("upstream/v1/examples/command-query.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var message map[string]any
|
|
if err := json.Unmarshal(raw, &message); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
message["issued_at"] = time.Now().UTC().Format(time.RFC3339Nano)
|
|
message["not_after"] = time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano)
|
|
raw, err = json.Marshal(message)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route, err := tenant.NewDispatcherRoute(id, "tenant-a")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.AcceptMQMessage(raw, route.InboundKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.AcceptMQMessage(raw, route.InboundKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
if err := s.DB().QueryRow(`SELECT COUNT(*) FROM outbox`).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("expected one persistent response before ACK, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestMQReplayPersistsDecisionBeforeAcknowledgment(t *testing.T) {
|
|
for _, name := range []string{"call-replay", "command-replay"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
s, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
const id = "c046b893-8628-4589-ae50-619d049248a6"
|
|
if err := s.BindDispatcherID(id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, err := New(s, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := contracts.Files.ReadFile("upstream/v1/examples/" + name + ".json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var request map[string]any
|
|
if err := json.Unmarshal(raw, &request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request["issued_at"] = time.Now().UTC().Format(time.RFC3339Nano)
|
|
request["not_after"] = time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano)
|
|
raw, err = json.Marshal(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route, err := tenant.NewDispatcherRoute(id, "tenant-a")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.AcceptMQMessage(raw, route.InboundKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.AcceptMQMessage(raw, route.InboundKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
if err := s.DB().QueryRow(`SELECT COUNT(*) FROM mq_command_receipts`).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("replay decision not durably deduplicated")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQCallQueryPersistsResponse(t *testing.T) {
|
|
s, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
const id = "c046b893-8628-4589-ae50-619d049248a6"
|
|
if err := s.BindDispatcherID(id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, err := New(s, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := contracts.Files.ReadFile("upstream/v1/examples/call-query.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var request map[string]any
|
|
if err := json.Unmarshal(raw, &request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request["issued_at"] = time.Now().UTC().Format(time.RFC3339Nano)
|
|
request["not_after"] = time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano)
|
|
raw, err = json.Marshal(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route, err := tenant.NewDispatcherRoute(id, "tenant-a")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.AcceptMQMessage(raw, route.InboundKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
if err := s.DB().QueryRow(`SELECT COUNT(*) FROM outbox WHERE json_extract(body,'$.message_type')='call.query.result'`).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("call query was acknowledged without a persisted response")
|
|
}
|
|
}
|
|
|
|
func TestInvalidServiceEncodingIsPermanent(t *testing.T) {
|
|
raw, err := contracts.Files.ReadFile("upstream/v1/examples/command-query.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw = bytes.Replace(raw, []byte("command-a"), []byte{0xff}, 1)
|
|
if !bytes.Contains(raw, []byte{0xff}) {
|
|
t.Fatal("invalid UTF-8 fixture was not constructed")
|
|
}
|
|
_, err = contract.DecodeService(raw)
|
|
if err == nil || !isPermanentCommandError(err) {
|
|
t.Fatalf("invalid encoding would be requeued: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPermanentCommandClassification(t *testing.T) {
|
|
if !isPermanentCommandError(contract.ErrInvalidTenantKey) {
|
|
t.Fatal("tenant validation must be permanent")
|
|
}
|
|
for _, raw := range [][]byte{[]byte(`{}`), []byte(`{"schema_version":"invalid"}`), []byte(`{`)} {
|
|
_, _, err := contract.DecodeExecute(raw)
|
|
if err == nil || !isPermanentCommandError(err) {
|
|
t.Fatalf("actual malformed message must be permanent: %v", err)
|
|
}
|
|
}
|
|
if isPermanentCommandError(errors.New("sqlite busy")) {
|
|
t.Fatal("storage failure must be retried")
|
|
}
|
|
if !mq.IsPermanent(mq.Permanent(contract.ErrInvalidTenantKey)) {
|
|
t.Fatal("permanent wrapper was not recognized")
|
|
}
|
|
}
|