package rabbitmq import ( "context" "reflect" "sync" "testing" ) type fakeChannel struct { mu sync.Mutex cancelCalls int closeCalls int closed bool } func (f *fakeChannel) Cancel(string, bool) error { f.mu.Lock() defer f.mu.Unlock() f.cancelCalls++ return nil } func (f *fakeChannel) Close() error { f.mu.Lock() defer f.mu.Unlock() f.closeCalls++ f.closed = true return nil } func (f *fakeChannel) IsClosed() bool { f.mu.Lock() defer f.mu.Unlock() return f.closed } func TestCloseIsIdempotent(t *testing.T) { resetRabbitMQState(t) channel := &fakeChannel{} done := make(chan struct{}) close(done) registerConsumer(&managedConsumer{ channel: channel, tag: "test-consumer", done: done, }) if err := Close(context.Background()); err != nil { t.Fatalf("first Close() error = %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := Close(ctx); err != nil { t.Fatalf("second Close() error = %v", err) } channel.mu.Lock() defer channel.mu.Unlock() if channel.cancelCalls != 1 { t.Fatalf("Cancel() calls = %d, want 1", channel.cancelCalls) } if channel.closeCalls != 1 { t.Fatalf("Close() calls = %d, want 1", channel.closeCalls) } } func TestCloseSkipsAlreadyClosedChannel(t *testing.T) { resetRabbitMQState(t) channel := &fakeChannel{closed: true} done := make(chan struct{}) close(done) registerConsumer(&managedConsumer{ channel: channel, tag: "closed-consumer", done: done, }) if err := Close(context.Background()); err != nil { t.Fatalf("Close() error = %v", err) } channel.mu.Lock() defer channel.mu.Unlock() if channel.cancelCalls != 0 || channel.closeCalls != 0 { t.Fatalf("closed channel calls: Cancel=%d Close=%d, want 0", channel.cancelCalls, channel.closeCalls) } } func TestCloseHonorsContextWhileWaiting(t *testing.T) { resetRabbitMQState(t) channel := &fakeChannel{} registerConsumer(&managedConsumer{ channel: channel, tag: "blocked-consumer", done: make(chan struct{}), }) ctx, cancel := context.WithCancel(context.Background()) cancel() if err := Close(ctx); err == nil { t.Fatal("Close() error = nil, want context cancellation error") } channel.mu.Lock() defer channel.mu.Unlock() if channel.closeCalls != 1 { t.Fatalf("Close() calls = %d, want forced cleanup", channel.closeCalls) } } func TestDeliveryMetadataContext(t *testing.T) { expected := DeliveryMetadata{ DeliveryTag: 42, Redelivered: true, Consumer: "batch-consumer", Queue: "iotlcm.batch.execute", } ctx := WithDeliveryMetadata(context.Background(), expected) actual, ok := DeliveryMetadataFromContext(ctx) if !ok { t.Fatal("delivery metadata not found in context") } if !reflect.DeepEqual(actual, expected) { t.Fatalf("delivery metadata = %+v, want %+v", actual, expected) } if _, ok = DeliveryMetadataFromContext(context.Background()); ok { t.Fatal("unexpected delivery metadata in empty context") } } func resetRabbitMQState(t *testing.T) { t.Helper() consumerMu.Lock() consumers = nil consumerMu.Unlock() connMu.Lock() conn = nil connMu.Unlock() t.Cleanup(func() { consumerMu.Lock() consumers = nil consumerMu.Unlock() connMu.Lock() conn = nil connMu.Unlock() }) }