Skip to content

Commit 55a2ad7

Browse files
committed
integration: test the oauthclients CLI
1 parent bb37aec commit 55a2ad7

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package integration
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
"time"
7+
8+
"github.com/juanfont/headscale/integration/hsic"
9+
"github.com/juanfont/headscale/integration/integrationutil"
10+
"github.com/juanfont/headscale/integration/tsic"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// cliOAuthClient is the JSON the `oauthclients` commands emit (the v2 Key shape,
16+
// only the fields the test asserts on).
17+
type cliOAuthClient struct {
18+
ID string `json:"id"`
19+
Key string `json:"key"`
20+
KeyType string `json:"keyType"`
21+
Scopes []string `json:"scopes"`
22+
Tags []string `json:"tags"`
23+
Description string `json:"description"`
24+
}
25+
26+
// TestOAuthClientCommand exercises the `headscale oauthclients` CLI end to end:
27+
// create (with scopes and tags) -> list (secret hidden) -> delete. The CLI talks
28+
// to the v2 keys handler over the local unix socket, so this also proves the v2
29+
// API is reachable over the socket with local trust.
30+
func TestOAuthClientCommand(t *testing.T) {
31+
IntegrationSkip(t)
32+
33+
scenario, err := NewScenario(ScenarioSpec{Users: []string{}})
34+
require.NoError(t, err)
35+
36+
defer scenario.ShutdownAssertNoPanics(t)
37+
38+
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cli-oauthclient"))
39+
require.NoError(t, err)
40+
41+
headscale, err := scenario.Headscale()
42+
require.NoError(t, err)
43+
44+
// Create an OAuth client with scopes and a tag. devices:core/auth_keys
45+
// require a tag, which is supplied.
46+
createOut, err := headscale.Execute([]string{
47+
"headscale", "oauthclients", "create",
48+
"--scope", "auth_keys",
49+
"--scope", "devices:core",
50+
"--tag", "tag:k8s-operator",
51+
"--description", "operator",
52+
"--output", "json",
53+
})
54+
require.NoError(t, err)
55+
56+
var created cliOAuthClient
57+
require.NoError(t, json.Unmarshal([]byte(createOut), &created))
58+
assert.Equal(t, "client", created.KeyType)
59+
assert.NotEmpty(t, created.ID, "client id returned")
60+
assert.NotEmpty(t, created.Key, "secret returned once on create")
61+
assert.ElementsMatch(t, []string{"auth_keys", "devices:core"}, created.Scopes)
62+
assert.Equal(t, []string{"tag:k8s-operator"}, created.Tags)
63+
64+
// List shows the client without its secret.
65+
var listed []cliOAuthClient
66+
67+
assert.EventuallyWithT(t, func(c *assert.CollectT) {
68+
err := executeAndUnmarshal(headscale,
69+
[]string{"headscale", "oauthclients", "list", "--output", "json"}, &listed)
70+
assert.NoError(c, err)
71+
assert.Len(c, listed, 1)
72+
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "waiting for oauth client list")
73+
74+
assert.Equal(t, created.ID, listed[0].ID)
75+
assert.Empty(t, listed[0].Key, "secret is never exposed on list")
76+
assert.ElementsMatch(t, []string{"auth_keys", "devices:core"}, listed[0].Scopes)
77+
assert.Equal(t, "operator", listed[0].Description)
78+
79+
// Delete it.
80+
_, err = headscale.Execute([]string{"headscale", "oauthclients", "delete", "--id", created.ID})
81+
require.NoError(t, err)
82+
83+
var afterDelete []cliOAuthClient
84+
85+
assert.EventuallyWithT(t, func(c *assert.CollectT) {
86+
err := executeAndUnmarshal(headscale,
87+
[]string{"headscale", "oauthclients", "list", "--output", "json"}, &afterDelete)
88+
assert.NoError(c, err)
89+
assert.Empty(c, afterDelete)
90+
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "waiting for oauth client list after delete")
91+
}
92+
93+
// TestOAuthClientCommandValidation covers the CLI's input validation and the
94+
// server's 404 on deleting an unknown client.
95+
func TestOAuthClientCommandValidation(t *testing.T) {
96+
IntegrationSkip(t)
97+
98+
scenario, err := NewScenario(ScenarioSpec{Users: []string{}})
99+
require.NoError(t, err)
100+
101+
defer scenario.ShutdownAssertNoPanics(t)
102+
103+
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cli-oauthclientval"))
104+
require.NoError(t, err)
105+
106+
headscale, err := scenario.Headscale()
107+
require.NoError(t, err)
108+
109+
tests := []struct {
110+
name string
111+
args []string
112+
wantErr string
113+
}{
114+
{name: "no scope", args: []string{"oauthclients", "create"}, wantErr: "at least one --scope is required"},
115+
{name: "devices:core needs tag", args: []string{"oauthclients", "create", "--scope", "devices:core"}, wantErr: "tags are required"},
116+
{name: "delete no id", args: []string{"oauthclients", "delete"}, wantErr: "--id is required"},
117+
{name: "delete nonexistent id", args: []string{"oauthclients", "delete", "--id", "doesnotexist"}, wantErr: "404"},
118+
}
119+
120+
for _, tt := range tests {
121+
t.Run(tt.name, func(t *testing.T) {
122+
_, err := headscale.Execute(append([]string{"headscale"}, tt.args...))
123+
require.ErrorContains(t, err, tt.wantErr)
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)