173 lines
5.9 KiB
Go
173 lines
5.9 KiB
Go
package mq
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/tenant"
|
|
"github.com/google/uuid"
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
)
|
|
|
|
// This opt-in is deliberately separate from any external broker setting.
|
|
func localBrokerURL(t *testing.T) string {
|
|
t.Helper()
|
|
value := os.Getenv("GO_SIP_LOCAL_MQ_URL")
|
|
if value == "" {
|
|
t.Skip("isolated local RabbitMQ not configured")
|
|
}
|
|
parsed, err := url.Parse(value)
|
|
if err != nil || (parsed.Hostname() != "127.0.0.1" && parsed.Hostname() != "::1" && parsed.Hostname() != "localhost") {
|
|
t.Fatal("MQ integration requires a loopback broker")
|
|
}
|
|
return value
|
|
}
|
|
|
|
func TestV2LocalBrokerIdentityIsolationAndReliableRouting(t *testing.T) {
|
|
address := localBrokerURL(t)
|
|
id1, id2 := uuid.NewString(), uuid.NewString()
|
|
first, err := Open(address, id1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Close()
|
|
second, err := Open(address, id2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Close()
|
|
if duplicate, err := Open(address, id1); err == nil {
|
|
duplicate.Close()
|
|
t.Fatal("duplicate live identity accepted")
|
|
}
|
|
key := "租户." + uuid.NewString()
|
|
q1, err := first.DeclareTenantQueue(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
q2, err := second.DeclareTenantQueue(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q1 == q2 {
|
|
t.Fatal("different Dispatchers share a queue")
|
|
}
|
|
r1, _ := tenant.NewDispatcherRoute(id1, key)
|
|
r2, _ := tenant.NewDispatcherRoute(id2, key)
|
|
connection, err := amqp.Dial(address)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer connection.Close()
|
|
channel, err := connection.Channel()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer channel.Close()
|
|
defer channel.QueueDelete(q1, false, false, false)
|
|
defer channel.QueueDelete(q2, false, false, false)
|
|
defer channel.QueueDelete(r1.DeadLetterQueue, false, false, false)
|
|
defer channel.QueueDelete(r2.DeadLetterQueue, false, false, false)
|
|
defer channel.QueueUnbind(SaaSQueue, r1.OutboundKey, EventExchange, nil)
|
|
defer channel.QueueUnbind(SaaSQueue, r2.OutboundKey, EventExchange, nil)
|
|
if err := channel.Confirm(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
confirmation, err := channel.PublishWithDeferredConfirmWithContext(ctx, DefaultExchange, r1.InboundKey, true, false, amqp.Publishing{DeliveryMode: amqp.Persistent, Body: []byte(`{"target":"first"}`)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ack, err := confirmation.WaitContext(ctx); err != nil || !ack {
|
|
t.Fatalf("command publication: ack=%v error=%v", ack, err)
|
|
}
|
|
if _, ok, err := channel.Get(q2, true); err != nil || ok {
|
|
t.Fatalf("second Dispatcher received first's message: ok=%v error=%v", ok, err)
|
|
}
|
|
message, ok, err := channel.Get(q1, true)
|
|
if err != nil || !ok || string(message.Body) != `{"target":"first"}` {
|
|
t.Fatalf("first did not receive its own message: ok=%v error=%v", ok, err)
|
|
}
|
|
if err := first.Consume(ctx, q2, func(context.Context, string, []byte) error { return nil }); err == nil {
|
|
t.Fatal("foreign queue consumption accepted")
|
|
}
|
|
if err := first.Publish(ctx, EventExchange, r2.OutboundKey, []byte(`{}`)); err == nil {
|
|
t.Fatal("foreign publication accepted")
|
|
}
|
|
if err := first.Publish(ctx, EventExchange, r1.OutboundKey, []byte(`{"event_id":"uploaded"}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
message, ok, err = channel.Get(SaaSQueue, true)
|
|
if err != nil || !ok || message.RoutingKey != r1.OutboundKey || message.DeliveryMode != amqp.Persistent {
|
|
t.Fatalf("expected persistent target-queue delivery: ok=%v error=%v", ok, err)
|
|
}
|
|
if err := channel.QueueUnbind(SaaSQueue, r1.OutboundKey, EventExchange, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := first.Publish(ctx, EventExchange, r1.OutboundKey, []byte(`{"event_id":"unroutable"}`)); err == nil || !strings.Contains(err.Error(), "returned") {
|
|
t.Fatalf("unroutable confirm treated as delivery: %v", err)
|
|
}
|
|
if err := channel.QueueBind(SaaSQueue, r1.OutboundKey, EventExchange, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := first.Publish(ctx, EventExchange, r1.OutboundKey, []byte(`{"event_id":"recovered"}`)); err != nil {
|
|
t.Fatalf("return contaminated later publication: %v", err)
|
|
}
|
|
message, ok, err = channel.Get(SaaSQueue, true)
|
|
if err != nil || !ok || string(message.Body) != `{"event_id":"recovered"}` || message.MessageId != "recovered" {
|
|
t.Fatalf("recovery delivery missing: ok=%v error=%v", ok, err)
|
|
}
|
|
consumeCtx, stop := context.WithCancel(ctx)
|
|
defer stop()
|
|
called := make(chan struct{}, 1)
|
|
finished := make(chan error, 1)
|
|
go func() {
|
|
finished <- first.Consume(consumeCtx, q1, func(context.Context, string, []byte) error { called <- struct{}{}; return nil })
|
|
}()
|
|
confirmation, err = channel.PublishWithDeferredConfirmWithContext(ctx, DefaultExchange, r1.InboundKey, true, false, amqp.Publishing{DeliveryMode: amqp.Persistent, Body: []byte(strings.Repeat("x", MaxMessageBytes+1))})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ack, err := confirmation.WaitContext(ctx); err != nil || !ack {
|
|
t.Fatalf("large message publication: %v", err)
|
|
}
|
|
deadline := time.NewTimer(2 * time.Second)
|
|
defer deadline.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-called:
|
|
t.Fatal("oversized message reached the business handler")
|
|
case <-deadline.C:
|
|
t.Fatal("oversized message was not durably dead-lettered")
|
|
case <-ticker.C:
|
|
message, ok, err := channel.Get(r1.DeadLetterQueue, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
if len(message.Body) != MaxMessageBytes+1 {
|
|
t.Fatal("wrong dead-lettered message")
|
|
}
|
|
stop()
|
|
<-finished
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenRejectsNonCanonicalIdentityBeforeDial(t *testing.T) {
|
|
for _, id := range []string{"", DefaultExchange, "dispatcher", "00000000-0000-0000-0000-000000000000"} {
|
|
if _, err := Open("amqp://unused.invalid", id); err == nil || !strings.Contains(err.Error(), "dispatcher_id") {
|
|
t.Fatalf("identity %q not rejected before dial: %v", id, err)
|
|
}
|
|
}
|
|
}
|