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/client/local/serve_test.go

284 lines
5.7 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_serve
package local
import (
"encoding/json"
"testing"
"tailscale.com/ipn"
)
func TestGetServeConfigFromJSON(t *testing.T) {
tests := []struct {
name string
input []byte
wantNil bool
wantErr bool
}{
{
name: "empty_object",
input: []byte(`{}`),
wantNil: false,
wantErr: false,
},
{
name: "null",
input: []byte(`null`),
wantNil: true,
wantErr: false,
},
{
name: "valid_config_with_web",
input: []byte(`{
"TCP": {},
"Web": {
"example.ts.net:443": {
"Handlers": {
"/": {"Proxy": "http://127.0.0.1:3000"}
}
}
},
"AllowFunnel": {}
}`),
wantNil: false,
wantErr: false,
},
{
name: "valid_config_with_tcp",
input: []byte(`{
"TCP": {
"443": {
"HTTPS": true
}
}
}`),
wantNil: false,
wantErr: false,
},
{
name: "invalid_json",
input: []byte(`{invalid json`),
wantNil: true,
wantErr: true,
},
{
name: "empty_string",
input: []byte(``),
wantNil: true,
wantErr: true,
},
{
name: "array_instead_of_object",
input: []byte(`[]`),
wantNil: true,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getServeConfigFromJSON(tt.input)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if tt.wantNil && got != nil {
t.Errorf("expected nil, got %+v", got)
}
if !tt.wantNil && got == nil {
t.Error("expected non-nil result")
}
})
}
}
func TestGetServeConfigFromJSON_RoundTrip(t *testing.T) {
// Create a serve config
original := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"example.ts.net:443": {
Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
},
},
},
}
// Marshal to JSON
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
// Parse back
parsed, err := getServeConfigFromJSON(data)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
if parsed == nil {
t.Fatal("parsed config is nil")
}
// Verify TCP config
if len(parsed.TCP) != 1 {
t.Errorf("TCP length = %d, want 1", len(parsed.TCP))
}
if handler, ok := parsed.TCP[443]; !ok || !handler.HTTPS {
t.Error("TCP[443] not configured correctly")
}
// Verify Web config
if len(parsed.Web) != 1 {
t.Errorf("Web length = %d, want 1", len(parsed.Web))
}
}
func TestGetServeConfigFromJSON_NullVsEmptyObject(t *testing.T) {
// Test that null JSON returns nil
nullResult, err := getServeConfigFromJSON([]byte(`null`))
if err != nil {
t.Errorf("null JSON should not error: %v", err)
}
if nullResult != nil {
t.Error("null JSON should return nil")
}
// Test that empty object returns non-nil
emptyResult, err := getServeConfigFromJSON([]byte(`{}`))
if err != nil {
t.Errorf("empty object should not error: %v", err)
}
if emptyResult == nil {
t.Error("empty object should return non-nil")
}
}
func TestGetServeConfigFromJSON_ComplexConfig(t *testing.T) {
complexJSON := []byte(`{
"TCP": {
"80": {"HTTPS": false, "TCPForward": "127.0.0.1:8080"},
"443": {"HTTPS": true},
"8080": {"TCPForward": "192.168.1.100:8080"}
},
"Web": {
"site1.ts.net:443": {
"Handlers": {
"/": {"Proxy": "http://localhost:3000"},
"/api": {"Proxy": "http://localhost:4000"},
"/static": {"Path": "/var/www/static"}
}
},
"site2.ts.net:443": {
"Handlers": {
"/": {"Proxy": "http://localhost:5000"}
}
}
},
"AllowFunnel": {
"site1.ts.net:443": true
}
}`)
config, err := getServeConfigFromJSON(complexJSON)
if err != nil {
t.Fatalf("failed to parse complex config: %v", err)
}
if config == nil {
t.Fatal("config is nil")
}
// Verify TCP ports
if len(config.TCP) != 3 {
t.Errorf("TCP ports = %d, want 3", len(config.TCP))
}
// Verify Web hosts
if len(config.Web) != 2 {
t.Errorf("Web hosts = %d, want 2", len(config.Web))
}
// Verify AllowFunnel
if len(config.AllowFunnel) != 1 {
t.Errorf("AllowFunnel entries = %d, want 1", len(config.AllowFunnel))
}
}
func TestGetServeConfigFromJSON_EdgeCases(t *testing.T) {
tests := []struct {
name string
input []byte
wantErr bool
}{
{
name: "extra_fields",
input: []byte(`{"TCP": {}, "UnknownField": "value"}`),
wantErr: false, // JSON unmarshaling ignores unknown fields by default
},
{
name: "numeric_string",
input: []byte(`"123"`),
wantErr: true,
},
{
name: "boolean",
input: []byte(`true`),
wantErr: true,
},
{
name: "nested_null",
input: []byte(`{"TCP": null, "Web": null}`),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := getServeConfigFromJSON(tt.input)
if tt.wantErr && err == nil {
t.Error("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func TestGetServeConfigFromJSON_WhitespaceHandling(t *testing.T) {
tests := []struct {
name string
input []byte
}{
{"leading_whitespace", []byte(` {}`)},"trailing_whitespace", []byte(`{} `)},
{"newlines", []byte("{\n\t\"TCP\": {}\n}")},
{"mixed_whitespace", []byte(" \n\t{\n \"Web\": {} \n}\t ")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := getServeConfigFromJSON(tt.input)
if err != nil {
t.Errorf("whitespace should not cause error: %v", err)
}
if config == nil {
t.Error("should return non-nil config")
}
})
}
}