diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 2c2cc7126..32c65f9d5 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -83,63 +83,63 @@ var handler = map[string]localAPIHandler{ // without a trailing slash: "bugreport": (*Handler).serveBugReport, "check-ip-forwarding": (*Handler).serveCheckIPForwarding, - "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding, "check-prefs": (*Handler).serveCheckPrefs, + "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding, "component-debug-logging": (*Handler).serveComponentDebugLogging, "debug": (*Handler).serveDebug, + "debug-capture": (*Handler).serveDebugCapture, "debug-derp-region": (*Handler).serveDebugDERPRegion, "debug-dial-types": (*Handler).serveDebugDialTypes, + "debug-log": (*Handler).serveDebugLog, "debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches, "debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules, - "debug-portmap": (*Handler).serveDebugPortmap, "debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges, - "debug-capture": (*Handler).serveDebugCapture, - "debug-log": (*Handler).serveDebugLog, + "debug-portmap": (*Handler).serveDebugPortmap, "derpmap": (*Handler).serveDERPMap, "dev-set-state-store": (*Handler).serveDevSetStateStore, - "set-push-device-token": (*Handler).serveSetPushDeviceToken, - "handle-push-message": (*Handler).serveHandlePushMessage, "dial": (*Handler).serveDial, + "drive/fileserver-address": (*Handler).serveDriveServerAddr, + "drive/shares": (*Handler).serveShares, "file-targets": (*Handler).serveFileTargets, "goroutines": (*Handler).serveGoroutines, + "handle-push-message": (*Handler).serveHandlePushMessage, "id-token": (*Handler).serveIDToken, "login-interactive": (*Handler).serveLoginInteractive, "logout": (*Handler).serveLogout, "logtap": (*Handler).serveLogTap, "metrics": (*Handler).serveMetrics, "ping": (*Handler).servePing, - "prefs": (*Handler).servePrefs, "pprof": (*Handler).servePprof, + "prefs": (*Handler).servePrefs, + "query-feature": (*Handler).serveQueryFeature, "reload-config": (*Handler).reloadConfig, "reset-auth": (*Handler).serveResetAuth, "serve-config": (*Handler).serveServeConfig, "set-dns": (*Handler).serveSetDNS, "set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-gui-visible": (*Handler).serveSetGUIVisible, - "drive/fileserver-address": (*Handler).serveDriveServerAddr, - "drive/shares": (*Handler).serveShares, + "set-push-device-token": (*Handler).serveSetPushDeviceToken, "start": (*Handler).serveStart, "status": (*Handler).serveStatus, + "tka/affected-sigs": (*Handler).serveTKAAffectedSigs, + "tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM, + "tka/disable": (*Handler).serveTKADisable, + "tka/force-local-disable": (*Handler).serveTKALocalDisable, + "tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM, "tka/init": (*Handler).serveTKAInit, "tka/log": (*Handler).serveTKALog, "tka/modify": (*Handler).serveTKAModify, "tka/sign": (*Handler).serveTKASign, "tka/status": (*Handler).serveTKAStatus, - "tka/disable": (*Handler).serveTKADisable, - "tka/force-local-disable": (*Handler).serveTKALocalDisable, - "tka/affected-sigs": (*Handler).serveTKAAffectedSigs, - "tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey, - "tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink, - "tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM, - "tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM, "tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM, - "upload-client-metrics": (*Handler).serveUploadClientMetrics, - "watch-ipn-bus": (*Handler).serveWatchIPNBus, - "whois": (*Handler).serveWhoIs, - "query-feature": (*Handler).serveQueryFeature, + "tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink, + "tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey, "update/check": (*Handler).serveUpdateCheck, "update/install": (*Handler).serveUpdateInstall, "update/progress": (*Handler).serveUpdateProgress, + "upload-client-metrics": (*Handler).serveUploadClientMetrics, + "watch-ipn-bus": (*Handler).serveWatchIPNBus, + "whois": (*Handler).serveWhoIs, } var ( diff --git a/ipn/localapi/localapi_test.go b/ipn/localapi/localapi_test.go index cbaf8aa63..0e97fd7b1 100644 --- a/ipn/localapi/localapi_test.go +++ b/ipn/localapi/localapi_test.go @@ -9,11 +9,18 @@ import ( "encoding/json" "errors" "fmt" + "go/ast" + "go/parser" + "go/token" "io" + "log" "net/http" "net/http/httptest" "net/netip" "net/url" + "os" + "slices" + "strconv" "strings" "testing" @@ -26,6 +33,7 @@ import ( "tailscale.com/tstest" "tailscale.com/types/logger" "tailscale.com/types/logid" + "tailscale.com/util/slicesx" "tailscale.com/wgengine" ) @@ -318,3 +326,67 @@ func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend { } return lb } + +func TestKeepItSorted(t *testing.T) { + // Parse the localapi.go file into an AST. + fset := token.NewFileSet() // positions are relative to fset + src, err := os.ReadFile("localapi.go") + if err != nil { + log.Fatal(err) + } + f, err := parser.ParseFile(fset, "localapi.go", src, 0) + if err != nil { + log.Fatal(err) + } + getHandler := func() *ast.ValueSpec { + for _, d := range f.Decls { + if g, ok := d.(*ast.GenDecl); ok && g.Tok == token.VAR { + for _, s := range g.Specs { + if vs, ok := s.(*ast.ValueSpec); ok { + if len(vs.Names) == 1 && vs.Names[0].Name == "handler" { + return vs + } + } + } + } + } + return nil + } + keys := func() (ret []string) { + h := getHandler() + if h == nil { + t.Fatal("no handler var found") + } + cl, ok := h.Values[0].(*ast.CompositeLit) + if !ok { + t.Fatalf("handler[0] is %T, want *ast.CompositeLit", h.Values[0]) + } + for _, e := range cl.Elts { + kv := e.(*ast.KeyValueExpr) + strLt := kv.Key.(*ast.BasicLit) + if strLt.Kind != token.STRING { + t.Fatalf("got: %T, %q", kv.Key, kv.Key) + } + k, err := strconv.Unquote(strLt.Value) + if err != nil { + t.Fatalf("unquote: %v", err) + } + ret = append(ret, k) + } + return + } + gotKeys := keys() + endSlash, noSlash := slicesx.Partition(keys(), func(s string) bool { return strings.HasSuffix(s, "/") }) + if !slices.IsSorted(endSlash) { + t.Errorf("the items ending in a slash aren't sorted") + } + if !slices.IsSorted(noSlash) { + t.Errorf("the items ending in a slash aren't sorted") + } + if !t.Failed() { + want := append(endSlash, noSlash...) + if !slices.Equal(gotKeys, want) { + t.Errorf("items with trailing slashes should precede those without") + } + } +}