Commit d444e49

Eric Bower  ·  2026-08-08 10:22:41 -0400 EDT
parent 0002674
chore: add more tests and ensure block and keepalive work with wildcards
3 files changed,  +243, -4
+8, -4
 1@@ -700,13 +700,15 @@ func (handler *CliHandler) pub(cmd *CliCmd, topic string, clientID string) error
 2 	if *block {
 3 		count := 0
 4 		for topic, channel := range handler.PubSub.GetChannels() {
 5-			if topic == name {
 6+			if topic == name || (psub.HasWildcard(topic) && psub.MatchTopic(topic, name)) {
 7 				for _, client := range channel.GetClients() {
 8 					if client.Direction == psub.ChannelDirectionOutput || client.Direction == psub.ChannelDirectionInputOutput {
 9 						count++
10 					}
11 				}
12-				break
13+				if topic == name {
14+					break
15+				}
16 			}
17 		}
18 
19@@ -735,13 +737,15 @@ func (handler *CliHandler) pub(cmd *CliCmd, topic string, clientID string) error
20 					case <-time.After(1 * time.Millisecond):
21 						count := 0
22 						for topic, channel := range handler.PubSub.GetChannels() {
23-							if topic == name {
24+							if topic == name || (psub.HasWildcard(topic) && psub.MatchTopic(topic, name)) {
25 								for _, client := range channel.GetClients() {
26 									if client.Direction == psub.ChannelDirectionOutput || client.Direction == psub.ChannelDirectionInputOutput {
27 										count++
28 									}
29 								}
30-								break
31+								if topic == name {
32+									break
33+								}
34 							}
35 						}
36 
+106, -0
  1@@ -1,6 +1,7 @@
  2 package pipe
  3 
  4 import (
  5+	"bytes"
  6 	"context"
  7 	"crypto/ed25519"
  8 	"crypto/rand"
  9@@ -9,6 +10,7 @@ import (
 10 	"log/slog"
 11 	"os"
 12 	"strings"
 13+	"sync"
 14 	"testing"
 15 	"time"
 16 
 17@@ -2200,3 +2202,107 @@ func TestMonitor_FixedWindowNonSliding(t *testing.T) {
 18 			monitor.WindowEnd.Format(time.RFC3339))
 19 	}
 20 }
 21+
 22+func TestSSH_WildcardSub(t *testing.T) {
 23+	server := NewTestSSHServer(t)
 24+	defer server.Shutdown()
 25+
 26+	user := GenerateUser("alice")
 27+	dbUser := &db.User{ID: "alice-id", Name: "alice"}
 28+	server.DBPool.AddUser(dbUser)
 29+	server.DBPool.AddPubkey(&db.PublicKey{
 30+		ID:     "alice-pk",
 31+		UserID: "alice-id",
 32+		Key:    user.PublicKey(),
 33+	})
 34+
 35+	client, err := user.NewClient()
 36+	if err != nil {
 37+		t.Fatalf("failed to dial ssh server: %v", err)
 38+	}
 39+	defer func() { _ = client.Close() }()
 40+
 41+	// 1. Subscribe to wildcard topic: "sub metric-drain*"
 42+	subSession, err := client.NewSession()
 43+	if err != nil {
 44+		t.Fatalf("failed to create sub session: %v", err)
 45+	}
 46+
 47+	subOut, err := subSession.StdoutPipe()
 48+	if err != nil {
 49+		t.Fatalf("failed stdout pipe: %v", err)
 50+	}
 51+
 52+	if err := subSession.Start("sub metric-drain*"); err != nil {
 53+		t.Fatalf("failed to start sub: %v", err)
 54+	}
 55+
 56+	var buf bytes.Buffer
 57+	var bufMu sync.Mutex
 58+	go func() {
 59+		b := make([]byte, 1024)
 60+		for {
 61+			n, err := subOut.Read(b)
 62+			if n > 0 {
 63+				bufMu.Lock()
 64+				buf.Write(b[:n])
 65+				bufMu.Unlock()
 66+			}
 67+			if err != nil {
 68+				break
 69+			}
 70+		}
 71+	}()
 72+
 73+	time.Sleep(100 * time.Millisecond)
 74+
 75+	// 2. Publish to "metric-drain-pgs"
 76+	pubClient1, err := user.NewClient()
 77+	if err != nil {
 78+		t.Fatalf("failed to dial pub client 1: %v", err)
 79+	}
 80+	defer func() { _ = pubClient1.Close() }()
 81+
 82+	pubSession1, err := pubClient1.NewSession()
 83+	if err != nil {
 84+		t.Fatalf("failed pub session 1: %v", err)
 85+	}
 86+	pubIn1, _ := pubSession1.StdinPipe()
 87+	go func() {
 88+		defer func() { _ = pubIn1.Close() }()
 89+		_, _ = io.WriteString(pubIn1, "pgs-event\n")
 90+	}()
 91+	_ = pubSession1.Run("pub metric-drain-pgs -b=false")
 92+
 93+	// 3. Publish to "metric-drain-prose"
 94+	pubClient2, err := user.NewClient()
 95+	if err != nil {
 96+		t.Fatalf("failed to dial pub client 2: %v", err)
 97+	}
 98+	defer func() { _ = pubClient2.Close() }()
 99+
100+	pubSession2, err := pubClient2.NewSession()
101+	if err != nil {
102+		t.Fatalf("failed pub session 2: %v", err)
103+	}
104+	pubIn2, _ := pubSession2.StdinPipe()
105+	go func() {
106+		defer func() { _ = pubIn2.Close() }()
107+		_, _ = io.WriteString(pubIn2, "prose-event\n")
108+	}()
109+	_ = pubSession2.Run("pub metric-drain-prose -b=false")
110+
111+	time.Sleep(150 * time.Millisecond)
112+	_ = subSession.Close()
113+
114+	bufMu.Lock()
115+	output := buf.String()
116+	bufMu.Unlock()
117+
118+	if !strings.Contains(output, "pgs-event") {
119+		t.Errorf("expected SSH wildcard subscriber output to contain 'pgs-event', got: %q", output)
120+	}
121+	if !strings.Contains(output, "prose-event") {
122+		t.Errorf("expected SSH wildcard subscriber output to contain 'prose-event', got: %q", output)
123+	}
124+}
+129, -0
  1@@ -73,3 +73,132 @@ func TestWildcardSubExistingAndNewTopics(t *testing.T) {
  2 		t.Errorf("wildcard subscriber should NOT receive other-data, got: %q", got)
  3 	}
  4 }
  5+
  6+// TestWildcardSubMultipleSubscribers verifies that multiple wildcard subscribers
  7+// listening on the same pattern both receive published messages.
  8+func TestWildcardSubMultipleSubscribers(t *testing.T) {
  9+	cast := NewMulticast(slog.Default())
 10+
 11+	subBuf1 := new(Buffer)
 12+	subBuf2 := new(Buffer)
 13+	subCtx, cancelSub := context.WithCancel(context.Background())
 14+	defer cancelSub()
 15+
 16+	wildcardChannel := NewChannel("logs-*")
 17+
 18+	var wg sync.WaitGroup
 19+	wg.Add(2)
 20+
 21+	go func() {
 22+		defer wg.Done()
 23+		_ = cast.Sub(subCtx, "sub-1", subBuf1, []*Channel{wildcardChannel}, false)
 24+	}()
 25+
 26+	go func() {
 27+		defer wg.Done()
 28+		_ = cast.Sub(subCtx, "sub-2", subBuf2, []*Channel{wildcardChannel}, false)
 29+	}()
 30+
 31+	time.Sleep(50 * time.Millisecond)
 32+
 33+	channel := NewChannel("logs-app1")
 34+	pubCtx, cancelPub := context.WithTimeout(context.Background(), 2*time.Second)
 35+	defer cancelPub()
 36+
 37+	_ = cast.Pub(pubCtx, "pub-1", &Buffer{b: *bytes.NewBufferString("app1-log\n")}, []*Channel{channel}, false)
 38+
 39+	time.Sleep(100 * time.Millisecond)
 40+	cancelSub()
 41+	wg.Wait()
 42+
 43+	if subBuf1.String() != "app1-log\n" {
 44+		t.Errorf("sub-1 expected app1-log, got %q", subBuf1.String())
 45+	}
 46+	if subBuf2.String() != "app1-log\n" {
 47+		t.Errorf("sub-2 expected app1-log, got %q", subBuf2.String())
 48+	}
 49+}
 50+
 51+// TestWildcardSubVariousPatterns verifies prefix, suffix, and middle asterisk wildcard matching.
 52+func TestWildcardSubVariousPatterns(t *testing.T) {
 53+	cast := NewMulticast(slog.Default())
 54+
 55+	prefixBuf := new(Buffer)
 56+	suffixBuf := new(Buffer)
 57+	middleBuf := new(Buffer)
 58+
 59+	ctx, cancel := context.WithCancel(context.Background())
 60+	defer cancel()
 61+
 62+	var wg sync.WaitGroup
 63+	wg.Add(3)
 64+
 65+	go func() {
 66+		defer wg.Done()
 67+		_ = cast.Sub(ctx, "sub-prefix", prefixBuf, []*Channel{NewChannel("metric-*")}, false)
 68+	}()
 69+	go func() {
 70+		defer wg.Done()
 71+		_ = cast.Sub(ctx, "sub-suffix", suffixBuf, []*Channel{NewChannel("*-drain")}, false)
 72+	}()
 73+	go func() {
 74+		defer wg.Done()
 75+		_ = cast.Sub(ctx, "sub-middle", middleBuf, []*Channel{NewChannel("metric-*-drain")}, false)
 76+	}()
 77+
 78+	time.Sleep(50 * time.Millisecond)
 79+
 80+	pubCtx, cancelPub := context.WithTimeout(context.Background(), 2*time.Second)
 81+	defer cancelPub()
 82+
 83+	// Publish to metric-app-drain
 84+	_ = cast.Pub(pubCtx, "pub", &Buffer{b: *bytes.NewBufferString("event\n")}, []*Channel{NewChannel("metric-app-drain")}, false)
 85+
 86+	time.Sleep(100 * time.Millisecond)
 87+	cancel()
 88+	wg.Wait()
 89+
 90+	if prefixBuf.String() != "event\n" {
 91+		t.Errorf("prefix subscriber expected event, got %q", prefixBuf.String())
 92+	}
 93+	if suffixBuf.String() != "event\n" {
 94+		t.Errorf("suffix subscriber expected event, got %q", suffixBuf.String())
 95+	}
 96+	if middleBuf.String() != "event\n" {
 97+		t.Errorf("middle subscriber expected event, got %q", middleBuf.String())
 98+	}
 99+}
100+
101+// TestWildcardSubLiteralCharsWithoutStar verifies that '?' or '[' without '*' are treated as literal names.
102+func TestWildcardSubLiteralCharsWithoutStar(t *testing.T) {
103+	cast := NewMulticast(slog.Default())
104+
105+	subBuf := new(Buffer)
106+	ctx, cancel := context.WithCancel(context.Background())
107+	defer cancel()
108+
109+	var wg sync.WaitGroup
110+	wg.Add(1)
111+
112+	// Subscribe to a topic with a literal '?' character
113+	go func() {
114+		defer wg.Done()
115+		_ = cast.Sub(ctx, "sub-literal", subBuf, []*Channel{NewChannel("topic?one")}, false)
116+	}()
117+
118+	time.Sleep(50 * time.Millisecond)
119+
120+	pubCtx, cancelPub := context.WithTimeout(context.Background(), 2*time.Second)
121+	defer cancelPub()
122+
123+	// Publish to topicXone (should NOT match because '?' is not treated as a wildcard)
124+	_ = cast.Pub(pubCtx, "pub", &Buffer{b: *bytes.NewBufferString("data\n")}, []*Channel{NewChannel("topicXone")}, false)
125+
126+	time.Sleep(100 * time.Millisecond)
127+	cancel()
128+	wg.Wait()
129+
130+	if subBuf.String() != "" {
131+		t.Errorf("literal subscriber should not have matched topicXone, got: %q", subBuf.String())
132+	}
133+}