Files
headscale/integration/cli_preauthkeys_test.go
T
Kristoffer Dalby 8efa5ad1fe 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.
2026-06-19 15:21:00 +02:00

397 lines
9.9 KiB
Go

package integration
import (
"testing"
"time"
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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPreAuthKeyCommand(t *testing.T) {
IntegrationSkip(t)
user := "preauthkeyspace"
count := 3
spec := ScenarioSpec{
Users: []string{user},
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipak"))
require.NoError(t, err)
headscale, err := scenario.Headscale()
require.NoError(t, err)
keys := make([]*clientv1.PreAuthKey, count)
require.NoError(t, err)
for index := range count {
var preAuthKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err := executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--user",
"1",
"create",
"--reusable",
"--expiration",
"24h",
"--output",
"json",
"--tags",
"tag:test1,tag:test2",
},
&preAuthKey,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation")
keys[index] = &preAuthKey
}
assert.Len(t, keys, 3)
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list")
// There is one key created by [Scenario.CreateHeadscaleEnv]
assert.Len(t, listedPreAuthKeys, 4)
assert.Equal(
t,
[]string{keys[0].Id, keys[1].Id, keys[2].Id},
[]string{
listedPreAuthKeys[1].Id,
listedPreAuthKeys[2].Id,
listedPreAuthKeys[3].Id,
},
)
// New keys show prefix after listing, so check the created keys instead
assert.NotEmpty(t, keys[0].Key)
assert.NotEmpty(t, keys[1].Key)
assert.NotEmpty(t, keys[2].Key)
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeys[2].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeys[3].Expiration.After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[2].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[3].Expiration.Before(time.Now().Add(time.Hour*26)),
)
for index := range listedPreAuthKeys {
if index == 0 {
continue
}
assert.Equal(
t,
[]string{"tag:test1", "tag:test2"},
listedPreAuthKeys[index].AclTags,
)
}
// Test key expiry
_, err = headscale.Execute(
[]string{
"headscale",
"preauthkeys",
"expire",
"--id",
keys[0].Id,
},
)
require.NoError(t, err)
var listedPreAuthKeysAfterExpire []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"list",
"--output",
"json",
},
&listedPreAuthKeysAfterExpire,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after expire")
assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.Before(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.After(time.Now()))
}
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
IntegrationSkip(t)
user := "pre-auth-key-without-exp-user"
spec := ScenarioSpec{
Users: []string{user},
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipaknaexp"))
require.NoError(t, err)
headscale, err := scenario.Headscale()
require.NoError(t, err)
var preAuthKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--user",
"1",
"create",
"--reusable",
"--output",
"json",
},
&preAuthKey,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation without expiry")
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list")
// There is one key created by [Scenario.CreateHeadscaleEnv]
assert.Len(t, listedPreAuthKeys, 2)
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Minute*70)),
)
}
func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
IntegrationSkip(t)
user := "pre-auth-key-reus-ephm-user"
spec := ScenarioSpec{
Users: []string{user},
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipakresueeph"))
require.NoError(t, err)
headscale, err := scenario.Headscale()
require.NoError(t, err)
var preAuthReusableKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--user",
"1",
"create",
"--reusable=true",
"--output",
"json",
},
&preAuthReusableKey,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for reusable preauth key creation")
var preAuthEphemeralKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"--user",
"1",
"create",
"--ephemeral=true",
"--output",
"json",
},
&preAuthEphemeralKey,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for ephemeral preauth key creation")
assert.True(t, preAuthEphemeralKey.Ephemeral)
assert.False(t, preAuthEphemeralKey.Reusable)
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"preauthkeys",
"list",
"--output",
"json",
},
&listedPreAuthKeys,
)
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after reusable/ephemeral creation")
// There is one key created by [Scenario.CreateHeadscaleEnv]
assert.Len(t, listedPreAuthKeys, 3)
}
// TestPreAuthKeyDeleteCommand covers `preauthkeys delete --id` (the only
// pre-auth-key subcommand not otherwise exercised) plus its missing-flag
// validation.
func TestPreAuthKeyDeleteCommand(t *testing.T) {
IntegrationSkip(t)
scenario, headscale := setupCLIScenario(t, "cli-pakdelete", []string{"user1"}, 0)
defer scenario.ShutdownAssertNoPanics(t)
// Create a key to delete.
created := assertJSONRoundtrip[*clientv1.PreAuthKey](t, headscale, []string{
"headscale",
"preauthkeys",
"--user", "1",
"create",
"--reusable",
"--output", "json",
})
require.NotEmpty(t, created.Id)
// delete with no --id must be rejected.
_, err := headscale.Execute([]string{"headscale", "preauthkeys", "delete"})
require.ErrorContains(t, err, "missing --id parameter")
// delete the created key by id.
_, err = headscale.Execute([]string{
"headscale", "preauthkeys", "delete",
"--id", created.Id,
})
require.NoError(t, err)
// The deleted key must be gone from the list.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var listed []clientv1.PreAuthKey
err := executeAndUnmarshal(headscale,
[]string{"headscale", "preauthkeys", "list", "--output", "json"},
&listed,
)
assert.NoError(c, err)
for i := range listed {
assert.NotEqual(c, created.Id, listed[i].Id, "deleted key should not be listed")
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key list after delete")
}
// TestPreAuthKeyCommandValidation covers the validation permutations of the
// pre-auth-key subcommands: create with a malformed tag or a non-existent user,
// the required --id on expire/delete, and deleting a non-existent key.
func TestPreAuthKeyCommandValidation(t *testing.T) {
IntegrationSkip(t)
scenario, headscale := setupCLIScenario(t, "cli-pakval", []string{"user1"}, 0)
defer scenario.ShutdownAssertNoPanics(t)
tests := []struct {
name string
args []string
wantErr string
}{
{name: "create malformed tag", args: []string{"preauthkeys", "--user", "1", "create", "--tags", "notatag", "--output", "json"}},
{name: "create nonexistent user", args: []string{"preauthkeys", "--user", "99999", "create", "--output", "json"}},
{name: "expire missing id", args: []string{"preauthkeys", "expire"}, wantErr: "missing --id parameter"},
{name: "delete missing id", args: []string{"preauthkeys", "delete"}, wantErr: "missing --id parameter"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := headscale.Execute(append([]string{"headscale"}, tt.args...))
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.Error(t, err)
})
}
}