mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-08 00:50:20 +09:00
9aba06ec7d
Regroup the CLI tests into per-command files and cover every command except debug/config with CRUD, flag and error permutations, asserted as JSON.
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package integration
|
|
|
|
import (
|
|
"cmp"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/juanfont/headscale/integration/hsic"
|
|
"github.com/juanfont/headscale/integration/tsic"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// This file holds the shared helpers used by the per-command CLI integration
|
|
// test files (cli_users_test.go, cli_nodes_test.go, cli_apikeys_test.go,
|
|
// cli_preauthkeys_test.go, cli_auth_test.go, cli_server_test.go and
|
|
// cli_policy_test.go). The tests themselves live in those files, grouped by
|
|
// the command they exercise.
|
|
//
|
|
// The whole point of the CLI test suite is to guard the transport: every
|
|
// command is invoked with `--output json` and the result is unmarshalled into
|
|
// the matching gen/go/headscale/v1 Go type, so a change to the gRPC handlers,
|
|
// proto definitions or output encoders that breaks a command is caught here.
|
|
|
|
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
|
str, err := headscale.Execute(command)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(str), result)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal: %w\n command err: %s", err, str)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// assertJSONRoundtrip executes command (which must include `--output json`),
|
|
// decodes the stdout into T, then marshals T back to JSON and re-decodes it,
|
|
// asserting the serialisation is stable. This is the transport contract guard:
|
|
// if the underlying v1 type drifts in a way that loses data, the round-trip
|
|
// breaks. The decoded value is returned so callers can assert on real fields.
|
|
func assertJSONRoundtrip[T any](t require.TestingT, headscale ControlServer, command []string) T {
|
|
var first T
|
|
|
|
err := executeAndUnmarshal(headscale, command, &first)
|
|
require.NoError(t, err, "decoding CLI json output")
|
|
|
|
firstBytes, err := json.Marshal(first)
|
|
require.NoError(t, err, "re-marshalling decoded value")
|
|
|
|
var second T
|
|
|
|
require.NoError(t, json.Unmarshal(firstBytes, &second), "re-decoding marshalled value")
|
|
|
|
secondBytes, err := json.Marshal(second)
|
|
require.NoError(t, err, "re-marshalling round-tripped value")
|
|
|
|
require.JSONEq(t, string(firstBytes), string(secondBytes), "json round-trip should be stable")
|
|
|
|
return second
|
|
}
|
|
|
|
// Interface ensuring that we can sort structs from gRPC that
|
|
// have an ID field.
|
|
type GRPCSortable interface {
|
|
GetId() uint64
|
|
}
|
|
|
|
func sortWithID[T GRPCSortable](a, b T) int {
|
|
return cmp.Compare(a.GetId(), b.GetId())
|
|
}
|
|
|
|
// setupCLIScenario boots a scenario with the given users and nodes-per-user,
|
|
// creates the headscale environment and returns the running scenario and its
|
|
// control server. It removes the repeated NewScenario/CreateHeadscaleEnv/
|
|
// Headscale boilerplate shared by the CLI tests. Callers still defer
|
|
// scenario.ShutdownAssertNoPanics(t) themselves so the cleanup is visible at
|
|
// the call site.
|
|
func setupCLIScenario(t *testing.T, testName string, users []string, nodesPerUser int) (*Scenario, ControlServer) {
|
|
t.Helper()
|
|
|
|
spec := ScenarioSpec{
|
|
Users: users,
|
|
NodesPerUser: nodesPerUser,
|
|
}
|
|
|
|
scenario, err := NewScenario(spec)
|
|
require.NoError(t, err)
|
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName(testName))
|
|
require.NoError(t, err)
|
|
|
|
headscale, err := scenario.Headscale()
|
|
require.NoError(t, err)
|
|
|
|
return scenario, headscale
|
|
}
|