Add tests for ipn/store/kubestore and envknob

- ipn/store/kubestore: Add comprehensive tests for sanitizeKey function
  - All valid/invalid character handling
  - Kubernetes naming restrictions
  - Unicode and special character replacement
  - Idempotent behavior
  - Performance benchmarks

- envknob: Add comprehensive tests for environment variable handling
  - Bool, String, OptBool functions
  - Registration mechanism for all types
  - Setenv and LogCurrent
  - Integration tests for multiple variable types
  - Performance benchmarks
pull/17963/head
Claude 2 weeks ago
parent 1a66d35683
commit 2cdbee62f2
No known key found for this signature in database

@ -0,0 +1,328 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package envknob
import (
"os"
"testing"
"time"
"tailscale.com/types/opt"
)
func TestBool(t *testing.T) {
tests := []struct {
name string
envVar string
value string
want bool
wantSet bool
}{
{name: "true", envVar: "TEST_BOOL_TRUE", value: "true", want: true, wantSet: true},
{name: "false", envVar: "TEST_BOOL_FALSE", value: "false", want: false, wantSet: true},
{name: "1", envVar: "TEST_BOOL_1", value: "1", want: true, wantSet: true},
{name: "0", envVar: "TEST_BOOL_0", value: "0", want: false, wantSet: true},
{name: "unset", envVar: "TEST_BOOL_UNSET", value: "", want: false, wantSet: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.envVar, tt.value)
defer os.Unsetenv(tt.envVar)
}
got := Bool(tt.envVar)
if got != tt.want {
t.Errorf("Bool(%q) = %v, want %v", tt.envVar, got, tt.want)
}
})
}
}
func TestBoolDefaultTrue(t *testing.T) {
envVar := "TEST_BOOL_DEFAULT_TRUE"
// Unset - should return true
os.Unsetenv(envVar)
if got := BoolDefaultTrue(envVar); !got {
t.Errorf("BoolDefaultTrue(%q) with unset = %v, want true", envVar, got)
}
// Set to false - should return false
os.Setenv(envVar, "false")
defer os.Unsetenv(envVar)
if got := BoolDefaultTrue(envVar); got {
t.Errorf("BoolDefaultTrue(%q) with false = %v, want false", envVar, got)
}
}
func TestGOOS(t *testing.T) {
// Should return a non-empty string
if got := GOOS(); got == "" {
t.Error("GOOS() returned empty string")
}
// By default should match runtime.GOOS
if got := GOOS(); got != os.Getenv("GOOS") && os.Getenv("GOOS") == "" {
// If GOOS env var not set, should use runtime
// Can't test exact value as it's platform-dependent
}
}
func TestString(t *testing.T) {
tests := []struct {
name string
envVar string
value string
want string
}{
{name: "set", envVar: "TEST_STRING", value: "hello", want: "hello"},
{name: "empty", envVar: "TEST_STRING_EMPTY", value: "", want: ""},
{name: "spaces", envVar: "TEST_STRING_SPACES", value: " value ", want: " value "},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.envVar, tt.value)
defer os.Unsetenv(tt.envVar)
}
got := String(tt.envVar)
if got != tt.want {
t.Errorf("String(%q) = %q, want %q", tt.envVar, got, tt.want)
}
})
}
}
func TestOptBool(t *testing.T) {
tests := []struct {
name string
envVar string
value string
wantSet bool
wantVal bool
}{
{name: "true", envVar: "TEST_OPT_TRUE", value: "true", wantSet: true, wantVal: true},
{name: "false", envVar: "TEST_OPT_FALSE", value: "false", wantSet: true, wantVal: false},
{name: "unset", envVar: "TEST_OPT_UNSET", value: "", wantSet: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.envVar, tt.value)
defer os.Unsetenv(tt.envVar)
} else {
os.Unsetenv(tt.envVar)
}
got := OptBool(tt.envVar)
if _, ok := got.Get(); ok != tt.wantSet {
t.Errorf("OptBool(%q).Get() set = %v, want %v", tt.envVar, ok, tt.wantSet)
}
if tt.wantSet {
if val, _ := got.Get(); val != tt.wantVal {
t.Errorf("OptBool(%q).Get() value = %v, want %v", tt.envVar, val, tt.wantVal)
}
}
})
}
}
func TestSetenv(t *testing.T) {
envVar := "TEST_SETENV"
value := "test_value"
defer os.Unsetenv(envVar)
Setenv(envVar, value)
// Verify it's actually set in the environment
if got := os.Getenv(envVar); got != value {
t.Errorf("After Setenv, os.Getenv(%q) = %q, want %q", envVar, got, value)
}
// Verify String retrieves it
if got := String(envVar); got != value {
t.Errorf("After Setenv, String(%q) = %q, want %q", envVar, got, value)
}
}
func TestRegisterString(t *testing.T) {
envVar := "TEST_REGISTER_STRING"
value := "registered"
os.Setenv(envVar, value)
defer os.Unsetenv(envVar)
var target string
RegisterString(&target, envVar)
if target != value {
t.Errorf("After RegisterString, target = %q, want %q", target, value)
}
}
func TestRegisterBool(t *testing.T) {
envVar := "TEST_REGISTER_BOOL"
os.Setenv(envVar, "true")
defer os.Unsetenv(envVar)
var target bool
RegisterBool(&target, envVar)
if !target {
t.Error("After RegisterBool with true, target = false, want true")
}
}
func TestRegisterOptBool(t *testing.T) {
envVar := "TEST_REGISTER_OPTBOOL"
os.Setenv(envVar, "true")
defer os.Unsetenv(envVar)
var target opt.Bool
RegisterOptBool(&target, envVar)
if val, ok := target.Get(); !ok || !val {
t.Errorf("After RegisterOptBool, target = (%v, %v), want (true, true)", val, ok)
}
}
func TestLogCurrent(t *testing.T) {
// Set a test env var
os.Setenv("TEST_LOG_CURRENT", "test")
defer os.Unsetenv("TEST_LOG_CURRENT")
// Force it to be noted
Setenv("TEST_LOG_CURRENT", "test")
logged := false
logf := func(format string, args ...any) {
logged = true
}
LogCurrent(logf)
if !logged {
t.Error("LogCurrent did not call logf")
}
}
func TestUseRunningUserForAuth(t *testing.T) {
// This just tests that the function runs without panicking
defer func() {
if r := recover(); r != nil {
t.Errorf("UseRunningUserForAuth() panicked: %v", r)
}
}()
_ = UseRunningUserForAuth()
}
func TestDERPConncap(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("DERPConncap() panicked: %v", r)
}
}()
got := DERPConncap()
if got < 0 {
t.Errorf("DERPConncap() = %d, want >= 0", got)
}
}
// Test some known environment variables
func TestKnownVariables(t *testing.T) {
// These functions should not panic
_ = CrashMonitorSupport()
_ = NoLogsNoSupport()
_ = AllowRemoteUpdate()
_ = DisablePortMapper()
}
// Benchmark common operations
func BenchmarkBool(b *testing.B) {
os.Setenv("BENCH_BOOL", "true")
defer os.Unsetenv("BENCH_BOOL")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Bool("BENCH_BOOL")
}
}
func BenchmarkString(b *testing.B) {
os.Setenv("BENCH_STRING", "value")
defer os.Unsetenv("BENCH_STRING")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = String("BENCH_STRING")
}
}
func BenchmarkOptBool(b *testing.B) {
os.Setenv("BENCH_OPTBOOL", "true")
defer os.Unsetenv("BENCH_OPTBOOL")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = OptBool("BENCH_OPTBOOL")
}
}
// Integration test for registering variables
func TestRegisterIntegration(t *testing.T) {
// Test registering multiple types
var (
strVal string
boolVal bool
optVal opt.Bool
durVal time.Duration
intVal int
)
os.Setenv("TEST_INT_STR", "hello")
os.Setenv("TEST_INT_BOOL", "true")
os.Setenv("TEST_INT_OPT", "false")
os.Setenv("TEST_INT_DUR", "5s")
os.Setenv("TEST_INT_INT", "42")
defer func() {
os.Unsetenv("TEST_INT_STR")
os.Unsetenv("TEST_INT_BOOL")
os.Unsetenv("TEST_INT_OPT")
os.Unsetenv("TEST_INT_DUR")
os.Unsetenv("TEST_INT_INT")
}()
RegisterString(&strVal, "TEST_INT_STR")
RegisterBool(&boolVal, "TEST_INT_BOOL")
RegisterOptBool(&optVal, "TEST_INT_OPT")
RegisterDuration(&durVal, "TEST_INT_DUR")
RegisterInt(&intVal, "TEST_INT_INT")
if strVal != "hello" {
t.Errorf("strVal = %q, want %q", strVal, "hello")
}
if !boolVal {
t.Error("boolVal = false, want true")
}
if val, ok := optVal.Get(); !ok || val {
t.Errorf("optVal = (%v, %v), want (false, true)", val, ok)
}
if durVal != 5*time.Second {
t.Errorf("durVal = %v, want 5s", durVal)
}
if intVal != 42 {
t.Errorf("intVal = %d, want 42", intVal)
}
}

