This commit is contained in:
kradalby
2025-07-14 07:48:32 +00:00
parent 044193bf34
commit 60521283ab
28 changed files with 8772 additions and 0 deletions

View 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")
})
}

View 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")
})
}

View 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")
})
}

View 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")
})
}

View 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")
})
}