From 4decc61f4b55d3e245f5d1b60388403e1efee611 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Tue, 23 Jun 2026 07:35:48 +0000 Subject: [PATCH] fix(log): normalize client namespace filters --- log/log.go | 26 +++++++++++++++++++++++++- log/log_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ ws/server_test.go | 46 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/log/log.go b/log/log.go index be5646d..97caba5 100644 --- a/log/log.go +++ b/log/log.go @@ -56,10 +56,34 @@ func (c *Client) matchesNamespace(namespace string) bool { return false } +func normalizeClientNamespaces(namespaces []string) []string { + if len(namespaces) == 0 { + return nil + } + + normalized := make([]string, 0, len(namespaces)) + seen := make(map[string]struct{}, len(namespaces)) + for _, namespace := range namespaces { + namespace = strings.TrimSpace(namespace) + if namespace == "" { + continue + } + if _, ok := seen[namespace]; ok { + continue + } + seen[namespace] = struct{}{} + normalized = append(normalized, namespace) + } + if len(normalized) == 0 { + return nil + } + return normalized +} + func CreateClient(namespaces ...string) *Client { var client Client client.initialized = true - client.Namespaces = namespaces + client.Namespaces = normalizeClientNamespaces(namespaces) client.writer = make(LogWriter, 1000) sliceTex.Lock() clients = append(clients, &client) diff --git a/log/log_test.go b/log/log_test.go index 2ade7ae..a99f599 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -580,6 +580,47 @@ func TestMatchesNamespace(t *testing.T) { c2.Destroy() } +func TestCreateClientNormalizesNamespaces(t *testing.T) { + c := CreateClient(" api ", "", "auth", "api", "\t") + defer c.Destroy() + + want := []string{"api", "auth"} + if len(c.Namespaces) != len(want) { + t.Fatalf("namespaces length = %d, want %d: %#v", len(c.Namespaces), len(want), c.Namespaces) + } + for i := range want { + if c.Namespaces[i] != want[i] { + t.Fatalf("namespaces[%d] = %q, want %q", i, c.Namespaces[i], want[i]) + } + } +} + +func TestCreateClientBlankNamespacesMatchAll(t *testing.T) { + c := CreateClient("", " ") + defer c.Destroy() + + if len(c.Namespaces) != 0 { + t.Fatalf("blank namespaces should normalize to all namespaces, got %#v", c.Namespaces) + } + if !c.matchesNamespace("api") { + t.Error("blank-only namespace filters should match all namespaces") + } +} + +func TestCreateClientCopiesNamespaceFilters(t *testing.T) { + namespaces := []string{"api"} + c := CreateClient(namespaces...) + defer c.Destroy() + + namespaces[0] = "auth" + if !c.matchesNamespace("api") { + t.Error("client namespace filters should not change when caller mutates input slice") + } + if c.matchesNamespace("auth") { + t.Error("client should not match mutated caller namespace") + } +} + // TestGetContext verifies context cancellation stops blocking Get. func TestGetContext(t *testing.T) { c := CreateClient(DefaultNamespace) diff --git a/ws/server_test.go b/ws/server_test.go index e4ed6db..1d3d600 100644 --- a/ws/server_test.go +++ b/ws/server_test.go @@ -183,18 +183,24 @@ func TestLogSocketHandler_NamespaceFilter_TrimsWhitespace(t *testing.T) { defer conn.Close() filteredLogger := logger.NewLogger("filtered-ns") - filteredLogger.Info("trimmed namespace should arrive") - - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - _, message, err := conn.ReadMessage() - if err != nil { - t.Fatalf("failed to read message: %v", err) - } + done := make(chan struct{}) + defer close(done) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + for i := 0; ; i++ { + select { + case <-done: + return + case <-ticker.C: + filteredLogger.Infof("trimmed namespace should arrive %d", i) + } + } + }() - var entry logger.Entry - if err := json.Unmarshal(message, &entry); err != nil { - t.Fatalf("failed to unmarshal entry: %v", err) - } + entry := waitForWebSocketEntryWithDeadline(t, conn, 2*time.Second, func(entry logger.Entry) bool { + return entry.Namespace == "filtered-ns" + }) if entry.Namespace != "filtered-ns" { t.Fatalf("namespace = %q, want filtered-ns", entry.Namespace) } @@ -202,3 +208,21 @@ func TestLogSocketHandler_NamespaceFilter_TrimsWhitespace(t *testing.T) { t.Fatalf("output = %q, want trimmed namespace message", entry.Output) } } + +func waitForWebSocketEntryWithDeadline(t *testing.T, conn *websocket.Conn, timeout time.Duration, match func(logger.Entry) bool) logger.Entry { + t.Helper() + conn.SetReadDeadline(time.Now().Add(timeout)) + for { + _, message, err := conn.ReadMessage() + if err != nil { + t.Fatalf("failed to read message: %v", err) + } + var entry logger.Entry + if err := json.Unmarshal(message, &entry); err != nil { + t.Fatalf("failed to unmarshal entry: %v", err) + } + if match(entry) { + return entry + } + } +}