servertest: compare user profiles in via captures

Self and peer profiles, by login and display name. A tagged self gets
its own profile in SaaS; listed as a skip entry until headscale matches.
This commit is contained in:
Kristoffer Dalby committed 2026-10-29 14:00:02 +01:00
1 parent 7d787867c9
commit 32652326b9
2 files changed
+78

No files matched your search

+76
View File
@@ -67,6 +67,13 @@ var viaCompatTests = []struct {
{"via-grant-v62", "via exit steering plus a member rule to autogroup:internet on one port"},
}
// viaCompatSkipReasons lists checks where headscale differs from SaaS,
// keyed by check alone where every capture differs the same way. A listed
// check that matches fails, so a fix must delete its entry.
var viaCompatSkipReasons = map[string]string{
"tagged-self": "TAGGED_SELF_PROFILE: SaaS gives a tagged node a profile of its own, headscale the shared tagged-devices one",
}
// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
// compares headscale's [tailcfg.MapResponse] structure against the captured [netmap.NetworkMap].
//
@@ -479,6 +486,75 @@ func compareNetmap(
}
comparePacketFilter(t, got, want, clients, saasAddrs)
compareUserProfiles(t, got, want.Netmap, clients)
}
// compareUserProfiles compares the profile the viewer holds for itself and
// for each peer in clients. Profile IDs differ between SaaS and headscale, so
// a profile is its login, with SaaS emails mapped to user names, and its
// display name.
func compareUserProfiles(
t require.TestingT,
got, want *netmap.NetworkMap,
clients map[string]*servertest.TestClient,
) {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
gotProfiles := nodeProfiles(got, clients)
for name, wantProfile := range nodeProfiles(want, clients) {
check := "peer"
if name == "" {
check = "self"
if want.SelfNode.Tags().Len() > 0 {
check = "tagged-self"
}
}
if reason, listed := viaCompatSkipReasons[check]; listed {
if gotProfiles[name] == wantProfile {
t.Errorf("node %q: %s profile matches SaaS; delete viaCompatSkipReasons[%q] (%s)",
name, check, check, reason)
}
continue
}
assert.Equalf(t, wantProfile, gotProfiles[name], "node %q: %s profile mismatch", name, check)
}
}
// nodeProfiles maps "" (self) and each peer in clients to its user profile.
func nodeProfiles(
nm *netmap.NetworkMap,
clients map[string]*servertest.TestClient,
) map[string]string {
profile := func(id tailcfg.UserID) string {
p, ok := nm.UserProfiles[id]
if !ok {
return "no profile"
}
login := p.LoginName()
if name, ok := captureUserNames[login]; ok {
login = name
}
return login + " / " + p.DisplayName()
}
out := map[string]string{"": profile(nm.SelfNode.User())}
for _, peer := range nm.Peers {
name := extractHostname(peer.Name())
if _, isOurs := clients[name]; isOurs {
out[name] = profile(peer.User())
}
}
return out
}
// comparePacketFilter compares PacketFilter rules as a set of (source,
@@ -337,6 +337,8 @@ func compareCaptureNetmap(
len(want.PacketFilterRules), gotLen,
)
}
compareUserProfiles(t, nm, want.Netmap, clients)
}
type capturePeerSummary struct {