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:
committed by
Brad Fitzpatrick
parent
0f02c20c5e
commit
5ef3713c9f
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")}},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user