This commit is contained in:
mats committed 2026-06-28 17:52:49 +02:00
1 parent e706f7d907
commit 21bcb63440
42 files changed
+5903

No files matched your search

+258
View File
@@ -0,0 +1,258 @@
package rabbit_test
import (
"os"
"sync"
"testing"
"time"
"git.k6n.net/mats/platform/rabbit"
)
// rabbitURL returns the RabbitMQ URL for integration tests, or empty if unset.
// Tests check this and skip when no broker is available.
func rabbitURL() string {
if u := os.Getenv("RABBIT_URL"); u != "" {
return u
}
if u := os.Getenv("RABBIT_HOST"); u != "" {
return "amqp://" + u
}
// Default local dev URL used by docker-compose.
return "amqp://guest:guest@localhost:5672"
}
// skipWithoutRabbit skips the test if no RabbitMQ broker is reachable.
func skipWithoutRabbit(t *testing.T) string {
t.Helper()
url := rabbitURL()
// Quick probe: try connecting and immediately close.
conn, err := rabbit.Connect(url, "test-probe")
if err != nil {
t.Skipf("RabbitMQ not available at %s: %v", url, err)
}
_ = conn.Close()
return url
}
func TestDial_ConnectAndClose(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-conn")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
// The underlying connection should be usable.
if c := conn.Connection(); c == nil {
t.Fatal("Connection() returned nil")
}
ch, err := conn.Channel()
if err != nil {
t.Fatalf("Channel() failed: %v", err)
}
if err := ch.Close(); err != nil {
t.Fatalf("channel close: %v", err)
}
}
func TestDial_Done(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-done")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
// Done should NOT be closed initially.
select {
case <-conn.Done():
t.Fatal("Done channel closed before Close()")
default:
}
// After Close, Done should be closed.
_ = conn.Close()
select {
case <-conn.Done():
// expected
case <-time.After(time.Second):
t.Fatal("Done channel not closed after Close()")
}
// Double close is safe.
_ = conn.Close()
}
func TestDial_NotifyOnReconnect(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-notify")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
// NotifyOnReconnect should call the function immediately.
called := make(chan struct{}, 1)
conn.NotifyOnReconnect(func() {
called <- struct{}{}
})
select {
case <-called:
// expected — immediate call
case <-time.After(time.Second):
t.Fatal("NotifyOnReconnect was not called immediately")
}
// Multiple registrations should each fire immediately.
var mu sync.Mutex
count := 0
conn.NotifyOnReconnect(func() {
mu.Lock()
count++
mu.Unlock()
})
conn.NotifyOnReconnect(func() {
mu.Lock()
count++
mu.Unlock()
})
// Wait a moment for the callbacks to run.
time.Sleep(50 * time.Millisecond)
mu.Lock()
if count != 2 {
t.Fatalf("expected 2 immediate NotifyOnReconnect calls, got %d", count)
}
mu.Unlock()
}
func TestDial_NotifyReconnect_NotImmediate(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-notify-reconnect")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
called := make(chan struct{}, 1)
conn.NotifyReconnect(func() {
called <- struct{}{}
})
select {
case <-called:
t.Fatal("NotifyReconnect fired immediately")
case <-time.After(100 * time.Millisecond):
// expected
}
}
func TestDial_OnReconnectChannel(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-onreconn")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
// OnReconnect returns an open channel immediately after Dial.
// It should NOT fire (only triggers on actual reconnects).
select {
case <-conn.OnReconnect():
t.Log("OnReconnect fired immediately (race with initial connect — allowed)")
default:
}
// Verify OnReconnect returns a non-nil channel.
ch := conn.OnReconnect()
if ch == nil {
t.Fatal("OnReconnect returned nil channel")
}
}
func TestDial_ConcurrentUse(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-concurrent")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
// Channel() and Connection() should be safe from multiple goroutines.
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ch, err := conn.Channel()
if err != nil {
t.Logf("concurrent Channel(): %v", err)
return
}
_ = ch.Close()
}()
}
wg.Wait()
}
// TestOnReconnect_RegisteredFnsFiresOnReconnect is a structural test: it
// verifies that the registered notification list is called on reconnect by
// checking that functions registered after initial connect are not called
// immediately (they only fire on the NEXT reconnect). Full reconnect testing
// requires controlling the broker — see the integration test harness.
func TestOnReconnect_NotImmediate(t *testing.T) {
url := skipWithoutRabbit(t)
conn, err := rabbit.Dial(url, "test-reconn-struct")
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
// Register a function with NotifyOnReconnect AFTER Dial. It fires
// immediately (that's the contract).
immediate := make(chan struct{}, 1)
conn.NotifyOnReconnect(func() {
immediate <- struct{}{}
})
select {
case <-immediate:
// expected
case <-time.After(time.Second):
t.Fatal("NotifyOnReconnect did not fire immediately")
}
// OnReconnect should NOT have fired (only fires on actual reconnect).
select {
case <-conn.OnReconnect():
t.Log("OnReconnect fired — race goroutine from initial connect")
default:
}
}
func TestConnect_BackwardCompat(t *testing.T) {
url := skipWithoutRabbit(t)
// Connect should still work and return a usable *amqp.Connection.
conn, err := rabbit.Connect(url, "test-compat")
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
t.Fatalf("Channel from Connect failed: %v", err)
}
_ = ch.Close()
}