mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-22 17:39:39 +01:00
init
This commit is contained in:
423
integration/debug_cli_test.go
Normal file
423
integration/debug_cli_test.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDebugCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"debug-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clidebug"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_debug_help", func(t *testing.T) {
|
||||
// Test debug command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "debug", "help should mention debug command")
|
||||
assert.Contains(t, result, "debug and testing commands", "help should contain command description")
|
||||
assert.Contains(t, result, "create-node", "help should mention create-node subcommand")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_help", func(t *testing.T) {
|
||||
// Test debug create-node command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "create-node", "help should mention create-node command")
|
||||
assert.Contains(t, result, "name", "help should mention name flag")
|
||||
assert.Contains(t, result, "user", "help should mention user flag")
|
||||
assert.Contains(t, result, "key", "help should mention key flag")
|
||||
assert.Contains(t, result, "route", "help should mention route flag")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"debug-create-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clidebugcreate"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Create a user first
|
||||
user := spec.Users[0]
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_debug_create_node_basic", func(t *testing.T) {
|
||||
// Test basic debug create-node functionality
|
||||
nodeName := "debug-test-node"
|
||||
// Generate a mock registration key (64 hex chars with nodekey prefix)
|
||||
registrationKey := "nodekey:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should output node creation confirmation
|
||||
assert.Contains(t, result, "Node created", "should confirm node creation")
|
||||
assert.Contains(t, result, nodeName, "should mention the created node name")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_with_routes", func(t *testing.T) {
|
||||
// Test debug create-node with advertised routes
|
||||
nodeName := "debug-route-node"
|
||||
registrationKey := "nodekey:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
"--route", "10.0.0.0/24",
|
||||
"--route", "192.168.1.0/24",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should output node creation confirmation
|
||||
assert.Contains(t, result, "Node created", "should confirm node creation")
|
||||
assert.Contains(t, result, nodeName, "should mention the created node name")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_json_output", func(t *testing.T) {
|
||||
// Test debug create-node with JSON output
|
||||
nodeName := "debug-json-node"
|
||||
registrationKey := "nodekey:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should produce valid JSON output
|
||||
var node v1.Node
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
assert.NoError(t, err, "debug create-node should produce valid JSON output")
|
||||
assert.Equal(t, nodeName, node.GetName(), "created node should have correct name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"debug-validation-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clidebugvalidation"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Create a user first
|
||||
user := spec.Users[0]
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_debug_create_node_missing_name", func(t *testing.T) {
|
||||
// Test debug create-node with missing name flag
|
||||
registrationKey := "nodekey:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
// Should fail for missing required name flag
|
||||
assert.Error(t, err, "should fail for missing name flag")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_missing_user", func(t *testing.T) {
|
||||
// Test debug create-node with missing user flag
|
||||
registrationKey := "nodekey:2222222222222222222222222222222222222222222222222222222222222222"
|
||||
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", "test-node",
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
// Should fail for missing required user flag
|
||||
assert.Error(t, err, "should fail for missing user flag")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_missing_key", func(t *testing.T) {
|
||||
// Test debug create-node with missing key flag
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", "test-node",
|
||||
"--user", user,
|
||||
},
|
||||
)
|
||||
// Should fail for missing required key flag
|
||||
assert.Error(t, err, "should fail for missing key flag")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_invalid_key", func(t *testing.T) {
|
||||
// Test debug create-node with invalid registration key format
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", "test-node",
|
||||
"--user", user,
|
||||
"--key", "invalid-key-format",
|
||||
},
|
||||
)
|
||||
// Should fail for invalid key format
|
||||
assert.Error(t, err, "should fail for invalid key format")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_nonexistent_user", func(t *testing.T) {
|
||||
// Test debug create-node with non-existent user
|
||||
registrationKey := "nodekey:3333333333333333333333333333333333333333333333333333333333333333"
|
||||
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", "test-node",
|
||||
"--user", "nonexistent-user",
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
// Should fail for non-existent user
|
||||
assert.Error(t, err, "should fail for non-existent user")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_duplicate_name", func(t *testing.T) {
|
||||
// Test debug create-node with duplicate node name
|
||||
nodeName := "duplicate-node"
|
||||
registrationKey1 := "nodekey:4444444444444444444444444444444444444444444444444444444444444444"
|
||||
registrationKey2 := "nodekey:5555555555555555555555555555555555555555555555555555555555555555"
|
||||
|
||||
// Create first node
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey1,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Try to create second node with same name
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey2,
|
||||
},
|
||||
)
|
||||
// Should fail for duplicate node name
|
||||
assert.Error(t, err, "should fail for duplicate node name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDebugCreateNodeCommandEdgeCases(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"debug-edge-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clidebugedge"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Create a user first
|
||||
user := spec.Users[0]
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_debug_create_node_invalid_route", func(t *testing.T) {
|
||||
// Test debug create-node with invalid route format
|
||||
nodeName := "invalid-route-node"
|
||||
registrationKey := "nodekey:6666666666666666666666666666666666666666666666666666666666666666"
|
||||
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
"--route", "invalid-cidr",
|
||||
},
|
||||
)
|
||||
// Should handle invalid route format gracefully
|
||||
assert.Error(t, err, "should fail for invalid route format")
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_empty_route", func(t *testing.T) {
|
||||
// Test debug create-node with empty route
|
||||
nodeName := "empty-route-node"
|
||||
registrationKey := "nodekey:7777777777777777777777777777777777777777777777777777777777777777"
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", nodeName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
"--route", "",
|
||||
},
|
||||
)
|
||||
// Should handle empty route (either succeed or fail gracefully)
|
||||
if err == nil {
|
||||
assert.Contains(t, result, "Node created", "should confirm node creation if empty route is allowed")
|
||||
} else {
|
||||
assert.Error(t, err, "should fail gracefully for empty route")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_debug_create_node_very_long_name", func(t *testing.T) {
|
||||
// Test debug create-node with very long node name
|
||||
longName := fmt.Sprintf("very-long-node-name-%s", "x")
|
||||
for i := 0; i < 10; i++ {
|
||||
longName += "-very-long-segment"
|
||||
}
|
||||
registrationKey := "nodekey:8888888888888888888888888888888888888888888888888888888888888888"
|
||||
|
||||
_, _ = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", longName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
// Should handle very long names (either succeed or fail gracefully)
|
||||
assert.NotPanics(t, func() {
|
||||
headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"debug",
|
||||
"create-node",
|
||||
"--name", longName,
|
||||
"--user", user,
|
||||
"--key", registrationKey,
|
||||
},
|
||||
)
|
||||
}, "should handle very long node names gracefully")
|
||||
})
|
||||
}
|
||||
391
integration/generate_cli_test.go
Normal file
391
integration/generate_cli_test.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"generate-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cligenerate"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_generate_help", func(t *testing.T) {
|
||||
// Test generate command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "generate", "help should mention generate command")
|
||||
assert.Contains(t, result, "Generate commands", "help should contain command description")
|
||||
assert.Contains(t, result, "private-key", "help should mention private-key subcommand")
|
||||
})
|
||||
|
||||
t.Run("test_generate_alias", func(t *testing.T) {
|
||||
// Test generate command alias (gen)
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"gen",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should work with alias
|
||||
assert.Contains(t, result, "generate", "alias should work and show generate help")
|
||||
assert.Contains(t, result, "private-key", "alias help should mention private-key subcommand")
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_help", func(t *testing.T) {
|
||||
// Test generate private-key command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "private-key", "help should mention private-key command")
|
||||
assert.Contains(t, result, "Generate a private key", "help should contain command description")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"generate-key-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cligenkey"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_generate_private_key_basic", func(t *testing.T) {
|
||||
// Test basic private key generation
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should output a private key
|
||||
assert.NotEmpty(t, result, "private key generation should produce output")
|
||||
|
||||
// Private key should start with expected prefix
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"private key should start with 'privkey:' prefix, got: %s", trimmed)
|
||||
|
||||
// Should be reasonable length (64+ hex characters after prefix)
|
||||
assert.True(t, len(trimmed) > 70,
|
||||
"private key should be reasonable length, got length: %d", len(trimmed))
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_json", func(t *testing.T) {
|
||||
// Test private key generation with JSON output
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should produce valid JSON output
|
||||
var keyData map[string]interface{}
|
||||
err = json.Unmarshal([]byte(result), &keyData)
|
||||
assert.NoError(t, err, "private key generation should produce valid JSON output")
|
||||
|
||||
// Should contain private_key field
|
||||
privateKey, exists := keyData["private_key"]
|
||||
assert.True(t, exists, "JSON output should contain 'private_key' field")
|
||||
assert.NotEmpty(t, privateKey, "private_key field should not be empty")
|
||||
|
||||
// Private key should be a string with correct format
|
||||
privateKeyStr, ok := privateKey.(string)
|
||||
assert.True(t, ok, "private_key should be a string")
|
||||
assert.True(t, strings.HasPrefix(privateKeyStr, "privkey:"),
|
||||
"private key should start with 'privkey:' prefix")
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_yaml", func(t *testing.T) {
|
||||
// Test private key generation with YAML output
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
"--output", "yaml",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should produce YAML output
|
||||
assert.NotEmpty(t, result, "YAML output should not be empty")
|
||||
assert.Contains(t, result, "private_key:", "YAML output should contain private_key field")
|
||||
assert.Contains(t, result, "privkey:", "YAML output should contain private key with correct prefix")
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_multiple_calls", func(t *testing.T) {
|
||||
// Test that multiple calls generate different keys
|
||||
var keys []string
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
trimmed := strings.TrimSpace(result)
|
||||
keys = append(keys, trimmed)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"each generated private key should have correct prefix")
|
||||
}
|
||||
|
||||
// All keys should be different
|
||||
assert.NotEqual(t, keys[0], keys[1], "generated keys should be different")
|
||||
assert.NotEqual(t, keys[1], keys[2], "generated keys should be different")
|
||||
assert.NotEqual(t, keys[0], keys[2], "generated keys should be different")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGeneratePrivateKeyCommandValidation(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"generate-validation-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cligenvalidation"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_generate_private_key_with_extra_args", func(t *testing.T) {
|
||||
// Test private key generation with unexpected extra arguments
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
"extra",
|
||||
"args",
|
||||
},
|
||||
)
|
||||
|
||||
// Should either succeed (ignoring extra args) or fail gracefully
|
||||
if err == nil {
|
||||
// If successful, should still produce valid key
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"should produce valid private key even with extra args")
|
||||
} else {
|
||||
// If failed, should be a reasonable error, not a panic
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic on extra arguments")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_invalid_output_format", func(t *testing.T) {
|
||||
// Test private key generation with invalid output format
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
"--output", "invalid-format",
|
||||
},
|
||||
)
|
||||
|
||||
// Should handle invalid output format gracefully
|
||||
// Might succeed with default format or fail gracefully
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, result, "should produce some output even with invalid format")
|
||||
} else {
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic on invalid output format")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_generate_private_key_with_config_flag", func(t *testing.T) {
|
||||
// Test that private key generation works with config flag
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"--config", "/etc/headscale/config.yaml",
|
||||
"generate",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should still generate valid private key
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"should generate valid private key with config flag")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"generate-edge-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cligenedge"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_generate_without_subcommand", func(t *testing.T) {
|
||||
// Test generate command without subcommand
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
},
|
||||
)
|
||||
|
||||
// Should show help or list available subcommands
|
||||
if err == nil {
|
||||
assert.Contains(t, result, "private-key", "should show available subcommands")
|
||||
} else {
|
||||
// If it errors, should be a usage error, not a crash
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic when no subcommand provided")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_generate_nonexistent_subcommand", func(t *testing.T) {
|
||||
// Test generate command with non-existent subcommand
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"nonexistent-command",
|
||||
},
|
||||
)
|
||||
|
||||
// Should fail gracefully for non-existent subcommand
|
||||
assert.Error(t, err, "should fail for non-existent subcommand")
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic on non-existent subcommand")
|
||||
})
|
||||
|
||||
t.Run("test_generate_key_format_consistency", func(t *testing.T) {
|
||||
// Test that generated keys are consistently formatted
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
trimmed := strings.TrimSpace(result)
|
||||
|
||||
// Check format consistency
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"private key should start with 'privkey:' prefix")
|
||||
|
||||
// Should be hex characters after prefix
|
||||
keyPart := strings.TrimPrefix(trimmed, "privkey:")
|
||||
assert.True(t, len(keyPart) == 64,
|
||||
"private key should be 64 hex characters after prefix, got length: %d", len(keyPart))
|
||||
|
||||
// Should only contain valid hex characters
|
||||
for _, char := range keyPart {
|
||||
assert.True(t,
|
||||
(char >= '0' && char <= '9') ||
|
||||
(char >= 'a' && char <= 'f') ||
|
||||
(char >= 'A' && char <= 'F'),
|
||||
"private key should only contain hex characters, found: %c", char)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_generate_alias_consistency", func(t *testing.T) {
|
||||
// Test that 'gen' alias produces same results as 'generate'
|
||||
result1, err1 := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"generate",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err1)
|
||||
|
||||
result2, err2 := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"gen",
|
||||
"private-key",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err2)
|
||||
|
||||
// Both should produce valid keys (though different values)
|
||||
trimmed1 := strings.TrimSpace(result1)
|
||||
trimmed2 := strings.TrimSpace(result2)
|
||||
|
||||
assert.True(t, strings.HasPrefix(trimmed1, "privkey:"),
|
||||
"generate command should produce valid key")
|
||||
assert.True(t, strings.HasPrefix(trimmed2, "privkey:"),
|
||||
"gen alias should produce valid key")
|
||||
|
||||
// Keys should be different (they're randomly generated)
|
||||
assert.NotEqual(t, trimmed1, trimmed2,
|
||||
"different calls should produce different keys")
|
||||
})
|
||||
}
|
||||
309
integration/routes_cli_test.go
Normal file
309
integration/routes_cli_test.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouteCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"route-user"},
|
||||
NodesPerUser: 1,
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(
|
||||
[]tsic.Option{tsic.WithAcceptRoutes()},
|
||||
hsic.WithTestName("cliroutes"),
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Wait for setup to complete
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Wait for node to be registered
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var listNodes []*v1.Node
|
||||
err := executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&listNodes,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, listNodes, 1)
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
|
||||
// Get the node ID for route operations
|
||||
var listNodes []*v1.Node
|
||||
err = executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&listNodes,
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
require.Len(t, listNodes, 1)
|
||||
nodeID := listNodes[0].GetId()
|
||||
|
||||
t.Run("test_route_advertisement", func(t *testing.T) {
|
||||
// Get the first tailscale client
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
assertNoErr(t, err)
|
||||
require.NotEmpty(t, allClients, "should have at least one client")
|
||||
client := allClients[0]
|
||||
|
||||
// Advertise a route
|
||||
_, _, err = client.Execute([]string{
|
||||
"tailscale",
|
||||
"set",
|
||||
"--advertise-routes=10.0.0.0/24",
|
||||
})
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Wait for route to appear in Headscale
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var updatedNodes []*v1.Node
|
||||
err := executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&updatedNodes,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, updatedNodes, 1)
|
||||
assert.Greater(c, len(updatedNodes[0].GetAvailableRoutes()), 0, "node should have available routes")
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
})
|
||||
|
||||
t.Run("test_route_approval", func(t *testing.T) {
|
||||
// List available routes
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Approve a route
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--routes",
|
||||
"10.0.0.0/24",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify route is approved
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var updatedNodes []*v1.Node
|
||||
err := executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&updatedNodes,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, updatedNodes, 1)
|
||||
assert.Contains(c, updatedNodes[0].GetApprovedRoutes(), "10.0.0.0/24", "route should be approved")
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
})
|
||||
|
||||
t.Run("test_route_removal", func(t *testing.T) {
|
||||
// Remove approved routes
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--routes",
|
||||
"", // Empty string removes all routes
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify routes are removed
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var updatedNodes []*v1.Node
|
||||
err := executeAndUnmarshal(headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&updatedNodes,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, updatedNodes, 1)
|
||||
assert.Empty(c, updatedNodes[0].GetApprovedRoutes(), "approved routes should be empty")
|
||||
}, 30*time.Second, 1*time.Second)
|
||||
})
|
||||
|
||||
t.Run("test_route_json_output", func(t *testing.T) {
|
||||
// Test JSON output for route commands
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify JSON output is valid
|
||||
var routes interface{}
|
||||
err = json.Unmarshal([]byte(result), &routes)
|
||||
assert.NoError(t, err, "route command should produce valid JSON output")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteCommandEdgeCases(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"route-test-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliroutesedge"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_route_commands_with_invalid_node", func(t *testing.T) {
|
||||
// Test route commands with non-existent node ID
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
"999999",
|
||||
},
|
||||
)
|
||||
// Should handle error gracefully
|
||||
assert.Error(t, err, "should fail for non-existent node")
|
||||
})
|
||||
|
||||
t.Run("test_route_approval_invalid_routes", func(t *testing.T) {
|
||||
// Test route approval with invalid CIDR
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
"1",
|
||||
"--routes",
|
||||
"invalid-cidr",
|
||||
},
|
||||
)
|
||||
// Should handle invalid CIDR gracefully
|
||||
assert.Error(t, err, "should fail for invalid CIDR")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteCommandHelp(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"help-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliroutehelp"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_list_routes_help", func(t *testing.T) {
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify help text contains expected information
|
||||
assert.Contains(t, result, "list-routes", "help should mention list-routes command")
|
||||
assert.Contains(t, result, "identifier", "help should mention identifier flag")
|
||||
})
|
||||
|
||||
t.Run("test_approve_routes_help", func(t *testing.T) {
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify help text contains expected information
|
||||
assert.Contains(t, result, "approve-routes", "help should mention approve-routes command")
|
||||
assert.Contains(t, result, "identifier", "help should mention identifier flag")
|
||||
assert.Contains(t, result, "routes", "help should mention routes flag")
|
||||
})
|
||||
}
|
||||
372
integration/serve_cli_test.go
Normal file
372
integration/serve_cli_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServeCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"serve-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliserve"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_serve_help", func(t *testing.T) {
|
||||
// Test serve command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"serve",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "serve", "help should mention serve command")
|
||||
assert.Contains(t, result, "Launches the headscale server", "help should contain command description")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeCommandValidation(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"serve-validation-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliservevalidation"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_serve_with_invalid_config", func(t *testing.T) {
|
||||
// Test serve command with invalid config file
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"--config", "/nonexistent/config.yaml",
|
||||
"serve",
|
||||
},
|
||||
)
|
||||
// Should fail for invalid config file
|
||||
assert.Error(t, err, "should fail for invalid config file")
|
||||
})
|
||||
|
||||
t.Run("test_serve_with_extra_args", func(t *testing.T) {
|
||||
// Test serve command with unexpected extra arguments
|
||||
// Note: This is a tricky test since serve runs a server
|
||||
// We'll test that it accepts extra args without crashing immediately
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use a goroutine to test that the command doesn't immediately fail
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"serve",
|
||||
"extra",
|
||||
"args",
|
||||
},
|
||||
)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
// If it returns an error quickly, it should be about args validation
|
||||
// or config issues, not a panic
|
||||
if err != nil {
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic on extra arguments")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// If it times out, that's actually good - it means the server started
|
||||
// and didn't immediately crash due to extra arguments
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeCommandHealthCheck(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"serve-health-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliservehealth"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_serve_health_endpoint", func(t *testing.T) {
|
||||
// Test that the serve command starts a server that responds to health checks
|
||||
// This is effectively testing that the server is running and accessible
|
||||
|
||||
// Get the server endpoint
|
||||
endpoint := headscale.GetEndpoint()
|
||||
assert.NotEmpty(t, endpoint, "headscale endpoint should not be empty")
|
||||
|
||||
// Make a simple HTTP request to verify the server is running
|
||||
healthURL := fmt.Sprintf("%s/health", endpoint)
|
||||
|
||||
// Use a timeout to avoid hanging
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(healthURL)
|
||||
if err != nil {
|
||||
// If we can't connect, check if it's because server isn't ready
|
||||
assert.Contains(t, err.Error(), "connection",
|
||||
"health check failure should be connection-related if server not ready")
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
// If we can connect, verify we get a reasonable response
|
||||
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500,
|
||||
"health endpoint should return reasonable status code")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_serve_api_endpoint", func(t *testing.T) {
|
||||
// Test that the serve command starts a server with API endpoints
|
||||
endpoint := headscale.GetEndpoint()
|
||||
assert.NotEmpty(t, endpoint, "headscale endpoint should not be empty")
|
||||
|
||||
// Try to access a known API endpoint (version info)
|
||||
// This tests that the gRPC gateway is running
|
||||
versionURL := fmt.Sprintf("%s/api/v1/version", endpoint)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(versionURL)
|
||||
if err != nil {
|
||||
// Connection errors are acceptable if server isn't fully ready
|
||||
assert.Contains(t, err.Error(), "connection",
|
||||
"API endpoint failure should be connection-related if server not ready")
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
// If we can connect, check that we get some response
|
||||
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500,
|
||||
"API endpoint should return reasonable status code")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeCommandServerBehavior(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"serve-behavior-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliservebenavior"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_serve_accepts_connections", func(t *testing.T) {
|
||||
// Test that the server accepts connections from clients
|
||||
// This is a basic integration test to ensure serve works
|
||||
|
||||
// Create a user for testing
|
||||
user := spec.Users[0]
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Create a pre-auth key
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"preauthkeys",
|
||||
"create",
|
||||
"--user", user,
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Verify the preauth key creation worked
|
||||
assert.NotEmpty(t, result, "preauth key creation should produce output")
|
||||
assert.Contains(t, result, "key", "preauth key output should contain key field")
|
||||
})
|
||||
|
||||
t.Run("test_serve_handles_node_operations", func(t *testing.T) {
|
||||
// Test that the server can handle basic node operations
|
||||
_ = spec.Users[0] // Test user for context
|
||||
|
||||
// List nodes (should work even if empty)
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list",
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should return valid JSON array (even if empty)
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"),
|
||||
"nodes list should return JSON array")
|
||||
})
|
||||
|
||||
t.Run("test_serve_handles_user_operations", func(t *testing.T) {
|
||||
// Test that the server can handle user operations
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"list",
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Should return valid JSON array
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"),
|
||||
"users list should return JSON array")
|
||||
|
||||
// Should contain our test user
|
||||
assert.Contains(t, result, spec.Users[0], "users list should contain test user")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeCommandEdgeCases(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"serve-edge-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliserverecge"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_serve_multiple_rapid_commands", func(t *testing.T) {
|
||||
// Test that the server can handle multiple rapid commands
|
||||
// This tests the server's ability to handle concurrent requests
|
||||
user := spec.Users[0]
|
||||
|
||||
// Create user first
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
user,
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Execute multiple commands rapidly
|
||||
for i := 0; i < 3; i++ {
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"list",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
assert.Contains(t, result, user, "users list should consistently contain test user")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_serve_handles_empty_commands", func(t *testing.T) {
|
||||
// Test that the server gracefully handles edge case commands
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Basic help should work
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"--version",
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, result, "version command should produce output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_serve_handles_malformed_requests", func(t *testing.T) {
|
||||
// Test that the server handles malformed CLI requests gracefully
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nonexistent-command",
|
||||
},
|
||||
)
|
||||
// Should fail gracefully for non-existent commands
|
||||
assert.Error(t, err, "should fail gracefully for non-existent commands")
|
||||
|
||||
// Should not cause server to crash (we can still execute other commands)
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"users",
|
||||
"list",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
assert.NotEmpty(t, result, "server should still work after malformed request")
|
||||
})
|
||||
}
|
||||
143
integration/version_cli_test.go
Normal file
143
integration/version_cli_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"version-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliversion"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_version_basic", func(t *testing.T) {
|
||||
// Test basic version output
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Version output should contain version information
|
||||
assert.NotEmpty(t, result, "version output should not be empty")
|
||||
// In development, version is "dev", in releases it would be semver like "1.0.0"
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, trimmed == "dev" || len(trimmed) > 2, "version should be 'dev' or valid version string")
|
||||
})
|
||||
|
||||
t.Run("test_version_help", func(t *testing.T) {
|
||||
// Test version command help
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
"--help",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Help text should contain expected information
|
||||
assert.Contains(t, result, "version", "help should mention version command")
|
||||
assert.Contains(t, result, "version of headscale", "help should contain command description")
|
||||
})
|
||||
|
||||
t.Run("test_version_with_extra_args", func(t *testing.T) {
|
||||
// Test version command with unexpected extra arguments
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
"extra",
|
||||
"args",
|
||||
},
|
||||
)
|
||||
// Should either ignore extra args or handle gracefully
|
||||
// The exact behavior depends on implementation, but shouldn't crash
|
||||
assert.NotPanics(t, func() {
|
||||
headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
"extra",
|
||||
"args",
|
||||
},
|
||||
)
|
||||
}, "version command should handle extra arguments gracefully")
|
||||
|
||||
// If it succeeds, should still contain version info
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, result, "version output should not be empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersionCommandEdgeCases(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
spec := ScenarioSpec{
|
||||
Users: []string{"version-edge-user"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
assertNoErr(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cliversionedge"))
|
||||
assertNoErr(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Run("test_version_multiple_calls", func(t *testing.T) {
|
||||
// Test that version command can be called multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
assert.NotEmpty(t, result, "version output should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_version_with_invalid_flag", func(t *testing.T) {
|
||||
// Test version command with invalid flag
|
||||
_, _ = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
"--invalid-flag",
|
||||
},
|
||||
)
|
||||
// Should handle invalid flag gracefully (either succeed ignoring flag or fail with error)
|
||||
assert.NotPanics(t, func() {
|
||||
headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"version",
|
||||
"--invalid-flag",
|
||||
},
|
||||
)
|
||||
}, "version command should handle invalid flags gracefully")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user