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

130 lines
4.0 KiB
Go

//go:build integration
package mq
import (
"context"
"os"
"testing"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
// A full task queue is SaaS-owned: the publisher must observe the rejection
// and retain its command, rather than expecting Dispatcher to create a queue
// or invent the missing message after restart.
func TestV3FullSaaSOwnedTaskQueueRejectsSecondCommand(t *testing.T) {
url := os.Getenv("RABBITMQ_URL")
if url == "" {
t.Skip("RABBITMQ_URL not set")
}
provisionerURL := os.Getenv("RABBITMQ_PROVISIONER_URL")
if provisionerURL == "" {
provisionerURL = url
}
const dispatcherID = "550e8400-e29b-41d4-a716-446655440099"
const taskQueue = "agent-call.d." + dispatcherID + ".task.queue-full-1.v3"
const routingKey = "d." + dispatcherID + ".task.queue-full-1.in"
provisioner, err := amqp.Dial(provisionerURL)
if err != nil {
t.Fatal(err)
}
defer provisioner.Close()
channel, err := provisioner.Channel()
if err != nil {
t.Fatal(err)
}
defer channel.Close()
for _, exchange := range []string{CommandsExchangeV3, ResultsExchangeV3, DeadLetterExchangeV3} {
if err := channel.ExchangeDeclare(exchange, "topic", true, false, false, false, nil); err != nil {
t.Fatal(err)
}
}
if _, err := channel.QueueDeclare(taskQueue, true, false, false, false, amqp.Table{
"x-max-length": int32(1), "x-overflow": "reject-publish",
}); err != nil {
t.Fatal(err)
}
defer channel.QueueDelete(taskQueue, false, false, false)
if err := channel.QueueBind(taskQueue, routingKey, CommandsExchangeV3, false, nil); err != nil {
t.Fatal(err)
}
controlQueue := V3ControlQueueName(dispatcherID)
resultQueue := "agent-call.saas.d." + dispatcherID + ".v3"
for _, queue := range []string{controlQueue, resultQueue} {
if _, err := channel.QueueDeclare(queue, true, false, false, false, nil); err != nil {
t.Fatal(err)
}
defer channel.QueueDelete(queue, false, false, false)
}
if err := channel.QueueBind(controlQueue, "d."+dispatcherID+".control.in", CommandsExchangeV3, false, nil); err != nil {
t.Fatal(err)
}
if err := channel.QueueBind(resultQueue, "d."+dispatcherID+".out", ResultsExchangeV3, false, nil); err != nil {
t.Fatal(err)
}
if err := channel.Confirm(false); err != nil {
t.Fatal(err)
}
confirms := channel.NotifyPublish(make(chan amqp.Confirmation, 2))
returned := channel.NotifyReturn(make(chan amqp.Return, 2))
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
publish := func(body string) bool {
t.Helper()
if err := channel.PublishWithContext(ctx, CommandsExchangeV3, routingKey, true, false, amqp.Publishing{
ContentType: "application/json", DeliveryMode: amqp.Persistent, Body: []byte(body),
}); err != nil {
t.Fatal(err)
}
select {
case confirmation := <-confirms:
if !confirmation.Ack {
return false
}
select {
case <-returned:
return false
default:
return true
}
case <-ctx.Done():
t.Fatal("SaaS publisher received no RabbitMQ confirmation")
return false
}
}
const first = `{"event_id":"original-task-command"}`
if !publish(first) || publish(`{"event_id":"rejected-task-command"}`) {
t.Fatal("full SaaS-owned queue did not confirm the first command and reject the second")
}
queued, err := channel.QueueInspect(taskQueue)
if err != nil || queued.Messages != 1 {
t.Fatalf("queue-full publication invented or discarded the original command: count=%d err=%v", queued.Messages, err)
}
broker, err := OpenV3(url, dispatcherID, 1)
if err != nil {
t.Fatal(err)
}
defer broker.Close()
got := make(chan string, 1)
consumer, err := broker.StartPredeclaredConsumer(ctx, taskQueue, func(_ context.Context, _ string, body []byte) error {
got <- string(body)
return nil
})
if err != nil {
t.Fatal(err)
}
select {
case body := <-got:
if body != first {
t.Fatalf("Dispatcher consumed an unpublished command: %s", body)
}
case <-ctx.Done():
t.Fatal("Dispatcher did not consume the SaaS-retained original command")
}
if err := consumer.Stop(context.Background()); err != nil {
t.Fatal(err)
}
}