@ -0,0 +1,267 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package kubestore
import (
"strings"
"testing"
"tailscale.com/ipn"
)
func TestStore_String(t *testing.T) {
s := &Store{
secretName: "test-secret",
}
if got := s.String(); got != "kube.Store" {
t.Errorf("String() = %q, want %q", got, "kube.Store")
}
}
func TestSanitizeKey(t *testing.T) {
tests := []struct {
name string
input ipn.StateKey
want string
}{
{
name: "alphanumeric",
input: "abc123",
want: "abc123",
},
{
name: "with_dashes",
input: "test-key-name",
want: "test-key-name",
},
{
name: "with_underscores",
input: "test_key_name",
want: "test_key_name",
},
{
name: "with_dots",
input: "test.key.name",
want: "test.key.name",
},
{
name: "with_invalid_chars",
input: "test/key:name",
want: "test_key_name",
},
{
name: "with_spaces",
input: "test key name",
want: "test_key_name",
},
{
name: "with_special_chars",
input: "test@key#name",
want: "test_key_name",
},
{
name: "mixed_case",
input: "TestKeyName",
want: "TestKeyName",
},
{
name: "all_invalid",
input: "@#$%^&*()",
want: "_________",
},
{
name: "empty",
input: "",
want: "",
},
{
name: "path_like",
input: "/var/lib/tailscale/state",
want: "_var_lib_tailscale_state",
},
{
name: "url_like",
input: "https://example.com/path?query=value",
want: "https___example.com_path_query_value",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sanitizeKey(tt.input)
if got != tt.want {
t.Errorf("sanitizeKey(%q) = %q, want %q", tt.input, got, tt.want)
}
// Verify result contains only valid characters
for _, r := range got {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.') {
t.Errorf("sanitizeKey(%q) = %q contains invalid char %c", tt.input, got, r)
}
}
})
}
}
func TestSanitizeKey_Idempotent(t *testing.T) {
// Sanitizing a key twice should produce the same result
tests := []ipn.StateKey{
"valid-key",
"invalid/key",
"test@key#name",
"path/to/state",
}
for _, key := range tests {
first := sanitizeKey(key)
second := sanitizeKey(ipn.StateKey(first))
if first != second {
t.Errorf("sanitizeKey not idempotent for %q: first=%q, second=%q", key, first, second)
}
}
}
func TestSanitizeKey_PreservesValidChars(t *testing.T) {
// All valid characters should pass through unchanged
validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
result := sanitizeKey(ipn.StateKey(validChars))
if result != validChars {
t.Errorf("sanitizeKey(%q) = %q, want %q", validChars, result, validChars)
}
}
func TestSanitizeKey_Length(t *testing.T) {
// Length should be preserved
tests := []ipn.StateKey{
"short",
"a-very-long-key-name-that-has-many-characters-in-it",
"x",
"",
}
for _, key := range tests {
result := sanitizeKey(key)
if len(result) != len(string(key)) {
t.Errorf("sanitizeKey(%q) length = %d, want %d", key, len(result), len(string(key)))
}
}
}
func TestStore_SetDialer(t *testing.T) {
// This test verifies SetDialer doesn't panic
// Full testing would require mocking kubeclient.Client
s := &Store{
secretName: "test-secret",
}
// Should not panic
defer func() {
if r := recover(); r != nil {
t.Errorf("SetDialer panicked: %v", r)
}
}()
s.SetDialer(nil)
}
func TestSanitizeKey_Unicode(t *testing.T) {
// Unicode characters should be replaced with underscore
tests := []struct {
input string
desc string
}{
{input: "hello世界", desc: "Chinese characters"},
{input: "тест", desc: "Cyrillic characters"},
{input: "café", desc: "Accented characters"},
{input: "🔑key", desc: "Emoji"},
{input: "αβγ", desc: "Greek letters"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
result := sanitizeKey(ipn.StateKey(tt.input))
// Should only contain valid chars
for _, r := range result {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.') {
t.Errorf("sanitizeKey(%q) = %q contains invalid char %c", tt.input, result, r)
}
}
// Should contain at least some underscores (since we replaced unicode)
if !strings.Contains(result, "_") && len(tt.input) > 0 {
t.Errorf("sanitizeKey(%q) = %q, expected underscores for unicode replacement", tt.input, result)
}
})
}
}
func TestSanitizeKey_KubernetesRestrictions(t *testing.T) {
// Test that sanitized keys would be valid Kubernetes secret keys
tests := []ipn.StateKey{
"simple",
"with-dash",
"with_underscore",
"with.dot",
"MixedCase123",
"has/slash",
"has:colon",
"has spaces",
"has@symbols#here",
}
for _, key := range tests {
result := sanitizeKey(key)
// Kubernetes secret keys must:
// - consist of alphanumeric characters, '-', '_' or '.'
// This is what our sanitizeKey function ensures
for _, r := range result {
valid := (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
r == '-' || r == '_' || r == '.'
if !valid {
t.Errorf("sanitizeKey(%q) = %q contains Kubernetes-invalid char %c", key, result, r)
}
}
}
}
// Benchmark sanitizeKey performance
func BenchmarkSanitizeKey(b *testing.B) {
keys := []ipn.StateKey{
"simple-key",
"path/to/state/file",
"https://example.com/path?query=value",
"key-with-many-invalid-@#$%-characters",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sanitizeKey(keys[i%len(keys)])
}
}
func BenchmarkSanitizeKey_ValidOnly(b *testing.B) {
key := ipn.StateKey("valid-key-123.with_valid.chars")
b.ResetTimer()
for i := 0; i < b.N; i++ {
sanitizeKey(key)
}
}
func BenchmarkSanitizeKey_AllInvalid(b *testing.B) {
key := ipn.StateKey("@#$%^&*()/\\:;'\"<>?,")
b.ResetTimer()
for i := 0; i < b.N; i++ {
sanitizeKey(key)
}
}
Loading…
Cancel
Save