fix(handlers/discovery): nil-guard role in filterPeersByQuery — type assertion panic on empty role (closes #730) #731
@ -293,7 +293,10 @@ func filterPeersByQuery(peers []map[string]interface{}, q string) []map[string]i
|
||||
out := make([]map[string]interface{}, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
name := p["name"].(string)
|
||||
role := p["role"].(string)
|
||||
role := ""
|
||||
if r := p["role"]; r != nil {
|
||||
role = r.(string)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(name), needle) ||
|
||||
strings.Contains(strings.ToLower(role), needle) {
|
||||
out = append(out, p)
|
||||
|
||||
140
workspace-server/internal/handlers/discovery_filter_test.go
Normal file
140
workspace-server/internal/handlers/discovery_filter_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package handlers
|
||||
|
||||
import "testing"
|
||||
|
||||
// Tests for filterPeersByQuery — the pure peer-filter helper used by
|
||||
// GET /registry/:id/peers?q=... query param. The function is exercised
|
||||
// via the Peers handler integration tests (discovery_test.go), but the
|
||||
// pure unit cases here give tighter fault-localisation.
|
||||
|
||||
func makePeer(name, role string) map[string]interface{} {
|
||||
m := map[string]interface{}{"name": name}
|
||||
if role != "" {
|
||||
m["role"] = role
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// filterPeersByQuery — 7 cases covering nil-role panic regression,
|
||||
// empty-q pass-through, name-only match, role-only match, both-match,
|
||||
// case-insensitivity, and non-ASCII.
|
||||
|
||||
func TestFilterPeersByQuery_EmptyQReturnsAll(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", "admin"),
|
||||
makePeer("Gamma", ""),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "")
|
||||
if len(got) != 3 {
|
||||
t.Errorf("empty q: got %d, want 3", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_WhitespaceOnlyQReturnsAll(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", "admin"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, " ")
|
||||
if len(got) != 2 {
|
||||
t.Errorf("whitespace q: got %d, want 2", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_NameMatch(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", "admin"),
|
||||
makePeer("Gamma", "agent"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "lph") // matches "Alpha"
|
||||
if len(got) != 1 || got[0]["name"] != "Alpha" {
|
||||
t.Errorf("name match: got %v, want [Alpha]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_RoleMatch(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", "admin"),
|
||||
makePeer("Gamma", "agent"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "admin") // matches "Beta"
|
||||
if len(got) != 1 || got[0]["name"] != "Beta" {
|
||||
t.Errorf("role match: got %v, want [Beta]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_RoleMatchWithNilRole(t *testing.T) {
|
||||
// Regression: role key is absent/nil when workspace has no role.
|
||||
// Previously p["role"].(string) panicked on nil.
|
||||
// Now it returns "" and nil-role peers are skipped for role queries.
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", ""), // role absent → nil
|
||||
makePeer("Gamma", "admin"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "admin")
|
||||
if len(got) != 1 || got[0]["name"] != "Gamma" {
|
||||
t.Errorf("nil-role peer excluded from role query: got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_NilRoleDoesNotPanic(t *testing.T) {
|
||||
// The nil panic is the primary regression this test guards.
|
||||
// A nil role peer should be silently skipped for role-filtering.
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", ""), // nil role
|
||||
makePeer("Beta", "admin"),
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("filterPeersByQuery panicked on nil role: %v", r)
|
||||
}
|
||||
}()
|
||||
got := filterPeersByQuery(peers, "al")
|
||||
// "al" matches "Alpha" by name (not role), so Alpha should be returned.
|
||||
if len(got) != 1 || got[0]["name"] != "Alpha" {
|
||||
t.Errorf("nil-role peer included by name match: got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_CaseInsensitive(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "AGENT"),
|
||||
makePeer("Beta", "admin"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "ALPHA")
|
||||
if len(got) != 1 || got[0]["name"] != "Alpha" {
|
||||
t.Errorf("case-insensitive name: got %v, want [Alpha]", got)
|
||||
}
|
||||
|
||||
got2 := filterPeersByQuery(peers, "AGENT")
|
||||
if len(got2) != 1 || got2[0]["name"] != "Alpha" {
|
||||
t.Errorf("case-insensitive role: got %v, want [Alpha]", got2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_NoMatch(t *testing.T) {
|
||||
peers := []map[string]interface{}{
|
||||
makePeer("Alpha", "agent"),
|
||||
makePeer("Beta", "admin"),
|
||||
}
|
||||
got := filterPeersByQuery(peers, "xyz")
|
||||
if len(got) != 0 {
|
||||
t.Errorf("no match: got %d, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPeersByQuery_EmptyPeersList(t *testing.T) {
|
||||
got := filterPeersByQuery(nil, "al")
|
||||
if len(got) != 0 {
|
||||
t.Errorf("nil peers: got %d, want 0", len(got))
|
||||
}
|
||||
|
||||
got2 := filterPeersByQuery([]map[string]interface{}{}, "al")
|
||||
if len(got2) != 0 {
|
||||
t.Errorf("empty peers: got %d, want 0", len(got2))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user