You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/ipn/conffile/conffile_test.go

400 lines
8.8 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package conffile
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"tailscale.com/ipn"
)
func TestConfig_WantRunning(t *testing.T) {
tests := []struct {
name string
c *Config
want bool
}{
{
name: "nil_config",
c: nil,
want: false,
},
{
name: "enabled_true",
c: &Config{
Parsed: ipn.ConfigVAlpha{
Enabled: ipn.BoolOrValue[bool]{Value: ipn.BoolTrue},
},
},
want: true,
},
{
name: "enabled_false",
c: &Config{
Parsed: ipn.ConfigVAlpha{
Enabled: ipn.BoolOrValue[bool]{Value: ipn.BoolFalse},
},
},
want: false,
},
{
name: "enabled_unset",
c: &Config{
Parsed: ipn.ConfigVAlpha{},
},
want: true, // default is to run
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.c.WantRunning()
if got != tt.want {
t.Errorf("WantRunning() = %v, want %v", got, tt.want)
}
})
}
}
func TestLoad_Success(t *testing.T) {
tests := []struct {
name string
content string
wantVer string
}{
{
name: "basic_alpha0",
content: `{
"version": "alpha0"
}`,
wantVer: "alpha0",
},
{
name: "alpha0_with_enabled",
content: `{
"version": "alpha0",
"enabled": true
}`,
wantVer: "alpha0",
},
{
name: "hujson_with_comments",
content: `{
// This is a comment
"version": "alpha0", // version field
"enabled": true
}`,
wantVer: "alpha0",
},
{
name: "hujson_trailing_commas",
content: `{
"version": "alpha0",
"enabled": true,
}`,
wantVer: "alpha0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(tt.content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
c, err := Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if c == nil {
t.Fatal("Load() returned nil config")
}
if c.Path != path {
t.Errorf("Path = %q, want %q", c.Path, path)
}
if c.Version != tt.wantVer {
t.Errorf("Version = %q, want %q", c.Version, tt.wantVer)
}
if len(c.Raw) == 0 {
t.Error("Raw is empty")
}
if len(c.Std) == 0 {
t.Error("Std is empty")
}
// Verify Std is valid JSON
var v map[string]any
if err := json.Unmarshal(c.Std, &v); err != nil {
t.Errorf("Std is not valid JSON: %v", err)
}
})
}
}
func TestLoad_Errors(t *testing.T) {
tests := []struct {
name string
content string
wantErrHave string // substring that should be in error
}{
{
name: "invalid_json",
content: `{invalid json}`,
wantErrHave: "error parsing",
},
{
name: "no_version",
content: `{"enabled": true}`,
wantErrHave: "no \"version\" field",
},
{
name: "empty_version",
content: `{"version": ""}`,
wantErrHave: "no \"version\" field",
},
{
name: "unsupported_version",
content: `{"version": "beta1"}`,
wantErrHave: "unsupported \"version\"",
},
{
name: "unsupported_version_v1",
content: `{"version": "v1"}`,
wantErrHave: "unsupported \"version\"",
},
{
name: "unknown_field",
content: `{
"version": "alpha0",
"unknownField": "value"
}`,
wantErrHave: "unknown field",
},
{
name: "trailing_data",
content: `{
"version": "alpha0"
}
{
"extra": "object"
}`,
wantErrHave: "trailing data",
},
{
name: "empty_file",
content: ``,
wantErrHave: "error parsing",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(tt.content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
c, err := Load(path)
if err == nil {
t.Errorf("Load() succeeded, want error containing %q", tt.wantErrHave)
} else if !strings.Contains(err.Error(), tt.wantErrHave) {
t.Errorf("Load() error = %q, want substring %q", err.Error(), tt.wantErrHave)
}
if c != nil {
t.Errorf("Load() returned non-nil config on error")
}
})
}
}
func TestLoad_FileNotFound(t *testing.T) {
_, err := Load("/nonexistent/path/config.json")
if err == nil {
t.Error("Load() with nonexistent file succeeded, want error")
}
if !os.IsNotExist(err) {
t.Errorf("Load() error type: got %T, want os.PathError or similar", err)
}
}
func TestLoad_VMUserDataPath(t *testing.T) {
// This will fail unless we're running on an EC2 instance
// Just verify it handles the special path
_, err := Load(VMUserDataPath)
// We expect an error since we're not on EC2
// but we want to make sure it tries the right code path
if err == nil {
t.Skip("unexpectedly succeeded loading VM user data (are we on EC2?)")
}
// Error should be related to metadata service, not file I/O
errStr := err.Error()
if strings.Contains(errStr, "no such file") {
t.Errorf("Load(VMUserDataPath) tried to read file instead of metadata service")
}
}
func TestVMUserDataPath_Constant(t *testing.T) {
if VMUserDataPath != "vm:user-data" {
t.Errorf("VMUserDataPath = %q, want %q", VMUserDataPath, "vm:user-data")
}
}
func TestLoad_PreservesRawBytes(t *testing.T) {
content := `{
// Comment
"version": "alpha0",
"enabled": true,
}`
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
c, err := Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
// Raw should contain the original HuJSON with comments
if !strings.Contains(string(c.Raw), "// Comment") {
t.Error("Raw doesn't preserve comments")
}
// Std should be valid JSON without comments
if strings.Contains(string(c.Std), "//") {
t.Error("Std contains comments (should be standardized JSON)")
}
}
func TestLoad_ComplexConfig(t *testing.T) {
content := `{
"version": "alpha0",
"enabled": true,
"server": "https://login.tailscale.com",
"hostname": "test-host",
"authKey": "tskey-test-key"
}`
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
c, err := Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if c.Parsed.ServerURL != "https://login.tailscale.com" {
t.Errorf("ServerURL = %q, want %q", c.Parsed.ServerURL, "https://login.tailscale.com")
}
if c.Parsed.Hostname != "test-host" {
t.Errorf("Hostname = %q, want %q", c.Parsed.Hostname, "test-host")
}
if c.Parsed.AuthKey != "tskey-test-key" {
t.Errorf("AuthKey = %q, want %q", c.Parsed.AuthKey, "tskey-test-key")
}
}
func TestLoad_EmptyConfig(t *testing.T) {
content := `{"version": "alpha0"}`
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
c, err := Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
// Empty config should still be valid and want to run
if !c.WantRunning() {
t.Error("WantRunning() = false, want true for empty config")
}
}
func TestLoad_PermissionCheck(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("skipping permission test when running as root")
}
content := `{"version": "alpha0"}`
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0000); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
_, err := Load(path)
if err == nil {
t.Error("Load() succeeded on unreadable file, want error")
}
}
// Test concurrent loads
func TestLoad_Concurrent(t *testing.T) {
content := `{"version": "alpha0", "enabled": true}`
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
// Load the same file concurrently
done := make(chan error, 10)
for i := 0; i < 10; i++ {
go func() {
_, err := Load(path)
done <- err
}()
}
for i := 0; i < 10; i++ {
if err := <-done; err != nil {
t.Errorf("concurrent Load() failed: %v", err)
}
}
}
// Benchmark config loading
func BenchmarkLoad(b *testing.B) {
content := `{
"version": "alpha0",
"enabled": true,
"server": "https://login.tailscale.com",
"hostname": "bench-host"
}`
tmpDir := b.TempDir()
path := filepath.Join(tmpDir, "config.json")
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
b.Fatalf("failed to write test file: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Load(path)
if err != nil {
b.Fatalf("Load() failed: %v", err)
}
}
}