mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-24 19:36:05 -05:00
derp
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestUserCommand(t *testing.T) {
|
||||
"users",
|
||||
"rename",
|
||||
"--output=json",
|
||||
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
|
||||
fmt.Sprintf("--user=%d", listUsers[1].GetId()),
|
||||
"--new-name=newname",
|
||||
},
|
||||
)
|
||||
@@ -161,7 +161,7 @@ func TestUserCommand(t *testing.T) {
|
||||
"list",
|
||||
"--output",
|
||||
"json",
|
||||
"--identifier=1",
|
||||
"--user=1",
|
||||
},
|
||||
&listByID,
|
||||
)
|
||||
@@ -187,7 +187,7 @@ func TestUserCommand(t *testing.T) {
|
||||
"destroy",
|
||||
"--force",
|
||||
// Delete "user1"
|
||||
"--identifier=1",
|
||||
"--user=1",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
@@ -354,7 +354,10 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"tag:test1", "tag:test2"}, listedPreAuthKeys[index].GetAclTags())
|
||||
// Sort tags for consistent comparison
|
||||
tags := listedPreAuthKeys[index].GetAclTags()
|
||||
slices.Sort(tags)
|
||||
assert.Equal(t, []string{"tag:test1", "tag:test2"}, tags)
|
||||
}
|
||||
|
||||
// Test key expiry
|
||||
@@ -604,7 +607,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
status, err := client.Status()
|
||||
assert.NoError(ct, err)
|
||||
assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState,
|
||||
assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState,
|
||||
"Expected node to be logged out, backend state: %s", status.BackendState)
|
||||
}, 30*time.Second, 2*time.Second)
|
||||
|
||||
@@ -869,7 +872,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"tag",
|
||||
"-i", "1",
|
||||
"--node", "1",
|
||||
"-t", "tag:test",
|
||||
"--output", "json",
|
||||
},
|
||||
@@ -884,7 +887,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"tag",
|
||||
"-i", "2",
|
||||
"--node", "2",
|
||||
"-t", "wrong-tag",
|
||||
"--output", "json",
|
||||
},
|
||||
@@ -1259,7 +1262,7 @@ func TestNodeCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"delete",
|
||||
"--identifier",
|
||||
"--node",
|
||||
// Delete the last added machine
|
||||
"4",
|
||||
"--output",
|
||||
@@ -1385,7 +1388,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"expire",
|
||||
"--identifier",
|
||||
"--node",
|
||||
strconv.FormatUint(listAll[idx].GetId(), 10),
|
||||
},
|
||||
)
|
||||
@@ -1511,7 +1514,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"rename",
|
||||
"--identifier",
|
||||
"--node",
|
||||
strconv.FormatUint(listAll[idx].GetId(), 10),
|
||||
fmt.Sprintf("newnode-%d", idx+1),
|
||||
},
|
||||
@@ -1549,7 +1552,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"rename",
|
||||
"--identifier",
|
||||
"--node",
|
||||
strconv.FormatUint(listAll[4].GetId(), 10),
|
||||
strings.Repeat("t", 64),
|
||||
},
|
||||
@@ -1649,7 +1652,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"move",
|
||||
"--identifier",
|
||||
"--node",
|
||||
strconv.FormatUint(node.GetId(), 10),
|
||||
"--user",
|
||||
strconv.FormatUint(userMap["new-user"].GetId(), 10),
|
||||
@@ -1687,7 +1690,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"move",
|
||||
"--identifier",
|
||||
"--node",
|
||||
nodeID,
|
||||
"--user",
|
||||
"999",
|
||||
@@ -1708,7 +1711,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"move",
|
||||
"--identifier",
|
||||
"--node",
|
||||
nodeID,
|
||||
"--user",
|
||||
strconv.FormatUint(userMap["old-user"].GetId(), 10),
|
||||
@@ -1727,7 +1730,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"move",
|
||||
"--identifier",
|
||||
"--node",
|
||||
nodeID,
|
||||
"--user",
|
||||
strconv.FormatUint(userMap["old-user"].GetId(), 10),
|
||||
|
||||
@@ -38,10 +38,10 @@ func TestDebugCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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, "debugging and testing", "help should contain command description")
|
||||
assert.Contains(t, result, "create-node", "help should mention create-node subcommand")
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestDebugCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -100,7 +100,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
nodeName := "debug-test-node"
|
||||
// Generate a mock registration key (64 hex chars with nodekey prefix)
|
||||
registrationKey := "nodekey:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -112,7 +112,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -122,7 +122,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
// Test debug create-node with advertised routes
|
||||
nodeName := "debug-route-node"
|
||||
registrationKey := "nodekey:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -136,7 +136,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -146,7 +146,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
// Test debug create-node with JSON output
|
||||
nodeName := "debug-json-node"
|
||||
registrationKey := "nodekey:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
|
||||
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -159,7 +159,7 @@ func TestDebugCreateNodeCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Should produce valid JSON output
|
||||
var node v1.Node
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
@@ -200,7 +200,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
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",
|
||||
@@ -217,7 +217,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
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",
|
||||
@@ -265,7 +265,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
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",
|
||||
@@ -285,7 +285,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
nodeName := "duplicate-node"
|
||||
registrationKey1 := "nodekey:4444444444444444444444444444444444444444444444444444444444444444"
|
||||
registrationKey2 := "nodekey:5555555555555555555555555555555555555555555555555555555555555555"
|
||||
|
||||
|
||||
// Create first node
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
@@ -298,7 +298,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Try to create second node with same name
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
@@ -348,7 +348,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) {
|
||||
// Test debug create-node with invalid route format
|
||||
nodeName := "invalid-route-node"
|
||||
registrationKey := "nodekey:6666666666666666666666666666666666666666666666666666666666666666"
|
||||
|
||||
|
||||
_, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -368,7 +368,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) {
|
||||
// Test debug create-node with empty route
|
||||
nodeName := "empty-route-node"
|
||||
registrationKey := "nodekey:7777777777777777777777777777777777777777777777777777777777777777"
|
||||
|
||||
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -395,7 +395,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) {
|
||||
longName += "-very-long-segment"
|
||||
}
|
||||
registrationKey := "nodekey:8888888888888888888888888888888888888888888888888888888888888888"
|
||||
|
||||
|
||||
_, _ = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -420,4 +420,4 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) {
|
||||
)
|
||||
}, "should handle very long node names gracefully")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,9 @@ func derpServerScenario(
|
||||
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
|
||||
|
||||
for _, health := range status.Health {
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
"Client %s should be connected to DERP relay", client.Hostname())
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
"Client %s should be connected to Headscale Embedded DERP", client.Hostname())
|
||||
}
|
||||
}, 30*time.Second, 2*time.Second)
|
||||
@@ -166,9 +166,9 @@ func derpServerScenario(
|
||||
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
|
||||
|
||||
for _, health := range status.Health {
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
"Client %s should be connected to DERP relay after first run", client.Hostname())
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
"Client %s should be connected to Headscale Embedded DERP after first run", client.Hostname())
|
||||
}
|
||||
}, 30*time.Second, 2*time.Second)
|
||||
@@ -191,9 +191,9 @@ func derpServerScenario(
|
||||
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
|
||||
|
||||
for _, health := range status.Health {
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
assert.NotContains(ct, health, "could not connect to any relay server",
|
||||
"Client %s should be connected to DERP relay after second run", client.Hostname())
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.",
|
||||
"Client %s should be connected to Headscale Embedded DERP after second run", client.Hostname())
|
||||
}
|
||||
}, 30*time.Second, 2*time.Second)
|
||||
|
||||
@@ -564,10 +564,10 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"node",
|
||||
"nodes",
|
||||
"rename",
|
||||
givenName,
|
||||
"--identifier",
|
||||
"--node",
|
||||
strconv.FormatUint(node.GetId(), 10),
|
||||
})
|
||||
assertNoErr(t, err)
|
||||
@@ -702,7 +702,7 @@ func TestExpireNode(t *testing.T) {
|
||||
// TODO(kradalby): This is Headscale specific and would not play nicely
|
||||
// with other implementations of the ControlServer interface
|
||||
result, err := headscale.Execute([]string{
|
||||
"headscale", "nodes", "expire", "--identifier", "1", "--output", "json",
|
||||
"headscale", "nodes", "expire", "--node", "1", "--output", "json",
|
||||
})
|
||||
assertNoErr(t, err)
|
||||
|
||||
@@ -1060,7 +1060,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"delete",
|
||||
"--identifier",
|
||||
"--node",
|
||||
// Delete the last added machine
|
||||
fmt.Sprintf("%d", nodeList[0].GetId()),
|
||||
"--output",
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestGenerateCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -54,7 +54,7 @@ func TestGenerateCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -71,7 +71,7 @@ func TestGenerateCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -105,17 +105,17 @@ func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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:"),
|
||||
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,
|
||||
assert.True(t, len(trimmed) > 70,
|
||||
"private key should be reasonable length, got length: %d", len(trimmed))
|
||||
})
|
||||
|
||||
@@ -130,21 +130,21 @@ func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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:"),
|
||||
assert.True(t, strings.HasPrefix(privateKeyStr, "privkey:"),
|
||||
"private key should start with 'privkey:' prefix")
|
||||
})
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -169,7 +169,7 @@ func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
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{
|
||||
@@ -179,13 +179,13 @@ func TestGeneratePrivateKeyCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
trimmed := strings.TrimSpace(result)
|
||||
keys = append(keys, trimmed)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
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")
|
||||
@@ -221,12 +221,12 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) {
|
||||
"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:"),
|
||||
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
|
||||
@@ -244,7 +244,7 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) {
|
||||
"--output", "invalid-format",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
// Should handle invalid output format gracefully
|
||||
// Might succeed with default format or fail gracefully
|
||||
if err == nil {
|
||||
@@ -265,10 +265,10 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Should still generate valid private key
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
"should generate valid private key with config flag")
|
||||
})
|
||||
}
|
||||
@@ -298,7 +298,7 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
"generate",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
// Should show help or list available subcommands
|
||||
if err == nil {
|
||||
assert.Contains(t, result, "private-key", "should show available subcommands")
|
||||
@@ -317,10 +317,12 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
"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")
|
||||
if err != nil {
|
||||
assert.NotContains(t, err.Error(), "panic", "should not panic on non-existent subcommand")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("test_generate_key_format_consistency", func(t *testing.T) {
|
||||
@@ -333,24 +335,24 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
trimmed := strings.TrimSpace(result)
|
||||
|
||||
|
||||
// Check format consistency
|
||||
assert.True(t, strings.HasPrefix(trimmed, "privkey:"),
|
||||
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,
|
||||
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'),
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -365,7 +367,7 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err1)
|
||||
|
||||
|
||||
result2, err2 := headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -374,18 +376,18 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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:"),
|
||||
|
||||
assert.True(t, strings.HasPrefix(trimmed1, "privkey:"),
|
||||
"generate command should produce valid key")
|
||||
assert.True(t, strings.HasPrefix(trimmed2, "privkey:"),
|
||||
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,
|
||||
assert.NotEqual(t, trimmed1, trimmed2,
|
||||
"different calls should produce different keys")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,7 +1122,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
|
||||
command := []string{
|
||||
"headscale", "nodes", "approve-routes",
|
||||
"--output", "json",
|
||||
"--identifier", strconv.FormatUint(id, 10),
|
||||
"--node", strconv.FormatUint(id, 10),
|
||||
"--routes=" + strings.Join(util.PrefixesToString(routes), ","),
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestRouteCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
},
|
||||
)
|
||||
@@ -124,7 +124,7 @@ func TestRouteCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--routes",
|
||||
"10.0.0.0/24",
|
||||
@@ -158,7 +158,7 @@ func TestRouteCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--routes",
|
||||
"", // Empty string removes all routes
|
||||
@@ -192,7 +192,7 @@ func TestRouteCommand(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
"--output",
|
||||
"json",
|
||||
@@ -231,7 +231,7 @@ func TestRouteCommandEdgeCases(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"list-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
"999999",
|
||||
},
|
||||
)
|
||||
@@ -246,7 +246,7 @@ func TestRouteCommandEdgeCases(t *testing.T) {
|
||||
"headscale",
|
||||
"nodes",
|
||||
"approve-routes",
|
||||
"--identifier",
|
||||
"--node",
|
||||
"1",
|
||||
"--routes",
|
||||
"invalid-cidr",
|
||||
@@ -284,10 +284,10 @@ func TestRouteCommandHelp(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
assert.Contains(t, result, "node", "help should mention node flag")
|
||||
})
|
||||
|
||||
t.Run("test_approve_routes_help", func(t *testing.T) {
|
||||
@@ -300,10 +300,10 @@ func TestRouteCommandHelp(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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, "node", "help should mention node flag")
|
||||
assert.Contains(t, result, "routes", "help should mention routes flag")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestServeCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -83,7 +83,7 @@ func TestServeCommandValidation(t *testing.T) {
|
||||
// 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() {
|
||||
@@ -97,7 +97,7 @@ func TestServeCommandValidation(t *testing.T) {
|
||||
)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
// If it returns an error quickly, it should be about args validation
|
||||
@@ -132,28 +132,28 @@ func TestServeCommandHealthCheck(t *testing.T) {
|
||||
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",
|
||||
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,
|
||||
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500,
|
||||
"health endpoint should return reasonable status code")
|
||||
}
|
||||
})
|
||||
@@ -162,24 +162,24 @@ func TestServeCommandHealthCheck(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",
|
||||
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,
|
||||
assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500,
|
||||
"API endpoint should return reasonable status code")
|
||||
}
|
||||
})
|
||||
@@ -205,7 +205,7 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
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(
|
||||
@@ -217,7 +217,7 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Create a pre-auth key
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
@@ -229,7 +229,7 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -238,7 +238,7 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
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{
|
||||
@@ -249,10 +249,10 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Should return valid JSON array (even if empty)
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"),
|
||||
assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"),
|
||||
"nodes list should return JSON array")
|
||||
})
|
||||
|
||||
@@ -267,12 +267,12 @@ func TestServeCommandServerBehavior(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Should return valid JSON array
|
||||
trimmed := strings.TrimSpace(result)
|
||||
assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"),
|
||||
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")
|
||||
})
|
||||
@@ -299,7 +299,7 @@ func TestServeCommandEdgeCases(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{
|
||||
@@ -310,7 +310,7 @@ func TestServeCommandEdgeCases(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Execute multiple commands rapidly
|
||||
for i := 0; i < 3; i++ {
|
||||
result, err := headscale.Execute(
|
||||
@@ -334,7 +334,7 @@ func TestServeCommandEdgeCases(t *testing.T) {
|
||||
},
|
||||
)
|
||||
assertNoErr(t, err)
|
||||
|
||||
|
||||
// Basic help should work
|
||||
result, err := headscale.Execute(
|
||||
[]string{
|
||||
@@ -357,7 +357,7 @@ func TestServeCommandEdgeCases(t *testing.T) {
|
||||
)
|
||||
// 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{
|
||||
@@ -369,4 +369,4 @@ func TestServeCommandEdgeCases(t *testing.T) {
|
||||
assertNoErr(t, err)
|
||||
assert.NotEmpty(t, result, "server should still work after malformed request")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
// derpPingTimeout defines the timeout for individual DERP ping operations
|
||||
// Used in DERP connectivity tests to verify relay server communication
|
||||
derpPingTimeout = 2 * time.Second
|
||||
|
||||
|
||||
// derpPingCount defines the number of ping attempts for DERP connectivity tests
|
||||
// Higher count provides better reliability assessment of DERP connectivity
|
||||
derpPingCount = 10
|
||||
@@ -317,7 +317,7 @@ func assertValidNetcheck(t *testing.T, client TailscaleClient) {
|
||||
|
||||
// assertCommandOutputContains executes a command with exponential backoff retry until the output
|
||||
// contains the expected string or timeout is reached (10 seconds).
|
||||
// This implements eventual consistency patterns and should be used instead of time.Sleep
|
||||
// This implements eventual consistency patterns and should be used instead of time.Sleep
|
||||
// before executing commands that depend on network state propagation.
|
||||
//
|
||||
// Timeout: 10 seconds with exponential backoff
|
||||
|
||||
@@ -35,10 +35,10 @@ func TestVersionCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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"
|
||||
// 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")
|
||||
})
|
||||
@@ -53,7 +53,7 @@ func TestVersionCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
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")
|
||||
@@ -81,7 +81,7 @@ func TestVersionCommand(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}, "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")
|
||||
@@ -140,4 +140,4 @@ func TestVersionCommandEdgeCases(t *testing.T) {
|
||||
)
|
||||
}, "version command should handle invalid flags gracefully")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user