Files
meshcore-analyzer/cmd/server/scope_config_state_test.go
T
efiten 296456f9c1 feat(map): colour and filter repeaters by scope-configuration state (#2006)
Closes #2001. Two review rounds plus a re-review; findings and evidence on the PR. Verified on a deployment against live data: the field over 1653 nodes, marker tints per filter state, the marker title and popup Scope row reaching the DOM, and the colorblind-preset cascade. The audit and the map are held to the same classification by an end-to-end test that fails when either side's wildcard handling drifts.
2026-09-11 15:07:31 +02:00

105 lines
4.5 KiB
Go

package main
import "testing"
// TestNodeScopeConfigState pins the classification behind the /api/nodes
// scope_config_state field (#2001). The four declared states are the audit's
// own vocabulary; the two extra ones exist because /api/nodes has to say
// something about a repeater the audit omits entirely — one that has never
// answered a declared-regions request.
func TestNodeScopeConfigState(t *testing.T) {
tests := []struct {
name string
declaredCSV string
hasDeclared bool
transported []string
want string
}{
{"named regions and wildcard", "be,*", true, nil, ScopeConfigFull},
{"wildcard only", "*", true, nil, ScopeConfigNoScopes},
{"named regions only", "be", true, nil, ScopeConfigNoUnscoped},
{"answered with an empty list", "", true, nil, ScopeConfigNoFlood},
{"never asked, but observed carrying a scope", "", false, []string{"#be"}, ScopeConfigObserved},
{"never asked, nothing scoped observed", "", false, nil, ScopeConfigNone},
// The declared answer is the repeater's own word about its config and
// outranks what we happened to see it carry: a node that answered with
// an empty list stays no-flood even though traffic says otherwise.
// That contradiction is the audit's finding to report, not a reason to
// re-label the node here.
{"empty answer outranks observed traffic", "", true, []string{"#be"}, ScopeConfigNoFlood},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := nodeScopeConfigState(tt.declaredCSV, tt.hasDeclared, tt.transported)
if got != tt.want {
t.Errorf("nodeScopeConfigState(%q, %v, %v) = %q, want %q",
tt.declaredCSV, tt.hasDeclared, tt.transported, got, tt.want)
}
})
}
}
// TestNodeScopeConfigStateIgnoresWhitespaceAndCase pins the two shapes a
// declared list actually arrives in. regions_csv is written by two different
// collectors, and one of them spaces its entries, so " be , * " has to
// classify as full rather than as a pair of unrecognised names.
func TestNodeScopeConfigStateIgnoresWhitespaceAndCase(t *testing.T) {
if got := nodeScopeConfigState(" be , * ", true, nil); got != ScopeConfigFull {
t.Errorf("spaced list = %q, want %q", got, ScopeConfigFull)
}
if got := nodeScopeConfigState("#be", true, nil); got != ScopeConfigNoUnscoped {
t.Errorf("hash-prefixed region = %q, want %q", got, ScopeConfigNoUnscoped)
}
}
// TestNodeScopeConfigStateAcceptsHashPrefixedWildcard pins the second spelling
// of the wildcard. The ingestor treats "*" and "#*" as the same thing
// (cmd/ingestor/region_keys.go:94), so a collector storing the prefixed form
// must not have it counted as a named region — that would turn a fully
// configured repeater into "no unscoped", which reads as a fault.
func TestNodeScopeConfigStateAcceptsHashPrefixedWildcard(t *testing.T) {
if got := nodeScopeConfigState("#be,#*", true, nil); got != ScopeConfigFull {
t.Errorf("nodeScopeConfigState(\"#be,#*\") = %q, want %q", got, ScopeConfigFull)
}
if got := nodeScopeConfigState("#*", true, nil); got != ScopeConfigNoScopes {
t.Errorf("nodeScopeConfigState(\"#*\") = %q, want %q", got, ScopeConfigNoScopes)
}
}
// TestScopeConfigStateAgreesWithTheAudit is the one assertion that keeps the
// map and the Scope Audit from telling an operator two different things about
// the same repeater. Both sides classify a declared list; they must classify it
// the same way, on the same input, or one of the two pages is lying.
//
// It compares the real functions rather than two copies of the rules: the
// audit's parse loop and nodeScopeConfigState both run over the same CSV.
func TestScopeConfigStateAgreesWithTheAudit(t *testing.T) {
for _, csv := range []string{
"be,*", "*", "be", "", "#be", "#be,*", "#be,#*", "#*", "#be,#*,#nl",
" be , * ", "be-vlg,be,*",
} {
audit := auditConfigStateForCSV(csv)
api := nodeScopeConfigState(csv, true, nil)
if audit != api {
t.Errorf("regions_csv %q: scope audit says %q, /api/nodes says %q", csv, audit, api)
}
}
}
// auditConfigStateForCSV replays the audit's own declared-list parse
// (handleScopeAudit's loop over splitRegionsCSV) and classifies the result, so
// the comparison above is against what that page actually computes.
func auditConfigStateForCSV(csv string) string {
declaredWildcard := false
declaredNamed := make([]string, 0)
for _, rgn := range splitRegionsCSV(csv) {
if isScopeWildcard(rgn) {
declaredWildcard = true
continue
}
declaredNamed = append(declaredNamed, normScope(rgn))
}
return scopeAuditConfigState(declaredNamed, declaredWildcard)
}