servertest: align via grant compat test with new tscap golden format

The previous tscap regeneration replaced the legacy uppercase
GRANT-V*.hujson files with lowercase via-grant-v*.hujson and switched
the topology nodes to anonymized pokémon identifiers, but
TestViaGrantMapCompat was still loading files by their old IDs and
matching topology entries by their old hostnames, so it could not even
open a single golden file.

  - viaCompatTests: GRANT-V29/V30/V31/V36 → via-grant-v29/v30/v31/v36
    so the path Join finds the renamed files on disk.

  - taggedNodes: rewrite the hardcoded servertest hostname list to the
    pokémon names tscap/anonymize writes (big-router→caterpie,
    exit-a→pidgey, …) so the topology lookup picks up the matching
    entries instead of skipping every node.

  - goldenNode: track the tscap schema. Replace advertised_routes →
    routable_ips and is_exit_node → approved_routes; the AdvertisedRoutes
    field name is kept on the Go side because the rest of the test
    code already references it.

  - convertViaPolicy: also rewrite the anonymized odin/thor/freya
    @example.com emails so the served policy resolves users when the
    file came from tscap/anonymize.

Updates #3157
This commit is contained in:
Kristoffer Dalby
2026-04-09 06:18:43 +00:00
parent c0fd9b60ed
commit 1e20972db0
+32 -17
View File
@@ -38,8 +38,8 @@ type goldenNode struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
IPv4 string `json:"ipv4"` IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"` IPv6 string `json:"ipv6"`
AdvertisedRoutes []string `json:"advertised_routes"` AdvertisedRoutes []string `json:"routable_ips"`
IsExitNode bool `json:"is_exit_node"` ApprovedRoutes []string `json:"approved_routes"`
} }
type goldenCapture struct { type goldenCapture struct {
@@ -70,10 +70,10 @@ var viaCompatTests = []struct {
id string id string
desc string desc string
}{ }{
{"GRANT-V29", "crossed subnet steering: group-a via router-a, group-b via router-b"}, {"via-grant-v29", "crossed subnet steering: group-a via router-a, group-b via router-b"},
{"GRANT-V30", "crossed mixed: subnet via router-a/b, exit via exit-b/a"}, {"via-grant-v30", "crossed mixed: subnet via router-a/b, exit via exit-b/a"},
{"GRANT-V31", "peer connectivity + via exit A/B steering"}, {"via-grant-v31", "peer connectivity + via exit A/B steering"},
{"GRANT-V36", "full complex: peer connectivity + crossed subnet + crossed exit"}, {"via-grant-v36", "full complex: peer connectivity + crossed subnet + crossed exit"},
// TODO: V33 and V35 are not yet compatible due to: // TODO: V33 and V35 are not yet compatible due to:
// - V33: PrimaryRoutes election differs (node ID ordering between SaaS and headscale) // - V33: PrimaryRoutes election differs (node ID ordering between SaaS and headscale)
// - V35: Extra peer visibility (autoApprovers create different routing topology) // - V35: Extra peer visibility (autoApprovers create different routing topology)
@@ -90,9 +90,10 @@ var viaCompatTests = []struct {
// //
// CROSS-DEPENDENCY WARNING: // CROSS-DEPENDENCY WARNING:
// This test reads golden files from ../policy/v2/testdata/grant_results/ // This test reads golden files from ../policy/v2/testdata/grant_results/
// (specifically GRANT-V29, V30, V31, V36). These files are shared with // (specifically via-grant-v29, v30, v31, v36). These files are shared
// TestGrantsCompat in the policy/v2 package. Any changes to the file // with TestGrantsCompat in the policy/v2 package. Any changes to the
// format, field structure, or naming must be coordinated with BOTH tests. // file format, field structure, or naming must be coordinated with
// BOTH tests.
// //
// Fields consumed by this test (but NOT by TestGrantsCompat): // Fields consumed by this test (but NOT by TestGrantsCompat):
// - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules) // - captures[name].netmap (Peers, AllowedIPs, PrimaryRoutes, PacketFilterRules)
@@ -131,15 +132,24 @@ func TestViaGrantMapCompat(t *testing.T) {
} }
} }
// taggedNodes are the nodes we create in the servertest. // taggedNodes are the nodes we create in the servertest. Names match
// the anonymized pokémon identifiers tscap writes into golden files —
// see github.com/kradalby/tscap/anonymize for the full substitution
// table (big-router→caterpie, exit-a→pidgey, etc).
var taggedNodes = []string{ var taggedNodes = []string{
"big-router", "caterpie", // big-router
"exit-a", "exit-b", "exit-node", "pidgey", // exit-a
"group-a-client", "group-b-client", "pidgeotto", // exit-b
"router-a", "router-b", "charmander", // exit-node
"subnet-router", "tagged-client", "rattata", // group-a-client
"tagged-server", "tagged-prod", "raticate", // group-b-client
"multi-exit-router", "spearow", // router-a
"fearow", // router-b
"squirtle", // subnet-router
"weedle", // tagged-client
"beedrill", // tagged-server
"kakuna", // tagged-prod
"blastoise", // multi-exit-router
} }
func runViaMapCompat(t *testing.T, gf goldenFile) { func runViaMapCompat(t *testing.T, gf goldenFile) {
@@ -624,6 +634,11 @@ func extractHostname(fqdn string) string {
// convertViaPolicy converts Tailscale SaaS policy emails to headscale format. // convertViaPolicy converts Tailscale SaaS policy emails to headscale format.
func convertViaPolicy(raw json.RawMessage) []byte { func convertViaPolicy(raw json.RawMessage) []byte {
s := string(raw) s := string(raw)
// Anonymized SaaS emails as written by tscap/anonymize.
s = strings.ReplaceAll(s, "odin@example.com", "tag-user@")
s = strings.ReplaceAll(s, "thor@example.com", "tag-user@")
s = strings.ReplaceAll(s, "freya@example.com", "tag-user@")
// Pre-anonymization emails (legacy captures / local reruns).
s = strings.ReplaceAll(s, "kratail2tid@passkey", "tag-user@") s = strings.ReplaceAll(s, "kratail2tid@passkey", "tag-user@")
s = strings.ReplaceAll(s, "kristoffer@dalby.cc", "tag-user@") s = strings.ReplaceAll(s, "kristoffer@dalby.cc", "tag-user@")
s = strings.ReplaceAll(s, "monitorpasskeykradalby@passkey", "tag-user@") s = strings.ReplaceAll(s, "monitorpasskeykradalby@passkey", "tag-user@")