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