fix(handlers/discovery): nil-guard role in filterPeersByQuery — type assertion panic on empty role (closes #730) #731

Closed
fullstack-engineer wants to merge 1 commits from fix/730-discovery-filter-nil-role into staging
2 changed files with 144 additions and 1 deletions

View File

@ -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)

View 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))
}
}