cmd/vet: add subtestnames analyzer; fix all existing violations

Add a new vet analyzer that checks t.Run subtest names don't contain
characters requiring quoting when re-running via "go test -run". This
enforces the style guide rule: don't use spaces or punctuation in
subtest names.

The analyzer flags:
- Direct t.Run calls with string literal names containing spaces,
  regex metacharacters, quotes, or other problematic characters
- Table-driven t.Run(tt.name, ...) calls where tt ranges over a
  slice/map literal with bad name field values

Also fix all 978 existing violations across 81 test files, replacing
spaces with hyphens and shortening long sentence-like names to concise
hyphenated forms.

Updates #19242

Change-Id: Ib0ad96a111bd8e764582d1d4902fe2599454ab65
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-04 21:32:14 +00:00
committed by Brad Fitzpatrick
parent 0f02c20c5e
commit 5ef3713c9f
87 changed files with 1405 additions and 982 deletions
+9 -9
View File
@@ -31,7 +31,7 @@ func TestSetupKube(t *testing.T) {
kc *kubeClient
}{
{
name: "TS_AUTHKEY set, state Secret exists",
name: "authkey-set-secret-exists",
cfg: &settings{
AuthKey: "foo",
KubeSecret: "foo",
@@ -50,7 +50,7 @@ func TestSetupKube(t *testing.T) {
},
},
{
name: "TS_AUTHKEY set, state Secret does not exist, we have permissions to create it",
name: "authkey-set-secret-missing-can-create",
cfg: &settings{
AuthKey: "foo",
KubeSecret: "foo",
@@ -69,7 +69,7 @@ func TestSetupKube(t *testing.T) {
},
},
{
name: "TS_AUTHKEY set, state Secret does not exist, we do not have permissions to create it",
name: "authkey-set-secret-missing-cannot-create",
cfg: &settings{
AuthKey: "foo",
KubeSecret: "foo",
@@ -89,7 +89,7 @@ func TestSetupKube(t *testing.T) {
wantErr: true,
},
{
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to retrieve the state Secret",
name: "authkey-set-get-secret-non-404-error",
cfg: &settings{
AuthKey: "foo",
KubeSecret: "foo",
@@ -109,7 +109,7 @@ func TestSetupKube(t *testing.T) {
wantErr: true,
},
{
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to check Secret permissions",
name: "authkey-set-check-perms-error",
cfg: &settings{
AuthKey: "foo",
KubeSecret: "foo",
@@ -127,7 +127,7 @@ func TestSetupKube(t *testing.T) {
},
{
// Interactive login using URL in Pod logs
name: "TS_AUTHKEY not set, state Secret does not exist, we have permissions to create it",
name: "no-authkey-secret-missing-can-create",
cfg: &settings{
KubeSecret: "foo",
},
@@ -145,7 +145,7 @@ func TestSetupKube(t *testing.T) {
},
{
// Interactive login using URL in Pod logs
name: "TS_AUTHKEY not set, state Secret exists, but does not contain auth key",
name: "no-authkey-secret-exists-no-key",
cfg: &settings{
KubeSecret: "foo",
},
@@ -162,7 +162,7 @@ func TestSetupKube(t *testing.T) {
}},
},
{
name: "TS_AUTHKEY not set, state Secret contains auth key, we do not have RBAC to patch it",
name: "no-authkey-secret-has-key-cannot-patch",
cfg: &settings{
KubeSecret: "foo",
},
@@ -180,7 +180,7 @@ func TestSetupKube(t *testing.T) {
wantErr: true,
},
{
name: "TS_AUTHKEY not set, state Secret contains auth key, we have RBAC to patch it",
name: "no-authkey-secret-has-key-can-patch",
cfg: &settings{
KubeSecret: "foo",
},
+18 -18
View File
@@ -89,24 +89,24 @@ type settings struct {
func configFromEnv() (*settings, error) {
cfg := &settings{
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
ClientID: defaultEnv("TS_CLIENT_ID", ""),
ClientSecret: defaultEnv("TS_CLIENT_SECRET", ""),
IDToken: defaultEnv("TS_ID_TOKEN", ""),
Audience: defaultEnv("TS_AUDIENCE", ""),
Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnvStringPointer("TS_ROUTES"),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
ProxyTargetIP: defaultEnv("TS_DEST_IP", ""),
ProxyTargetDNSName: defaultEnv("TS_EXPERIMENTAL_DEST_DNS_NAME", ""),
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
UserspaceMode: defaultBool("TS_USERSPACE", true),
StateDir: defaultEnv("TS_STATE_DIR", ""),
AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
ClientID: defaultEnv("TS_CLIENT_ID", ""),
ClientSecret: defaultEnv("TS_CLIENT_SECRET", ""),
IDToken: defaultEnv("TS_ID_TOKEN", ""),
Audience: defaultEnv("TS_AUDIENCE", ""),
Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnvStringPointer("TS_ROUTES"),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
ProxyTargetIP: defaultEnv("TS_DEST_IP", ""),
ProxyTargetDNSName: defaultEnv("TS_EXPERIMENTAL_DEST_DNS_NAME", ""),
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
UserspaceMode: defaultBool("TS_USERSPACE", true),
StateDir: defaultEnv("TS_STATE_DIR", ""),
AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
KubeSecret: func() string {
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
return defaultEnv("TS_KUBE_SECRET", "tailscale")
+6 -6
View File
@@ -46,30 +46,30 @@ func TestNoContent(t *testing.T) {
want string
}{
{
name: "no challenge",
name: "no-challenge",
},
{
name: "valid challenge",
name: "valid-challenge",
input: "input",
want: "response input",
},
{
name: "valid challenge hostname",
name: "valid-challenge-hostname",
input: "ts_derp99b.tailscale.com",
want: "response ts_derp99b.tailscale.com",
},
{
name: "invalid challenge",
name: "invalid-challenge",
input: "foo\x00bar",
want: "",
},
{
name: "whitespace invalid challenge",
name: "whitespace-invalid-challenge",
input: "foo bar",
want: "",
},
{
name: "long challenge",
name: "long-challenge",
input: strings.Repeat("x", 65),
want: "",
},
+2 -2
View File
@@ -30,7 +30,7 @@ func TestEmbeddedTypeUnmarshal(t *testing.T) {
},
}
t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
t.Run("unmarshal-gitops-from-acl", func(t *testing.T) {
b, _ := json.Marshal(aclTestErr)
var e ACLGitopsTestError
err := json.Unmarshal(b, &e)
@@ -41,7 +41,7 @@ func TestEmbeddedTypeUnmarshal(t *testing.T) {
t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
}
})
t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
t.Run("unmarshal-acl-from-gitops", func(t *testing.T) {
b, _ := json.Marshal(gitopsErr)
var e tailscale.ACLTestError
err := json.Unmarshal(b, &e)
+15 -15
View File
@@ -24,7 +24,7 @@ func TestNameserver(t *testing.T) {
wantResp *dns.Msg
}{
{
name: "A record query, record exists",
name: "A-record-exists",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeA}},
@@ -46,7 +46,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "A record query, record does not exist",
name: "A-record-not-exists",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeA}},
@@ -64,7 +64,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "A record query, but the name is not a valid FQDN",
name: "A-record-invalid-FQDN",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "foo..bar.com", Qtype: dns.TypeA}},
@@ -80,7 +80,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "AAAA record query, A record exists",
name: "AAAA-query-A-record-exists",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
@@ -97,7 +97,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "AAAA record query, A record does not exist",
name: "AAAA-query-A-record-not-exists",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "baz.bar.com", Qtype: dns.TypeAAAA}},
@@ -114,7 +114,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "AAAA record query with IPv6 record",
name: "AAAA-query-ipv6-record",
ip6: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {net.ParseIP("2001:db8::1")}},
query: &dns.Msg{
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeAAAA}},
@@ -136,7 +136,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "Dual-stack: both A and AAAA records exist",
name: "dual-stack-A-and-AAAA",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("dual.bar.com."): {{10, 0, 0, 1}}},
ip6: map[dnsname.FQDN][]net.IP{dnsname.FQDN("dual.bar.com."): {net.ParseIP("2001:db8::1")}},
query: &dns.Msg{
@@ -157,7 +157,7 @@ func TestNameserver(t *testing.T) {
}},
},
{
name: "CNAME record query",
name: "CNAME-query",
ip4: map[dnsname.FQDN][]net.IP{dnsname.FQDN("foo.bar.com."): {{1, 2, 3, 4}}},
query: &dns.Msg{
Question: []dns.Question{{Name: "foo.bar.com", Qtype: dns.TypeCNAME}},
@@ -200,20 +200,20 @@ func TestResetRecords(t *testing.T) {
wantsErr bool
}{
{
name: "previously empty nameserver.ip4 gets set",
name: "previously-empty-nameserver-ip4-gets-set",
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
wantsIp6: make(map[dnsname.FQDN][]net.IP),
},
{
name: "nameserver.ip4 gets reset",
name: "nameserver-ip4-gets-reset",
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
config: []byte(`{"version": "v1alpha1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
wantsIp4: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {{1, 2, 3, 4}}},
wantsIp6: make(map[dnsname.FQDN][]net.IP),
},
{
name: "configuration with incompatible version",
name: "configuration-with-incompatible-version",
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
config: []byte(`{"version": "v1beta1", "ip4": {"foo.bar.com": ["1.2.3.4"]}}`),
wantsIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
@@ -221,26 +221,26 @@ func TestResetRecords(t *testing.T) {
wantsErr: true,
},
{
name: "nameserver.ip4 gets reset to empty config when no configuration is provided",
name: "nameserver-ip4-gets-reset-to-empty-config-when-no-configuration-is-provided",
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
wantsIp4: make(map[dnsname.FQDN][]net.IP),
wantsIp6: make(map[dnsname.FQDN][]net.IP),
},
{
name: "nameserver.ip4 gets reset to empty config when the provided configuration is empty",
name: "nameserver-ip4-gets-reset-to-empty-config-when-the-provided-configuration-is-empty",
hasIp4: map[dnsname.FQDN][]net.IP{"baz.bar.com.": {{1, 1, 3, 3}}},
config: []byte(`{"version": "v1alpha1", "ip4": {}}`),
wantsIp4: make(map[dnsname.FQDN][]net.IP),
wantsIp6: make(map[dnsname.FQDN][]net.IP),
},
{
name: "nameserver.ip6 gets set",
name: "nameserver-ip6-gets-set",
config: []byte(`{"version": "v1alpha1", "ip6": {"foo.bar.com": ["2001:db8::1"]}}`),
wantsIp4: make(map[dnsname.FQDN][]net.IP),
wantsIp6: map[dnsname.FQDN][]net.IP{"foo.bar.com.": {net.ParseIP("2001:db8::1")}},
},
{
name: "dual-stack configuration",
name: "dual-stack-configuration",
config: []byte(`{"version": "v1alpha1", "ip4": {"dual.bar.com": ["10.0.0.1"]}, "ip6": {"dual.bar.com": ["2001:db8::1"]}}`),
wantsIp4: map[dnsname.FQDN][]net.IP{"dual.bar.com.": {{10, 0, 0, 1}}},
wantsIp6: map[dnsname.FQDN][]net.IP{"dual.bar.com.": {net.ParseIP("2001:db8::1")}},
+6 -6
View File
@@ -80,7 +80,7 @@ func TestNameserverReconciler(t *testing.T) {
nameserverLabels := nameserverResourceLabels(dnsConfig.Name, tsNamespace)
wantsDeploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: tsNamespace}, TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: appsv1.SchemeGroupVersion.Identifier()}}
t.Run("deployment has expected fields", func(t *testing.T) {
t.Run("deployment-expected-fields", func(t *testing.T) {
if err = yaml.Unmarshal(deployYaml, wantsDeploy); err != nil {
t.Fatalf("unmarshalling yaml: %v", err)
}
@@ -102,7 +102,7 @@ func TestNameserverReconciler(t *testing.T) {
})
wantsSvc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: tsNamespace}, TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: corev1.SchemeGroupVersion.Identifier()}}
t.Run("service has expected fields", func(t *testing.T) {
t.Run("service-expected-fields", func(t *testing.T) {
if err = yaml.Unmarshal(svcYaml, wantsSvc); err != nil {
t.Fatalf("unmarshalling yaml: %v", err)
}
@@ -113,7 +113,7 @@ func TestNameserverReconciler(t *testing.T) {
expectEqual(t, fc, wantsSvc)
})
t.Run("dns config status is set", func(t *testing.T) {
t.Run("dns-config-status-set", func(t *testing.T) {
// Verify that DNSConfig advertizes the nameserver's Service IP address,
// has the ready status condition and tailscale finalizer.
mustUpdate(t, fc, "tailscale", "nameserver", func(svc *corev1.Service) {
@@ -136,7 +136,7 @@ func TestNameserverReconciler(t *testing.T) {
expectEqual(t, fc, dnsConfig)
})
t.Run("nameserver image can be updated", func(t *testing.T) {
t.Run("nameserver-image-updated", func(t *testing.T) {
// Verify that nameserver image gets updated to match DNSConfig spec.
mustUpdate(t, fc, "", "test", func(dnsCfg *tsapi.DNSConfig) {
dnsCfg.Spec.Nameserver.Image.Tag = "v0.0.2"
@@ -146,7 +146,7 @@ func TestNameserverReconciler(t *testing.T) {
expectEqual(t, fc, wantsDeploy)
})
t.Run("reconciler does not overwrite custom configuration", func(t *testing.T) {
t.Run("reconciler-preserves-custom-config", func(t *testing.T) {
// Verify that when another actor sets ConfigMap data, it does not get
// overwritten by nameserver reconciler.
dnsRecords := &operatorutils.Records{Version: "v1alpha1", IP4: map[string][]string{"foo.ts.net": {"1.2.3.4"}}}
@@ -175,7 +175,7 @@ func TestNameserverReconciler(t *testing.T) {
expectEqual(t, fc, wantCm)
})
t.Run("uses default nameserver image", func(t *testing.T) {
t.Run("uses-default-nameserver-image", func(t *testing.T) {
// Verify that if dnsconfig.spec.nameserver.image.{repo,tag} are unset,
// the nameserver image defaults to tailscale/k8s-nameserver:unstable.
mustUpdate(t, fc, "", "test", func(dnsCfg *tsapi.DNSConfig) {
+11 -7
View File
@@ -1498,24 +1498,28 @@ func TestProxyFirewallMode(t *testing.T) {
func Test_isMagicDNSName(t *testing.T) {
tests := []struct {
name string
in string
want bool
}{
{
name: "foo-tail4567-ts-net",
in: "foo.tail4567.ts.net",
want: true,
},
{
name: "foo-tail4567-ts-net-trailing-dot",
in: "foo.tail4567.ts.net.",
want: true,
},
{
name: "foo-tail4567",
in: "foo.tail4567",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
if got := isMagicDNSName(tt.in); got != tt.want {
t.Errorf("isMagicDNSName(%q) = %v, want %v", tt.in, got, tt.want)
}
@@ -1756,7 +1760,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want string
}{
{
name: "success- custom domain",
name: "success-custom-domain",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "department.org.io")},
},
@@ -1764,7 +1768,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want: "department.org.io",
},
{
name: "success- default domain",
name: "success-default-domain",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.cluster.local."), toFQDN(t, "svc.cluster.local."), toFQDN(t, "cluster.local.")},
},
@@ -1772,7 +1776,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want: "cluster.local",
},
{
name: "only two search domains found",
name: "only-two-search-domains",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "svc.department.org.io"), toFQDN(t, "department.org.io")},
},
@@ -1780,7 +1784,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want: "cluster.local",
},
{
name: "first search domain does not match the expected structure",
name: "first-search-domain-mismatch",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.bar.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "some.other.fqdn")},
},
@@ -1788,7 +1792,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want: "cluster.local",
},
{
name: "second search domain does not match the expected structure",
name: "second-search-domain-mismatch",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "foo.department.org.io"), toFQDN(t, "some.other.fqdn")},
},
@@ -1796,7 +1800,7 @@ func Test_clusterDomainFromResolverConf(t *testing.T) {
want: "cluster.local",
},
{
name: "third search domain does not match the expected structure",
name: "third-search-domain-mismatch",
conf: &resolvconffile.Config{
SearchDomains: []dnsname.FQDN{toFQDN(t, "foo.svc.department.org.io"), toFQDN(t, "svc.department.org.io"), toFQDN(t, "some.other.fqdn")},
},
+12 -12
View File
@@ -324,76 +324,76 @@ func Test_mergeStatefulSetLabelsOrAnnots(t *testing.T) {
want map[string]string
}{
{
name: "no custom labels specified and none present in current labels, return current labels",
name: "no-custom-labels-none-present",
current: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
want: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
managed: tailscaleManagedLabels,
},
{
name: "no custom labels specified, but some present in current labels, return tailscale managed labels only from the current labels",
name: "no-custom-labels-some-present",
current: map[string]string{"foo": "bar", "something.io/foo": "bar", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
want: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
managed: tailscaleManagedLabels,
},
{
name: "custom labels specified, current labels only contain tailscale managed labels, return a union of both",
name: "custom-labels-with-managed-only",
current: map[string]string{kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
managed: tailscaleManagedLabels,
},
{
name: "custom labels specified, current labels contain tailscale managed labels and custom labels, some of which re not present in the new custom labels, return a union of managed labels and the desired custom labels",
name: "custom-labels-stale-removed",
current: map[string]string{"foo": "bar", "bar": "baz", "app": "1234", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar", "app": "1234", kubetypes.LabelManaged: "true", LabelParentName: "foo", LabelParentType: "svc", LabelParentNamespace: "foo"},
managed: tailscaleManagedLabels,
},
{
name: "no current labels present, return custom labels only",
name: "no-current-labels-return-custom",
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar"},
managed: tailscaleManagedLabels,
},
{
name: "no current labels present, no custom labels specified, return empty map",
name: "no-current-no-custom-return-empty",
want: map[string]string{},
managed: tailscaleManagedLabels,
},
{
name: "no custom annots specified and none present in current annots, return current annots",
name: "no-custom-annots-none-present",
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "no custom annots specified, but some present in current annots, return tailscale managed annots only from the current annots",
name: "no-custom-annots-some-present",
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "custom annots specified, current annots only contain tailscale managed annots, return a union of both",
name: "custom-annots-with-managed-only",
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "custom annots specified, current annots contain tailscale managed annots and custom annots, some of which are not present in the new custom annots, return a union of managed annots and the desired custom annots",
name: "custom-annots-stale-removed",
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
custom: map[string]string{"something.io/foo": "bar"},
want: map[string]string{"something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "no current annots present, return custom annots only",
name: "no-current-annots-return-custom",
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar"},
managed: tailscaleManagedAnnotations,
},
{
name: "no current labels present, no custom labels specified, return empty map",
name: "no-current-annots-no-custom-return-empty",
want: map[string]string{},
managed: tailscaleManagedAnnotations,
},
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
func TestRecorderSpecs(t *testing.T) {
t.Run("ensure spec fields are passed through correctly", func(t *testing.T) {
t.Run("spec-fields-passthrough", func(t *testing.T) {
tsr := &tsapi.Recorder{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
+4 -4
View File
@@ -1533,13 +1533,13 @@ func TestParseNLArgs(t *testing.T) {
parseDisablements: true,
},
{
name: "key no votes",
name: "key-no-votes",
input: []string{"nlpub:" + strings.Repeat("00", 32)},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
},
{
name: "key with votes",
name: "key-with-votes",
input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
@@ -1551,13 +1551,13 @@ func TestParseNLArgs(t *testing.T) {
wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
},
{
name: "disablements not allowed",
name: "disablements-not-allowed",
input: []string{"disablement:" + strings.Repeat("02", 32)},
parseKeys: true,
wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix tlpub:"),
},
{
name: "keys not allowed",
name: "keys-not-allowed",
input: []string{"nlpub:" + strings.Repeat("02", 32)},
parseDisablements: true,
wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
+1 -1
View File
@@ -76,7 +76,7 @@ users:
token: unused`,
},
{
name: "all configs, clusters, users have been deleted",
name: "all-configs-clusters-users-deleted",
in: `apiVersion: v1
clusters: null
contexts: null
@@ -30,7 +30,7 @@ func Test_listCerts(t *testing.T) {
wantErr bool
}{
{
name: "normal response",
name: "normal-response",
caller: fakeAPICaller{
Data: json.RawMessage(`{
"certificates" : [
@@ -117,12 +117,12 @@ func Test_listCerts(t *testing.T) {
},
},
{
name: "call error",
name: "call-error",
caller: fakeAPICaller{nil, fmt.Errorf("caller failed")},
wantErr: true,
},
{
name: "payload decode error",
name: "payload-decode-error",
caller: fakeAPICaller{json.RawMessage("This isn't JSON!"), nil},
wantErr: true,
},
+2 -2
View File
@@ -14,7 +14,7 @@ import (
)
func TestFilterFormatAndSortExitNodes(t *testing.T) {
t.Run("without filter", func(t *testing.T) {
t.Run("without-filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "everest-1",
@@ -139,7 +139,7 @@ func TestFilterFormatAndSortExitNodes(t *testing.T) {
}
})
t.Run("with country filter", func(t *testing.T) {
t.Run("with-country-filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "baker-1",
+45 -44
View File
@@ -1056,49 +1056,49 @@ func TestSrcTypeFromFlags(t *testing.T) {
expectedErr bool
}{
{
name: "only http set",
name: "only-http-set",
env: &serveEnv{http: 80},
expectedType: serveTypeHTTP,
expectedPort: 80,
expectedErr: false,
},
{
name: "only https set",
name: "only-https-set",
env: &serveEnv{https: 10000},
expectedType: serveTypeHTTPS,
expectedPort: 10000,
expectedErr: false,
},
{
name: "only tcp set",
name: "only-tcp-set",
env: &serveEnv{tcp: 8000},
expectedType: serveTypeTCP,
expectedPort: 8000,
expectedErr: false,
},
{
name: "only tls-terminated-tcp set",
name: "only-tls-terminated-tcp-set",
env: &serveEnv{tlsTerminatedTCP: 8080},
expectedType: serveTypeTLSTerminatedTCP,
expectedPort: 8080,
expectedErr: false,
},
{
name: "defaults to https, port 443",
name: "defaults-to-https-443",
env: &serveEnv{},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
},
{
name: "defaults to https, port 443 for service",
name: "defaults-to-https-443-for-service",
env: &serveEnv{service: "svc:foo"},
expectedType: serveTypeHTTPS,
expectedPort: 443,
expectedErr: false,
},
{
name: "multiple types set",
name: "multiple-types-set",
env: &serveEnv{http: 80, https: 443},
expectedPort: 0,
expectedErr: true,
@@ -1235,19 +1235,20 @@ func TestAcceptSetAppCapsFlag(t *testing.T) {
func TestCleanURLPath(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{input: "", expected: "/"},
{input: "/", expected: "/"},
{input: "/foo", expected: "/foo"},
{input: "/foo/", expected: "/foo/"},
{input: "/../bar", wantErr: true},
{name: "empty", input: "", expected: "/"},
{name: "slash", input: "/", expected: "/"},
{name: "foo", input: "/foo", expected: "/foo"},
{name: "foo-trailing-slash", input: "/foo/", expected: "/foo/"},
{name: "dotdot-bar", input: "/../bar", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
actual, err := cleanURLPath(tt.input)
if tt.wantErr == true && err == nil {
@@ -1275,18 +1276,18 @@ func TestAddServiceToPrefs(t *testing.T) {
expected []string
}{
{
name: "add service to empty prefs",
name: "add-service-to-empty-prefs",
svcName: "svc:foo",
expected: []string{"svc:foo"},
},
{
name: "add service to existing prefs",
name: "add-service-to-existing-prefs",
svcName: "svc:bar",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo", "svc:bar"},
},
{
name: "add existing service to prefs",
name: "add-existing-service-to-prefs",
svcName: "svc:foo",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo"},
@@ -1323,18 +1324,18 @@ func TestRemoveServiceFromPrefs(t *testing.T) {
expected []string
}{
{
name: "remove service from empty prefs",
name: "remove-service-from-empty-prefs",
svcName: "svc:foo",
expected: []string{},
},
{
name: "remove existing service from prefs",
name: "remove-existing-service-from-prefs",
svcName: "svc:foo",
startServices: []string{"svc:foo"},
expected: []string{},
},
{
name: "remove service not in prefs",
name: "remove-service-not-in-prefs",
svcName: "svc:bar",
startServices: []string{"svc:foo"},
expected: []string{"svc:foo"},
@@ -1446,7 +1447,7 @@ func TestMessageForPort(t *testing.T) {
}, "\n"),
},
{
name: "serve service http",
name: "serve-service-http",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1490,7 +1491,7 @@ func TestMessageForPort(t *testing.T) {
}, "\n"),
},
{
name: "serve service no capmap",
name: "serve-service-no-capmap",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1534,7 +1535,7 @@ func TestMessageForPort(t *testing.T) {
}, "\n"),
},
{
name: "serve service https non-default port",
name: "serve-service-https-non-default-port",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1576,7 +1577,7 @@ func TestMessageForPort(t *testing.T) {
}, "\n"),
},
{
name: "serve service TCPForward",
name: "serve-service-TCPForward",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1613,7 +1614,7 @@ func TestMessageForPort(t *testing.T) {
}, "\n"),
},
{
name: "serve service Tun",
name: "serve-service-Tun",
subcmd: serve,
serveConfig: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1790,7 +1791,7 @@ func TestSetServe(t *testing.T) {
expectErr bool
}{
{
name: "add new handler",
name: "add-new-handler",
desc: "add a new http handler to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "foo.test.ts.net",
@@ -1810,7 +1811,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "update http handler",
name: "update-http-handler",
desc: "update an existing http handler on the same port to same type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
@@ -1839,7 +1840,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "update TCP handler",
name: "update-TCP-handler",
desc: "update an existing TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {TCPForward: "http://localhost:3000"}},
@@ -1852,7 +1853,7 @@ func TestSetServe(t *testing.T) {
expectErr: true,
},
{
name: "add new service handler",
name: "add-new-service-handler",
desc: "add a new service TCP handler to empty config",
cfg: &ipn.ServeConfig{},
@@ -1869,7 +1870,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "update service handler",
name: "update-service-handler",
desc: "update an existing service TCP handler on the same port to same type",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1891,7 +1892,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "update service handler",
name: "update-service-handler",
desc: "update an existing service TCP handler on the same port to a http handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1908,7 +1909,7 @@ func TestSetServe(t *testing.T) {
expectErr: true,
},
{
name: "add new service handler",
name: "add-new-service-handler",
desc: "add a new service HTTP handler to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "svc:bar",
@@ -1932,7 +1933,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "update existing service handler",
name: "update-existing-service-handler",
desc: "update an existing service HTTP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -1969,7 +1970,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "add new service handler",
name: "add-new-service-handler",
desc: "add a new service HTTP handler to existing service config",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2014,7 +2015,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "add new service mount",
name: "add-new-service-mount",
desc: "add a new service mount to existing service config",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2054,7 +2055,7 @@ func TestSetServe(t *testing.T) {
},
},
{
name: "add new service handler",
name: "add-new-service-handler",
desc: "add a new service handler in tun mode to empty config",
cfg: &ipn.ServeConfig{},
dnsName: "svc:bar",
@@ -2103,7 +2104,7 @@ func TestUnsetServe(t *testing.T) {
expectErr bool
}{
{
name: "unset http handler",
name: "unset-http-handler",
desc: "remove an existing http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
@@ -2128,7 +2129,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: false,
},
{
name: "unset service handler",
name: "unset-service-handler",
desc: "remove an existing service TCP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2157,7 +2158,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: false,
},
{
name: "unset service handler tun",
name: "unset-service-handler-tun",
desc: "remove an existing service handler in tun mode",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2175,7 +2176,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: false,
},
{
name: "unset service handler tcp",
name: "unset-service-handler-tcp",
desc: "remove an existing service TCP handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2196,7 +2197,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: false,
},
{
name: "unset http handler not found",
name: "unset-http-handler-not-found",
desc: "try to remove a non-existing http handler",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
@@ -2221,7 +2222,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: true,
},
{
name: "unset service handler not found",
name: "unset-service-handler-not-found",
desc: "try to remove a non-existing service TCP handler",
cfg: &ipn.ServeConfig{
@@ -2253,7 +2254,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: true,
},
{
name: "unset service doesn't exist",
name: "unset-service-doesnt-exist",
desc: "try to remove a non-existing service's handler",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
@@ -2273,7 +2274,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: true,
},
{
name: "unset tcp while port is in use",
name: "unset-tcp-while-port-in-use",
desc: "try to remove a TCP handler while the port is used for web",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
@@ -2297,7 +2298,7 @@ func TestUnsetServe(t *testing.T) {
expectErr: true,
},
{
name: "unset service tcp while port is in use",
name: "unset-service-tcp-while-port-in-use",
desc: "try to remove a service TCP handler while the port is used for web",
cfg: &ipn.ServeConfig{
Services: map[tailcfg.ServiceName]*ipn.ServiceConfig{
+2 -2
View File
@@ -58,7 +58,7 @@ func TestStateStoreError(t *testing.T) {
args.statedir = t.TempDir()
args.tunname = "userspace-networking"
t.Run("new state", func(t *testing.T) {
t.Run("new-state", func(t *testing.T) {
sys := tsd.NewSystem()
sys.NetMon.Set(must.Get(netmon.New(sys.Bus.Get(), t.Logf)))
lb, err := getLocalBackend(t.Context(), t.Logf, logID.Public(), sys)
@@ -70,7 +70,7 @@ func TestStateStoreError(t *testing.T) {
t.Errorf("StateStoreHealth is unhealthy on fresh LocalBackend:\n%s", strings.Join(lb.HealthTracker().Strings(), "\n"))
}
})
t.Run("corrupt state", func(t *testing.T) {
t.Run("corrupt-state", func(t *testing.T) {
sys := tsd.NewSystem()
sys.NetMon.Set(must.Get(netmon.New(sys.Bus.Get(), t.Logf)))
// Populate the state file with something that will fail to parse to
+65 -58
View File
@@ -137,14 +137,14 @@ func TestFlattenExtraClaims(t *testing.T) {
expected map[string]any
}{
{
name: "empty extra claims",
name: "empty-extra-claims",
input: []capRule{
{ExtraClaims: map[string]any{}},
},
expected: map[string]any{},
},
{
name: "string and number values",
name: "string-and-number-values",
input: []capRule{
{
ExtraClaims: map[string]any{
@@ -159,7 +159,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "slice of strings and ints",
name: "slice-of-strings-and-ints",
input: []capRule{
{
ExtraClaims: map[string]any{
@@ -172,7 +172,8 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "duplicate values deduplicated (slice input)",
// duplicate values deduplicated via slice input
name: "dedup-slice-input",
input: []capRule{
{
ExtraClaims: map[string]any{
@@ -190,7 +191,8 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "ignore unsupported map type, keep valid scalar",
// ignore unsupported map type, keep valid scalar
name: "ignore-unsupported-map-keep-scalar",
input: []capRule{
{
ExtraClaims: map[string]any{
@@ -204,7 +206,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "scalar first, slice second",
name: "scalar-first-slice-second",
input: []capRule{
{ExtraClaims: map[string]any{"foo": "bar"}},
{ExtraClaims: map[string]any{"foo": []any{"baz"}}},
@@ -214,7 +216,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "conflicting scalar and unsupported map",
name: "conflicting-scalar-and-unsupported-map",
input: []capRule{
{ExtraClaims: map[string]any{"foo": "bar"}},
{ExtraClaims: map[string]any{"foo": map[string]any{"bad": "entry"}}},
@@ -224,7 +226,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "multiple slices with overlap",
name: "multiple-slices-with-overlap",
input: []capRule{
{ExtraClaims: map[string]any{"roles": []any{"admin", "user"}}},
{ExtraClaims: map[string]any{"roles": []any{"admin", "guest"}}},
@@ -234,7 +236,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "slice with unsupported values",
name: "slice-with-unsupported-values",
input: []capRule{
{ExtraClaims: map[string]any{
"mixed": []any{"ok", 42, map[string]string{"oops": "fail"}},
@@ -245,7 +247,7 @@ func TestFlattenExtraClaims(t *testing.T) {
},
},
{
name: "duplicate scalar value",
name: "duplicate-scalar-value",
input: []capRule{
{ExtraClaims: map[string]any{"env": "prod"}},
{ExtraClaims: map[string]any{"env": "prod"}},
@@ -279,7 +281,7 @@ func TestExtraClaims(t *testing.T) {
expectError bool
}{
{
name: "extra claim",
name: "extra-claim",
claim: tailscaleClaims{
Claims: jwt.Claims{},
Nonce: "foobar",
@@ -312,7 +314,7 @@ func TestExtraClaims(t *testing.T) {
},
},
{
name: "duplicate claim distinct values",
name: "duplicate-claim-distinct-values",
claim: tailscaleClaims{
Claims: jwt.Claims{},
Nonce: "foobar",
@@ -350,7 +352,7 @@ func TestExtraClaims(t *testing.T) {
},
},
{
name: "multiple extra claims",
name: "multiple-extra-claims",
claim: tailscaleClaims{
Claims: jwt.Claims{},
Nonce: "foobar",
@@ -389,7 +391,7 @@ func TestExtraClaims(t *testing.T) {
},
},
{
name: "overwrite claim",
name: "overwrite-claim",
claim: tailscaleClaims{
Claims: jwt.Claims{},
Nonce: "foobar",
@@ -422,7 +424,7 @@ func TestExtraClaims(t *testing.T) {
expectError: true,
},
{
name: "empty extra claims",
name: "empty-extra-claims",
claim: tailscaleClaims{
Claims: jwt.Claims{},
Nonce: "foobar",
@@ -496,21 +498,21 @@ func TestServeToken(t *testing.T) {
expected map[string]any
}{
{
name: "GET not allowed",
name: "GET-not-allowed",
method: "GET",
grantType: "authorization_code",
strictMode: false,
expectError: true,
},
{
name: "unsupported grant type",
name: "unsupported-grant-type",
method: "POST",
grantType: "pkcs",
strictMode: false,
expectError: true,
},
{
name: "invalid code",
name: "invalid-code",
method: "POST",
grantType: "authorization_code",
code: "invalid-code",
@@ -518,7 +520,7 @@ func TestServeToken(t *testing.T) {
expectError: true,
},
{
name: "omit code from form",
name: "omit-code-from-form",
method: "POST",
grantType: "authorization_code",
omitCode: true,
@@ -526,7 +528,7 @@ func TestServeToken(t *testing.T) {
expectError: true,
},
{
name: "invalid redirect uri",
name: "invalid-redirect-uri",
method: "POST",
grantType: "authorization_code",
code: "valid-code",
@@ -536,7 +538,7 @@ func TestServeToken(t *testing.T) {
expectError: true,
},
{
name: "invalid remoteAddr",
name: "invalid-remoteAddr",
method: "POST",
grantType: "authorization_code",
redirectURI: "https://rp.example.com/callback",
@@ -546,7 +548,7 @@ func TestServeToken(t *testing.T) {
expectError: true,
},
{
name: "extra claim included (non-strict)",
name: "extra-claim-included-non-strict",
method: "POST",
grantType: "authorization_code",
redirectURI: "https://rp.example.com/callback",
@@ -568,7 +570,8 @@ func TestServeToken(t *testing.T) {
},
},
{
name: "attempt to overwrite protected claim (non-strict)",
// attempt to overwrite protected claim in non-strict mode
name: "overwrite-protected-claim-non-strict",
method: "POST",
grantType: "authorization_code",
redirectURI: "https://rp.example.com/callback",
@@ -708,7 +711,7 @@ func TestExtraUserInfo(t *testing.T) {
expectError bool
}{
{
name: "extra claim",
name: "extra-claim",
tokenValidTill: time.Now().Add(1 * time.Minute),
caps: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityTsIDP: {
@@ -725,7 +728,7 @@ func TestExtraUserInfo(t *testing.T) {
},
},
{
name: "duplicate claim distinct values",
name: "duplicate-claim-distinct-values",
tokenValidTill: time.Now().Add(1 * time.Minute),
caps: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityTsIDP: {
@@ -742,7 +745,7 @@ func TestExtraUserInfo(t *testing.T) {
},
},
{
name: "multiple extra claims",
name: "multiple-extra-claims",
tokenValidTill: time.Now().Add(1 * time.Minute),
caps: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityTsIDP: {
@@ -761,13 +764,13 @@ func TestExtraUserInfo(t *testing.T) {
},
},
{
name: "empty extra claims",
name: "empty-extra-claims",
caps: tailcfg.PeerCapMap{},
tokenValidTill: time.Now().Add(1 * time.Minute),
expected: map[string]any{},
},
{
name: "attempt to overwrite protected claim",
name: "overwrite-protected-claim",
tokenValidTill: time.Now().Add(1 * time.Minute),
caps: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityTsIDP: {
@@ -783,7 +786,7 @@ func TestExtraUserInfo(t *testing.T) {
expectError: true,
},
{
name: "extra claim omitted",
name: "extra-claim-omitted",
tokenValidTill: time.Now().Add(1 * time.Minute),
caps: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityTsIDP: {
@@ -798,7 +801,7 @@ func TestExtraUserInfo(t *testing.T) {
expected: map[string]any{},
},
{
name: "expired token",
name: "expired-token",
caps: tailcfg.PeerCapMap{},
tokenValidTill: time.Now().Add(-1 * time.Minute),
expected: map[string]any{},
@@ -1131,19 +1134,22 @@ func TestGetAllowInsecureRegistration(t *testing.T) {
expectAllowInsecureRegistration bool
}{
{
name: "flag explicitly set to false - insecure registration disabled (strict mode)",
// flag explicitly set to false - insecure registration disabled (strict mode)
name: "flag-false-insecure-disabled",
flagSet: true,
flagValue: false,
expectAllowInsecureRegistration: false,
},
{
name: "flag explicitly set to true - insecure registration enabled",
// flag explicitly set to true - insecure registration enabled
name: "flag-true-insecure-enabled",
flagSet: true,
flagValue: true,
expectAllowInsecureRegistration: true,
},
{
name: "flag unset - insecure registration enabled (default for backward compatibility)",
// flag unset - insecure registration enabled (default for backward compatibility)
name: "flag-unset-insecure-enabled-default",
flagSet: false,
flagValue: false, // not used when unset
expectAllowInsecureRegistration: true,
@@ -1192,7 +1198,7 @@ func TestMigrateOAuthClients(t *testing.T) {
expectOldRenamed bool
}{
{
name: "migrate from old file to new file",
name: "migrate-old-to-new",
setupOldFile: true,
oldFileContent: map[string]*funnelClient{
"old-client": {
@@ -1206,7 +1212,7 @@ func TestMigrateOAuthClients(t *testing.T) {
expectOldRenamed: true,
},
{
name: "new file already exists - no migration",
name: "new-file-exists-no-migration",
setupNewFile: true,
newFileContent: map[string]*funnelClient{
"existing-client": {
@@ -1220,12 +1226,12 @@ func TestMigrateOAuthClients(t *testing.T) {
expectOldRenamed: false,
},
{
name: "neither file exists - create empty new file",
name: "neither-exists-create-empty",
expectNewFileExists: true,
expectOldRenamed: false,
},
{
name: "both files exist - prefer new file",
name: "both-exist-prefer-new",
setupOldFile: true,
setupNewFile: true,
oldFileContent: map[string]*funnelClient{
@@ -1373,19 +1379,19 @@ func TestGetConfigFilePath(t *testing.T) {
expectError bool
}{
{
name: "file exists in current directory - use current directory",
name: "file-in-cwd-use-cwd",
fileName: "test-config.json",
createInCwd: true,
expectInCwd: true,
},
{
name: "file does not exist - use root path",
name: "file-missing-use-root",
fileName: "test-config.json",
createInCwd: false,
expectInCwd: false,
},
{
name: "file exists in both - prefer current directory",
name: "file-in-both-prefer-cwd",
fileName: "test-config.json",
createInCwd: true,
createInRoot: true,
@@ -1472,7 +1478,7 @@ func TestAuthorizeStrictMode(t *testing.T) {
}{
// Security boundary test: funnel rejection
{
name: "funnel requests are always rejected for security",
name: "funnel-rejected",
strictMode: true,
clientID: "test-client",
redirectURI: "https://rp.example.com/callback",
@@ -1487,7 +1493,7 @@ func TestAuthorizeStrictMode(t *testing.T) {
// Strict mode parameter validation tests (non-funnel)
{
name: "strict mode - missing client_id",
name: "strict-missing-client_id",
strictMode: true,
clientID: "",
redirectURI: "https://rp.example.com/callback",
@@ -1496,7 +1502,7 @@ func TestAuthorizeStrictMode(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "strict mode - missing redirect_uri",
name: "strict-missing-redirect_uri",
strictMode: true,
clientID: "test-client",
redirectURI: "",
@@ -1507,7 +1513,7 @@ func TestAuthorizeStrictMode(t *testing.T) {
// Strict mode client validation tests (non-funnel)
{
name: "strict mode - invalid client_id",
name: "strict-invalid-client_id",
strictMode: true,
clientID: "invalid-client",
redirectURI: "https://rp.example.com/callback",
@@ -1517,7 +1523,7 @@ func TestAuthorizeStrictMode(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "strict mode - redirect_uri mismatch",
name: "strict-redirect_uri-mismatch",
strictMode: true,
clientID: "test-client",
redirectURI: "https://wrong.example.com/callback",
@@ -1666,7 +1672,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectIDToken bool
}{
{
name: "strict mode - valid token exchange with form credentials",
name: "strict-token-exchange-form-creds",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1680,7 +1686,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectIDToken: true,
},
{
name: "strict mode - valid token exchange with basic auth",
name: "strict-token-exchange-basic-auth",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1695,7 +1701,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectIDToken: true,
},
{
name: "strict mode - missing client credentials",
name: "strict-missing-client-creds",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1708,7 +1714,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectCode: http.StatusUnauthorized,
},
{
name: "strict mode - client_id mismatch",
name: "strict-client_id-mismatch",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1722,7 +1728,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "strict mode - invalid client secret",
name: "strict-invalid-client-secret",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1737,7 +1743,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectCode: http.StatusUnauthorized,
},
{
name: "strict mode - redirect_uri mismatch",
name: "strict-redirect_uri-mismatch",
strictMode: true,
method: "POST",
grantType: "authorization_code",
@@ -1752,7 +1758,7 @@ func TestServeTokenWithClientValidation(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "non-strict mode - no client validation required",
name: "non-strict-no-client-validation",
strictMode: false,
method: "POST",
grantType: "authorization_code",
@@ -1913,7 +1919,7 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectUserInfo bool
}{
{
name: "strict mode - valid token with existing client",
name: "strict-valid-token-existing-client",
strictMode: true,
setupToken: true,
setupClient: true,
@@ -1923,7 +1929,8 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectUserInfo: true,
},
{
name: "strict mode - valid token but client no longer exists",
// valid token but client no longer exists
name: "strict-token-client-deleted",
strictMode: true,
setupToken: true,
setupClient: false,
@@ -1934,7 +1941,7 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectCode: http.StatusUnauthorized,
},
{
name: "strict mode - expired token",
name: "strict-expired-token",
strictMode: true,
setupToken: true,
setupClient: true,
@@ -1945,7 +1952,7 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "strict mode - invalid token",
name: "strict-invalid-token",
strictMode: true,
setupToken: false,
token: "invalid-token",
@@ -1953,7 +1960,7 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "strict mode - token without client association",
name: "strict-token-no-client-assoc",
strictMode: true,
setupToken: true,
setupClient: false,
@@ -1964,7 +1971,7 @@ func TestServeUserInfoWithClientValidation(t *testing.T) {
expectCode: http.StatusBadRequest,
},
{
name: "non-strict mode - no client validation required",
name: "non-strict-no-client-validation",
strictMode: false,
setupToken: true,
setupClient: false,
+227
View File
@@ -0,0 +1,227 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package subtestnames checks that t.Run subtest names don't contain characters
// that require quoting or escaping when re-running via "go test -run".
//
// Go's testing package rewrites subtest names: spaces become underscores,
// non-printable characters get escaped, and regex metacharacters require
// escaping in -run patterns. This makes it hard to re-run specific subtests
// or search for them in code.
//
// This analyzer flags:
// - Direct t.Run calls with string literal names containing bad characters
// - t.Run calls using tt.name (or similar) where tt ranges over a slice/map
// of test cases with string literal names containing bad characters
package subtestnames
import (
"go/ast"
"go/token"
"strconv"
"strings"
"unicode"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
// Analyzer checks that t.Run subtest names are clean for use with "go test -run".
var Analyzer = &analysis.Analyzer{
Name: "subtestnames",
Doc: "check that t.Run subtest names don't require quoting when re-running via go test -run",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
// badChars are characters that are problematic in subtest names.
// Spaces are rewritten to underscores by testing.rewrite, and regex
// metacharacters require escaping in -run patterns.
const badChars = " \t\n\r^$.*+?()[]{}|\\'\"#"
// hasBadChar reports whether s contains any character that would be
// problematic in a subtest name.
func hasBadChar(s string) bool {
return strings.ContainsAny(s, badChars) || strings.ContainsFunc(s, func(r rune) bool {
return !unicode.IsPrint(r)
})
}
// hasBadDash reports whether s starts or ends with a dash, which is
// problematic in subtest names because "go test -run" may interpret a
// leading dash as a flag, and trailing dashes are confusing.
func hasBadDash(s string) bool {
return strings.HasPrefix(s, "-") || strings.HasSuffix(s, "-")
}
func run(pass *analysis.Pass) (any, error) {
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Build a stack of enclosing nodes so we can find the RangeStmt
// enclosing a given t.Run call.
nodeFilter := []ast.Node{
(*ast.RangeStmt)(nil),
(*ast.CallExpr)(nil),
}
var rangeStack []*ast.RangeStmt
insp.Nodes(nodeFilter, func(n ast.Node, push bool) bool {
switch n := n.(type) {
case *ast.RangeStmt:
if push {
rangeStack = append(rangeStack, n)
} else {
rangeStack = rangeStack[:len(rangeStack)-1]
}
return true
case *ast.CallExpr:
if !push {
return true
}
checkCallExpr(pass, n, rangeStack)
return true
}
return true
})
return nil, nil
}
func checkCallExpr(pass *analysis.Pass, call *ast.CallExpr, rangeStack []*ast.RangeStmt) {
// Check if this is a t.Run(...) or b.Run(...) call.
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Run" || len(call.Args) < 2 {
return
}
// Verify the receiver is *testing.T, *testing.B, or *testing.F.
if !isTestingTBF(pass, sel) {
return
}
nameArg := call.Args[0]
// Case 1: Direct string literal, e.g. t.Run("foo bar", ...)
if lit, ok := nameArg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
val, err := strconv.Unquote(lit.Value)
if err != nil {
return
}
if hasBadChar(val) {
pass.Reportf(lit.Pos(), "subtest name %s contains characters that require quoting in go test -run patterns", lit.Value)
} else if hasBadDash(val) {
pass.Reportf(lit.Pos(), "subtest name %s starts or ends with '-' which is problematic in go test -run patterns", lit.Value)
}
return
}
// Case 2: Selector expression like tt.name, tc.name, etc.
// where tt is a range variable over a slice/map of test cases.
selExpr, ok := nameArg.(*ast.SelectorExpr)
if !ok {
return
}
ident, ok := selExpr.X.(*ast.Ident)
if !ok {
return
}
// Find the enclosing range statement where ident is the value variable.
for i := len(rangeStack) - 1; i >= 0; i-- {
rs := rangeStack[i]
valIdent, ok := rs.Value.(*ast.Ident)
if !ok || valIdent.Obj != ident.Obj {
continue
}
// Found the range statement. Check the source being iterated.
checkRangeSource(pass, rs.X, selExpr.Sel)
return
}
}
// isTestingTBF checks whether sel looks like a method call on *testing.T, *testing.B, or *testing.F.
func isTestingTBF(pass *analysis.Pass, sel *ast.SelectorExpr) bool {
typ := pass.TypesInfo.TypeOf(sel.X)
if typ != nil {
s := typ.String()
return s == "*testing.T" || s == "*testing.B" || s == "*testing.F"
}
return false
}
// checkRangeSource examines the expression being ranged over and checks
// composite literal elements for bad subtest name fields.
func checkRangeSource(pass *analysis.Pass, rangeExpr ast.Expr, fieldName *ast.Ident) {
switch x := rangeExpr.(type) {
case *ast.Ident:
if x.Obj == nil {
return
}
switch decl := x.Obj.Decl.(type) {
case *ast.AssignStmt:
// e.g. tests := []struct{...}{...}
for _, rhs := range decl.Rhs {
checkCompositeLit(pass, rhs, fieldName)
}
case *ast.ValueSpec:
// e.g. var tests = []struct{...}{...}
for _, val := range decl.Values {
checkCompositeLit(pass, val, fieldName)
}
}
case *ast.CompositeLit:
checkCompositeLit(pass, x, fieldName)
}
}
// checkCompositeLit checks a composite literal (slice/map) for elements
// that have a field with a bad subtest name.
func checkCompositeLit(pass *analysis.Pass, expr ast.Expr, fieldName *ast.Ident) {
comp, ok := expr.(*ast.CompositeLit)
if !ok {
return
}
for _, elt := range comp.Elts {
// For map literals, check the value.
if kv, ok := elt.(*ast.KeyValueExpr); ok {
elt = kv.Value
}
checkStructLitField(pass, elt, fieldName)
}
}
// checkStructLitField checks a struct literal for a field with the given name
// that contains a bad subtest name string.
func checkStructLitField(pass *analysis.Pass, expr ast.Expr, fieldName *ast.Ident) {
comp, ok := expr.(*ast.CompositeLit)
if !ok {
return
}
for _, elt := range comp.Elts {
kv, ok := elt.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := kv.Key.(*ast.Ident)
if !ok || key.Name != fieldName.Name {
continue
}
lit, ok := kv.Value.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue
}
val, err := strconv.Unquote(lit.Value)
if err != nil {
continue
}
if hasBadChar(val) {
pass.Reportf(lit.Pos(), "subtest name %s contains characters that require quoting in go test -run patterns", lit.Value)
} else if hasBadDash(val) {
pass.Reportf(lit.Pos(), "subtest name %s starts or ends with '-' which is problematic in go test -run patterns", lit.Value)
}
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package subtestnames
import (
"testing"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "example")
}
@@ -0,0 +1,112 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package example
import "testing"
func TestDirect(t *testing.T) {
// Bad: spaces
t.Run("that everything's cool", func(t *testing.T) {}) // want `subtest name "that everything's cool" contains characters that require quoting`
// Bad: apostrophe
t.Run("it's working", func(t *testing.T) {}) // want `subtest name "it's working" contains characters that require quoting`
// Bad: regex metacharacters
t.Run("test(foo)", func(t *testing.T) {}) // want `subtest name "test\(foo\)" contains characters that require quoting`
t.Run("test[0]", func(t *testing.T) {}) // want `subtest name "test\[0\]" contains characters that require quoting`
t.Run("a|b", func(t *testing.T) {}) // want `subtest name "a\|b" contains characters that require quoting`
t.Run("a*b", func(t *testing.T) {}) // want `subtest name "a\*b" contains characters that require quoting`
t.Run("a+b", func(t *testing.T) {}) // want `subtest name "a\+b" contains characters that require quoting`
t.Run("a.b", func(t *testing.T) {}) // want `subtest name "a\.b" contains characters that require quoting`
t.Run("^start", func(t *testing.T) {}) // want `subtest name "\^start" contains characters that require quoting`
t.Run("end$", func(t *testing.T) {}) // want `subtest name "end\$" contains characters that require quoting`
t.Run("a{2}", func(t *testing.T) {}) // want `subtest name "a\{2\}" contains characters that require quoting`
t.Run("a?b", func(t *testing.T) {}) // want `subtest name "a\?b" contains characters that require quoting`
t.Run("a\\b", func(t *testing.T) {}) // want `subtest name "a\\\\b" contains characters that require quoting`
// Bad: double quotes
t.Run("say \"hello\"", func(t *testing.T) {}) // want `subtest name "say \\"hello\\"" contains characters that require quoting`
// Bad: hash
t.Run("comment#1", func(t *testing.T) {}) // want `subtest name "comment#1" contains characters that require quoting`
// Bad: leading/trailing dash
t.Run("-leading-dash", func(t *testing.T) {}) // want `subtest name "-leading-dash" starts or ends with '-' which is problematic`
t.Run("trailing-dash-", func(t *testing.T) {}) // want `subtest name "trailing-dash-" starts or ends with '-' which is problematic`
t.Run("-both-", func(t *testing.T) {}) // want `subtest name "-both-" starts or ends with '-' which is problematic`
// Good: clean names
t.Run("zero-passes", func(t *testing.T) {})
t.Run("simple_test", func(t *testing.T) {})
t.Run("CamelCase", func(t *testing.T) {})
t.Run("with-dashes", func(t *testing.T) {})
t.Run("123", func(t *testing.T) {})
t.Run("comma,separated", func(t *testing.T) {})
t.Run("colon:value", func(t *testing.T) {})
t.Run("slash/path", func(t *testing.T) {})
t.Run("equals=sign", func(t *testing.T) {})
}
func TestTableDriven(t *testing.T) {
tests := []struct {
name string
val int
}{
{name: "bad space name", val: 1}, // want `subtest name "bad space name" contains characters that require quoting`
{name: "good-name", val: 2},
{name: "also(bad)", val: 3}, // want `subtest name "also\(bad\)" contains characters that require quoting`
{name: "it's-bad", val: 4}, // want `subtest name "it's-bad" contains characters that require quoting`
{name: "clean-name", val: 5},
{name: "-leading-dash", val: 6}, // want `subtest name "-leading-dash" starts or ends with '-' which is problematic`
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {})
}
}
func TestTableDrivenVar(t *testing.T) {
var tests = []struct {
name string
val int
}{
{name: "has spaces", val: 1}, // want `subtest name "has spaces" contains characters that require quoting`
{name: "ok-name", val: 2},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {})
}
}
func TestTableDrivenMap(t *testing.T) {
tests := map[string]struct {
name string
val int
}{
"key1": {name: "bad name here", val: 1}, // want `subtest name "bad name here" contains characters that require quoting`
"key2": {name: "ok-name", val: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {})
}
}
func TestNotTesting(t *testing.T) {
// Not a t.Run call, should not trigger.
s := struct{ Run func(string, func()) }{}
s.Run("bad name here", func() {})
}
func TestDynamicName(t *testing.T) {
// Dynamic name, not a string literal — should not trigger.
name := getName()
t.Run(name, func(t *testing.T) {})
}
func getName() string { return "foo" }
func BenchmarkDirect(b *testing.B) {
// Also check b.Run.
b.Run("bad name here", func(b *testing.B) {}) // want `subtest name "bad name here" contains characters that require quoting`
b.Run("good-name", func(b *testing.B) {})
}
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"golang.org/x/tools/go/analysis/unitchecker"
"tailscale.com/cmd/vet/jsontags"
"tailscale.com/cmd/vet/subtestnames"
)
//go:embed jsontags_allowlist
@@ -20,5 +21,5 @@ func init() {
}
func main() {
unitchecker.Main(jsontags.Analyzer)
unitchecker.Main(jsontags.Analyzer, subtestnames.Analyzer)
}