cli: migrate the CLI and integration tests to the v1 HTTP API

Replace the gRPC client with the generated HTTP client across every
command: locally over the unix socket without auth (matching the previous
local gRPC socket), remotely over TLS with a Bearer API key. Output
rendering and integration tests move to the HTTP client types; the
transport changes, the assertions do not.
This commit is contained in:
Kristoffer Dalby
2026-06-19 06:13:50 +00:00
parent ba90048cfb
commit 8efa5ad1fe
35 changed files with 1654 additions and 1371 deletions
+29 -30
View File
@@ -2,14 +2,13 @@ package integration
import (
"encoding/json"
"fmt"
"slices"
"testing"
"time"
tcmp "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
@@ -36,7 +35,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
var (
listUsers []*v1.User
listUsers []*clientv1.User
result []string
)
@@ -54,7 +53,7 @@ func TestUserCommand(t *testing.T) {
assert.NoError(ct, err)
slices.SortFunc(listUsers, sortWithID)
result = []string{listUsers[0].GetName(), listUsers[1].GetName()}
result = []string{listUsers[0].Name, listUsers[1].Name}
assert.Equal(
ct,
@@ -70,13 +69,13 @@ func TestUserCommand(t *testing.T) {
"users",
"rename",
"--output=json",
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
"--identifier=" + listUsers[1].Id,
"--new-name=newname",
},
)
require.NoError(t, err)
var listAfterRenameUsers []*v1.User
var listAfterRenameUsers []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -92,7 +91,7 @@ func TestUserCommand(t *testing.T) {
assert.NoError(ct, err)
slices.SortFunc(listAfterRenameUsers, sortWithID)
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
result = []string{listAfterRenameUsers[0].Name, listAfterRenameUsers[1].Name}
assert.Equal(
ct,
@@ -102,7 +101,7 @@ func TestUserCommand(t *testing.T) {
)
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
var listByUsername []*v1.User
var listByUsername []*clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -121,19 +120,19 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listByUsername, sortWithID)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
}
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users (-want +got):\n%s", diff)
}
var listByID []*v1.User
var listByID []*clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -152,15 +151,15 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listByID, sortWithID)
want = []*v1.User{
want = []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
}
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users (-want +got):\n%s", diff)
}
@@ -177,7 +176,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterIDDelete []*v1.User
var listAfterIDDelete []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -194,15 +193,15 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listAfterIDDelete, sortWithID)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 2,
Id: "2",
Name: "newname",
Email: "user2@test.no",
},
}
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
assert.Fail(ct, "unexpected users", "diff (-want +got):\n%s", diff)
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
@@ -219,7 +218,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterNameDelete []v1.User
var listAfterNameDelete []clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -249,8 +248,8 @@ func TestUserCreateCommand(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t)
// Create a user populated with every optional field. The created user is
// returned on stdout and round-tripped through the v1.User type.
created := assertJSONRoundtrip[*v1.User](t, headscale, []string{
// returned on stdout and round-tripped through the User type.
created := assertJSONRoundtrip[*clientv1.User](t, headscale, []string{
"headscale",
"users",
"create",
@@ -261,14 +260,14 @@ func TestUserCreateCommand(t *testing.T) {
"--output", "json",
})
assert.Equal(t, "cli-created", created.GetName())
assert.Equal(t, "CLI Created", created.GetDisplayName())
assert.Equal(t, "cli-created@example.com", created.GetEmail())
assert.Equal(t, "https://example.com/avatar.png", created.GetProfilePicUrl())
assert.Equal(t, "cli-created", created.Name)
assert.Equal(t, "CLI Created", created.DisplayName)
assert.Equal(t, "cli-created@example.com", created.Email)
assert.Equal(t, "https://example.com/avatar.png", created.ProfilePicUrl)
// The created fields must survive a list query (read-after-write) and be
// filterable by email.
var byEmail []*v1.User
var byEmail []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -286,8 +285,8 @@ func TestUserCreateCommand(t *testing.T) {
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by email")
require.Len(t, byEmail, 1)
assert.Equal(t, "cli-created", byEmail[0].GetName())
assert.Equal(t, "CLI Created", byEmail[0].GetDisplayName())
assert.Equal(t, "cli-created", byEmail[0].Name)
assert.Equal(t, "CLI Created", byEmail[0].DisplayName)
}
// TestUserCommandValidation exercises the validation and error permutations of
@@ -325,7 +324,7 @@ func TestUserCommandValidation(t *testing.T) {
case tt.wantEmptyList:
require.NoError(t, err)
var users []v1.User
var users []clientv1.User
require.NoError(t, json.Unmarshal([]byte(out), &users))
require.Empty(t, users)