mirror of https://github.com/tailscale/tailscale/
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 benchmarkspull/17963/head
parent
1a66d35683
commit
2cdbee62f2
@ -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…
Reference in New Issue