package compatibility import ( "context" "fmt" "sync" "testing" "time" "github.com/redis/go-redis/v9" ) // TestGoRedisBasicSubscribe tests basic SUBSCRIBE functionality func TestGoRedisBasicSubscribe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: ts.Addr(), }) defer func() { _ = rdb.Close() }() pubsub := rdb.Subscribe(ctx, "test-channel") defer func() { _ = pubsub.Close() }() // Channel() returns a channel that receives messages (not subscription confirmations) // The subscription confirmation is handled internally by go-redis // We just need to verify we can get the channel without errors ch := pubsub.Channel() // Publish a message to verify subscription works pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "test-channel", "test-msg").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } // Now we should receive the message msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for message") } if msg.Channel != "test-channel" { t.Errorf("Expected channel 'test-channel', got '%s'", msg.Channel) } if msg.Payload != "test-msg" { t.Errorf("Expected 'test-msg', got '%s'", msg.Payload) } } // TestGoRedisBasicPublish tests basic PUBLISH functionality func TestGoRedisBasicPublish(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() pubsub := sub.Subscribe(ctx, "test-pub") defer func() { _ = pubsub.Close() }() ch := pubsub.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Publish message err := pub.Publish(ctx, "test-pub", "hello world").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } // Receive message msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for message") } if msg.Payload != "hello world" { t.Errorf("Expected 'hello world', got '%s'", msg.Payload) } if msg.Channel != "test-pub" { t.Errorf("Expected channel 'test-pub', got '%s'", msg.Channel) } } // TestGoRedisMultipleSubscribers tests multiple clients subscribing to same channel func TestGoRedisMultipleSubscribers(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() numSubscribers := 5 // Create multiple subscribers var subscribers []*redis.Client var pubsubs []*redis.PubSub var channels []<-chan *redis.Message for i := 0; i < numSubscribers; i++ { rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) subscribers = append(subscribers, rdb) ps := rdb.Subscribe(ctx, "multi-sub") pubsubs = append(pubsubs, ps) channels = append(channels, ps.Channel()) } defer func() { for _, ps := range pubsubs { _ = ps.Close() } for _, rdb := range subscribers { _ = rdb.Close() } }() // Give subscriptions time to register time.Sleep(100 * time.Millisecond) // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Publish message err := pub.Publish(ctx, "multi-sub", "broadcast").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } // All subscribers should receive for i, ch := range channels { msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Errorf("Subscriber %d timeout", i) continue } if msg.Payload != "broadcast" { t.Errorf("Subscriber %d got wrong payload: %s", i, msg.Payload) } } } // TestGoRedisUnsubscribe tests UNSUBSCRIBE functionality func TestGoRedisUnsubscribe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() pubsub := rdb.Subscribe(ctx, "unsub-test") defer func() { _ = pubsub.Close() }() // Verify subscription by publishing and receiving ch := pubsub.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "unsub-test", "msg").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for message") } // Unsubscribe err = pubsub.Unsubscribe(ctx, "unsub-test") if err != nil { t.Fatalf("Unsubscribe failed: %v", err) } // After unsubscribe, should not receive messages // Give time for unsubscribe to process time.Sleep(100 * time.Millisecond) } // TestGoRedisPatternSubscribe tests PSUBSCRIBE functionality func TestGoRedisPatternSubscribe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() pubsub := rdb.PSubscribe(ctx, "news.*") defer func() { _ = pubsub.Close() }() // Get the channel for receiving messages ch := pubsub.Channel() // Publish to a matching channel to verify subscription pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "news.sports", "game results").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } // Should receive the message msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for pattern message") } if msg.Payload != "game results" { t.Errorf("Expected 'game results', got '%s'", msg.Payload) } } // TestGoRedisPatternMatch tests pattern matching with PUBLISH func TestGoRedisPatternMatch(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber with pattern sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() pubsub := sub.PSubscribe(ctx, "news.*") defer func() { _ = pubsub.Close() }() ch := pubsub.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Publish to matching channel err := pub.Publish(ctx, "news.sports", "game results").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } // Receive pattern message msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for pattern message") } if msg.Payload != "game results" { t.Errorf("Expected 'game results', got '%s'", msg.Payload) } if msg.Channel != "news.sports" { t.Errorf("Expected channel 'news.sports', got '%s'", msg.Channel) } } // TestGoRedisMultiplePatterns tests subscribing to multiple patterns func TestGoRedisMultiplePatterns(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() pubsub := rdb.PSubscribe(ctx, "news.*", "sports.*", "weather.*") defer func() { _ = pubsub.Close() }() // Get channel and verify subscription by publishing ch := pubsub.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Publish to each pattern _ = pub.Publish(ctx, "news.test", "news-msg").Err() _ = pub.Publish(ctx, "sports.test", "sports-msg").Err() _ = pub.Publish(ctx, "weather.test", "weather-msg").Err() // Should receive 3 messages for i := 0; i < 3; i++ { _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatalf("Timeout waiting for message %d", i) } } } // TestGoRedisMultipleChannels tests subscribing to multiple channels func TestGoRedisMultipleChannels(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() pubsub := rdb.Subscribe(ctx, "ch1", "ch2", "ch3") defer func() { _ = pubsub.Close() }() // Verify by publishing to each channel ch := pubsub.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() _ = pub.Publish(ctx, "ch1", "msg1").Err() _ = pub.Publish(ctx, "ch2", "msg2").Err() _ = pub.Publish(ctx, "ch3", "msg3").Err() // Should receive 3 messages for i := 0; i < 3; i++ { _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatalf("Timeout waiting for channel %d message", i) } } } // TestGoRedisPing tests PING command in non-pubsub mode func TestGoRedisPing(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() result, err := rdb.Ping(ctx).Result() if err != nil { t.Fatalf("Ping failed: %v", err) } if result != "PONG" { t.Errorf("Expected 'PONG', got '%s'", result) } } // TestGoRedisPingWithMessage tests PING with message in non-pubsub mode func TestGoRedisPingWithMessage(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() result, err := rdb.Do(ctx, "PING", "hello").Text() if err != nil { t.Fatalf("Ping failed: %v", err) } if result != "hello" { t.Errorf("Expected 'hello', got '%s'", result) } } // TestGoRedisPubSubNumsub tests PUBSUB NUMSUB command func TestGoRedisPubSubNumsub(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Create subscribers sub1 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub1.Close() }() ps1 := sub1.Subscribe(ctx, "channel1") defer func() { _ = ps1.Close() }() sub2 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub2.Close() }() ps2 := sub2.Subscribe(ctx, "channel1", "channel2") defer func() { _ = ps2.Close() }() // Wait for subscriptions time.Sleep(100 * time.Millisecond) // Use regular client for PUBSUB command pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() result, err := pub.Do(ctx, "PUBSUB", "NUMSUB", "channel1", "channel2").Result() if err != nil { t.Fatalf("PUBSUB NUMSUB failed: %v", err) } // Result should be an array with channel,count pairs t.Logf("PUBSUB NUMSUB result: %v", result) } // TestGoRedisPubSubChannels tests PUBSUB CHANNELS command func TestGoRedisPubSubChannels(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscribe to channels sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() ps := sub.Subscribe(ctx, "test-cha-1", "test-cha-2") defer func() { _ = ps.Close() }() time.Sleep(100 * time.Millisecond) // Query channels pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() result, err := pub.Do(ctx, "PUBSUB", "CHANNELS").Result() if err != nil { t.Fatalf("PUBSUB CHANNELS failed: %v", err) } t.Logf("PUBSUB CHANNELS result: %v", result) } // TestGoRedisPubSubNumpat tests PUBSUB NUMPAT command func TestGoRedisPubSubNumpat(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Create pattern subscribers sub1 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub1.Close() }() ps1 := sub1.PSubscribe(ctx, "foo.*") defer func() { _ = ps1.Close() }() sub2 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub2.Close() }() ps2 := sub2.PSubscribe(ctx, "bar.*") defer func() { _ = ps2.Close() }() time.Sleep(100 * time.Millisecond) pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() result, err := pub.Do(ctx, "PUBSUB", "NUMPAT").Result() if err != nil { t.Fatalf("PUBSUB NUMPAT failed: %v", err) } t.Logf("PUBSUB NUMPAT result: %v", result) } // TestGoRedisBinarySafe tests binary-safe message passing func TestGoRedisBinarySafe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() pubsub := sub.Subscribe(ctx, "binary-test") defer func() { _ = pubsub.Close() }() ch := pubsub.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Binary message with null bytes and special chars binaryMsg := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, '\r', '\n', 0x00} err := pub.Publish(ctx, "binary-test", string(binaryMsg)).Err() if err != nil { t.Fatalf("Publish binary failed: %v", err) } msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for binary message") } if msg.Payload != string(binaryMsg) { t.Errorf("Binary message mismatch: got %v, want %v", []byte(msg.Payload), binaryMsg) } } // TestGoRedisEmptyMessage tests publishing empty message func TestGoRedisEmptyMessage(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() pubsub := sub.Subscribe(ctx, "empty-test") defer func() { _ = pubsub.Close() }() ch := pubsub.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "empty-test", "").Err() if err != nil { t.Fatalf("Publish empty failed: %v", err) } msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for empty message") } if msg.Payload != "" { t.Errorf("Expected empty payload, got '%s'", msg.Payload) } } // TestGoRedisLongMessage tests publishing long message func TestGoRedisLongMessage(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() pubsub := sub.Subscribe(ctx, "long-test") defer func() { _ = pubsub.Close() }() ch := pubsub.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Create 1MB message longMsg := make([]byte, 1024*1024) for i := range longMsg { longMsg[i] = byte(i % 256) } err := pub.Publish(ctx, "long-test", string(longMsg)).Err() if err != nil { t.Fatalf("Publish long failed: %v", err) } msg, ok := waitForMessage(ch, 5*time.Second) if !ok { t.Fatal("Timeout waiting for long message") } if len(msg.Payload) != len(longMsg) { t.Errorf("Message length mismatch: got %d, want %d", len(msg.Payload), len(longMsg)) } } // TestGoRedisConcurrentSubscribers tests concurrent subscription/unsubscription func TestGoRedisConcurrentSubscribers(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() var wg sync.WaitGroup // Concurrent subscribe/unsubscribe for i := 0; i < 20; i++ { wg.Add(1) go func(id int) { defer wg.Done() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() ps := rdb.Subscribe(ctx, fmt.Sprintf("concurrent-%d", id%5)) defer func() { _ = ps.Close() }() time.Sleep(50 * time.Millisecond) }(i) } wg.Wait() } // TestGoRedisChannelWithSpecialChars tests channel names with special characters func TestGoRedisChannelWithSpecialChars(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() channels := []string{ "channel-with-dash", "channel_with_underscore", "channel.with.dots", "channel:with:colons", "channel/with/slash", } for _, channelName := range channels { t.Run(channelName, func(t *testing.T) { // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() ps := sub.Subscribe(ctx, channelName) defer func() { _ = ps.Close() }() ch := ps.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, channelName, "test").Err() if err != nil { t.Fatalf("Publish to %s failed: %v", channelName, err) } msg, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatalf("Timeout waiting for message on %s", channelName) } if msg.Channel != channelName { t.Errorf("Channel mismatch: got %s, want %s", msg.Channel, channelName) } }) } } // TestGoRedisUnicodeMessages tests Unicode message content func TestGoRedisUnicodeMessages(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() messages := []string{ "Hello, 世界!", "Emoji test: 🎉🚀✨", "Cyrillic: Привет мир", "Arabic: مرحبا بالعالم", "Japanese: こんにちは世界", } for _, msg := range messages { t.Run(msg[:min(20, len(msg))], func(t *testing.T) { // Subscriber sub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub.Close() }() ps := sub.Subscribe(ctx, "unicode-test") defer func() { _ = ps.Close() }() ch := ps.Channel() // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "unicode-test", msg).Err() if err != nil { t.Fatalf("Publish failed: %v", err) } received, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout waiting for Unicode message") } if received.Payload != msg { t.Errorf("Unicode mismatch: got %q, want %q", received.Payload, msg) } }) } } // TestGoRedisPublishNoSubscribers tests PUBLISH with no subscribers func TestGoRedisPublishNoSubscribers(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() count, err := rdb.Publish(ctx, "no-subs-channel", "message").Result() if err != nil { t.Fatalf("Publish failed: %v", err) } if count != 0 { t.Errorf("Expected 0 receivers, got %d", count) } } // TestGoRedisSubscribeReturnCount tests that SUBSCRIBE returns correct count func TestGoRedisSubscribeReturnCount(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() ps := rdb.Subscribe(ctx, "ch1") defer func() { _ = ps.Close() }() // The subscription is confirmed internally by go-redis // Verify by publishing a message ch := ps.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() err := pub.Publish(ctx, "ch1", "test").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout") } } // TestGoRedisConcurrentPublishSubscribe tests concurrent pub/sub operations func TestGoRedisConcurrentPublishSubscribe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() numMessages := 100 numSubscribers := 5 var mu sync.Mutex receivedCounts := make([]int, numSubscribers) var wg sync.WaitGroup // Start subscribers for i := 0; i < numSubscribers; i++ { wg.Add(1) go func(id int) { defer wg.Done() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() ps := rdb.Subscribe(ctx, "concurrent-pub-test") defer func() { _ = ps.Close() }() ch := ps.Channel() for j := 0; j < numMessages; j++ { _, ok := waitForMessage(ch, 5*time.Second) if ok { mu.Lock() receivedCounts[id]++ mu.Unlock() } } }(i) } // Give subscribers time to connect time.Sleep(200 * time.Millisecond) // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() for i := 0; i < numMessages; i++ { err := pub.Publish(ctx, "concurrent-pub-test", fmt.Sprintf("msg-%d", i)).Err() if err != nil { t.Errorf("Publish %d failed: %v", i, err) } } wg.Wait() // Verify all subscribers received all messages totalReceived := 0 for i, count := range receivedCounts { totalReceived += count if count < numMessages-1 { // Allow 1 message loss due to timing t.Logf("Subscriber %d received %d/%d messages", i, count, numMessages) } } t.Logf("Total messages received: %d/%d", totalReceived, numMessages*numSubscribers) } // TestGoRedisSubscribePatternThenChannel tests mixing pattern and channel subscriptions func TestGoRedisSubscribePatternThenChannel(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() // Subscribe to pattern first ps := rdb.PSubscribe(ctx, "test.*") defer func() { _ = ps.Close() }() ch := ps.Channel() // Subscribe to a regular channel err := ps.Subscribe(ctx, "direct-channel") if err != nil { t.Fatalf("Subscribe to channel failed: %v", err) } // Verify both subscriptions work pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() _ = pub.Publish(ctx, "test.foo", "pattern-msg").Err() _ = pub.Publish(ctx, "direct-channel", "channel-msg").Err() // Should receive both messages for i := 0; i < 2; i++ { _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatalf("Timeout waiting for message %d", i) } } } // TestGoRedisUnsubscribeNoChannel tests UNSUBSCRIBE when not subscribed func TestGoRedisUnsubscribeNoChannel(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() ps := rdb.Subscribe(ctx, "temp-channel") defer func() { _ = ps.Close() }() // Verify subscription works ch := ps.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() _ = pub.Publish(ctx, "temp-channel", "test").Err() _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout") } // Unsubscribe err := ps.Unsubscribe(ctx, "temp-channel") if err != nil { t.Fatalf("Unsubscribe failed: %v", err) } } // TestGoRedisPunsubscribe tests PUNSUBSCRIBE functionality func TestGoRedisPunsubscribe(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() ps := rdb.PSubscribe(ctx, "pattern.*") defer func() { _ = ps.Close() }() // Verify subscription ch := ps.Channel() pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() _ = pub.Publish(ctx, "pattern.test", "msg").Err() _, ok := waitForMessage(ch, 2*time.Second) if !ok { t.Fatal("Timeout") } // Punsubscribe err := ps.PUnsubscribe(ctx, "pattern.*") if err != nil { t.Fatalf("PUnsubscribe failed: %v", err) } } // TestGoRedisMixedPubSub tests a mix of channel and pattern subscriptions func TestGoRedisMixedPubSub(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() // Subscriber 1: regular channel sub1 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub1.Close() }() ps1 := sub1.Subscribe(ctx, "mixed-channel") defer func() { _ = ps1.Close() }() // Subscriber 2: pattern sub2 := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = sub2.Close() }() ps2 := sub2.PSubscribe(ctx, "mixed*") defer func() { _ = ps2.Close() }() // Wait for subscriptions time.Sleep(100 * time.Millisecond) // Publisher pub := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = pub.Close() }() // Publish to "mixed-channel" - both should receive err := pub.Publish(ctx, "mixed-channel", "hello").Err() if err != nil { t.Fatalf("Publish failed: %v", err) } ch1 := ps1.Channel() ch2 := ps2.Channel() msg1, ok := waitForMessage(ch1, 2*time.Second) if !ok { t.Error("Channel subscriber timeout") } else if msg1.Payload != "hello" { t.Errorf("Wrong payload: %s", msg1.Payload) } msg2, ok := waitForMessage(ch2, 2*time.Second) if !ok { t.Error("Pattern subscriber timeout") } else if msg2.Payload != "hello" { t.Errorf("Wrong payload: %s", msg2.Payload) } } // TestGoRedisQuit tests QUIT command properly closes connection func TestGoRedisQuit(t *testing.T) { ts := startTestServer(t) defer ts.Close() ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: ts.Addr()}) defer func() { _ = rdb.Close() }() // Use Do to send QUIT command directly (Quit method may not be implemented in all versions) result, err := rdb.Do(ctx, "QUIT").Text() if err != nil { t.Fatalf("Quit failed: %v", err) } if result != "OK" { t.Errorf("Expected 'OK', got '%s'", result) } } func min(a, b int) int { if a < b { return a } return b }