960 lines
24 KiB
Go
960 lines
24 KiB
Go
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
|
|
}
|