Files
go-sip/internal/mq/v2_integration_test.go
T

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)
}
}
}