mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-29 12:53:57 +00:00
310 lines
6.9 KiB
Go
310 lines
6.9 KiB
Go
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",
|
|
"--node",
|
|
fmt.Sprintf("%d", nodeID),
|
|
},
|
|
)
|
|
assertNoErr(t, err)
|
|
|
|
// Approve a route
|
|
_, err = headscale.Execute(
|
|
[]string{
|
|
"headscale",
|
|
"nodes",
|
|
"approve-routes",
|
|
"--node",
|
|
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",
|
|
"--node",
|
|
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",
|
|
"--node",
|
|
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",
|
|
"--node",
|
|
"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",
|
|
"--node",
|
|
"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, "node", "help should mention node 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, "node", "help should mention node flag")
|
|
assert.Contains(t, result, "routes", "help should mention routes flag")
|
|
})
|
|
}
|