This commit is contained in:
Kristoffer Dalby
2025-07-15 14:51:23 +00:00
parent 024ed59ea9
commit 8253d588c6
31 changed files with 300 additions and 364 deletions

View File

@@ -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),

View File

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

View File

@@ -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)

View File

@@ -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",

View File

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

View File

@@ -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), ","),
}

View File

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

View File

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

View File

@@ -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

View File

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