Skip to content

Commit 2fa28bc

Browse files
leggetterclaudecursoragent
committed
test(login): cover guest valid-key claim and stale-key guest credentials
Add unit test for guest profile with valid key reaching signup claim flow. Update guest acceptance test to assert guest_user_id/guest_api_key on POST after validate 401 without auth_intent. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 96b4b11 commit 2fa28bc

4 files changed

Lines changed: 106 additions & 29 deletions

File tree

pkg/login/client_login_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,95 @@ api_key = "hk_test_oldkey_abcdefghij"
119119
require.Equal(t, 1, pollHits, "poll should run once with immediate claimed=true")
120120
require.Equal(t, "hk_test_newkey_abcdefghij", cfg.Profile.APIKey)
121121
}
122+
123+
// TestLogin_guestProfileWithValidKeyStartsClaimFlow verifies that a guest Console
124+
// profile with a still-valid API key skips the early-return path and POSTs guest
125+
// credentials to /cli-auth for browser signup claim.
126+
func TestLogin_guestProfileWithValidKeyStartsClaimFlow(t *testing.T) {
127+
configpkg.ResetAPIClientForTesting()
128+
t.Cleanup(configpkg.ResetAPIClientForTesting)
129+
130+
oldCan := canOpenBrowser
131+
oldOpen := openBrowser
132+
canOpenBrowser = func() bool { return false }
133+
openBrowser = func(string) error { return nil }
134+
t.Cleanup(func() {
135+
canOpenBrowser = oldCan
136+
openBrowser = oldOpen
137+
})
138+
139+
var cli_auth_body map[string]string
140+
pollHits := 0
141+
var serverURL string
142+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143+
switch {
144+
case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/cli-auth/validate"):
145+
body, err := json.Marshal(map[string]string{
146+
"user_id": "usr_guest",
147+
"user_name": "Guest",
148+
"user_email": "guest@example.com",
149+
"organization_name": "Org",
150+
"organization_id": "org_1",
151+
"team_id": "tm_console",
152+
"team_name_no_org": "Sandbox",
153+
"team_mode": "console",
154+
"client_id": "cl_guest",
155+
})
156+
require.NoError(t, err)
157+
_, _ = w.Write(body)
158+
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/cli-auth"):
159+
require.NoError(t, json.NewDecoder(r.Body).Decode(&cli_auth_body))
160+
pollURL := serverURL + hookdeck.APIPathPrefix + "/cli-auth/poll?key=pollkey"
161+
body, err := json.Marshal(map[string]string{
162+
"browser_url": "https://example.test/signup?redirect=%2Fcli-auth%2Fkey",
163+
"poll_url": pollURL,
164+
})
165+
require.NoError(t, err)
166+
_, _ = w.Write(body)
167+
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/cli-auth/poll"):
168+
pollHits++
169+
resp := map[string]interface{}{
170+
"claimed": true,
171+
"key": "hk_test_claimed_abcdefghij",
172+
"team_id": "tm_console",
173+
"team_mode": "console",
174+
"team_name": "Sandbox",
175+
"user_name": "Guest",
176+
"user_email": "claimed@example.com",
177+
"organization_name": "Org",
178+
"organization_id": "org_1",
179+
"client_id": "cl_claimed",
180+
}
181+
enc, err := json.Marshal(resp)
182+
require.NoError(t, err)
183+
_, _ = w.Write(enc)
184+
default:
185+
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
186+
}
187+
}))
188+
serverURL = ts.URL
189+
t.Cleanup(ts.Close)
190+
191+
configPath := filepath.Join(t.TempDir(), "config.toml")
192+
require.NoError(t, os.WriteFile(configPath, []byte(`profile = "default"
193+
194+
[default]
195+
api_key = "hk_test_guestkey_abcdefghij"
196+
guest_url = "https://console.test/signin/guest?token=abc"
197+
guest_user_id = "usr_guest"
198+
`), 0o600))
199+
200+
cfg, err := configpkg.LoadConfigFromFile(configPath)
201+
require.NoError(t, err)
202+
cfg.APIBaseURL = ts.URL
203+
cfg.DeviceName = "test-device"
204+
cfg.LogLevel = "error"
205+
cfg.TelemetryDisabled = true
206+
207+
err = Login(cfg, strings.NewReader("\n"))
208+
require.NoError(t, err)
209+
require.Equal(t, "usr_guest", cli_auth_body["guest_user_id"])
210+
require.Equal(t, "hk_test_guestkey_abcdefghij", cli_auth_body["guest_api_key"])
211+
require.Equal(t, 1, pollHits)
212+
require.Equal(t, "hk_test_claimed_abcdefghij", cfg.Profile.APIKey)
213+
}

pkg/login/login_intent_test.go

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,31 @@ import (
44
"testing"
55

66
configpkg "github.com/hookdeck/hookdeck-cli/pkg/config"
7-
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
87
"github.com/stretchr/testify/require"
98
)
109

11-
func TestResolveLoginIntent_guestProfileDefaultsClaimGuest(t *testing.T) {
10+
func TestBuildStartLoginInput_guestProfileIncludesCredentials(t *testing.T) {
1211
cfg := &configpkg.Config{
12+
DeviceName: "dev",
1313
Profile: configpkg.Profile{
1414
GuestURL: "https://console.test/signin/guest?token=abc",
1515
GuestUserID: "usr_guest",
1616
APIKey: "hk_guest_key",
1717
},
1818
}
1919

20-
require.Equal(t, hookdeck.CLIAuthIntentClaimGuest, resolveLoginIntent(cfg))
21-
}
22-
23-
func TestResolveLoginIntent_noGuestProfileDefaultsLogin(t *testing.T) {
24-
cfg := &configpkg.Config{
25-
Profile: configpkg.Profile{},
26-
}
27-
28-
require.Equal(t, hookdeck.CLIAuthIntentLogin, resolveLoginIntent(cfg))
20+
input := buildStartLoginInput(cfg, "")
21+
require.Equal(t, "usr_guest", input.GuestUserID)
22+
require.Equal(t, "hk_guest_key", input.GuestAPIKey)
2923
}
3024

31-
func TestBuildStartLoginInput_guestCredentialsOnlyForClaim(t *testing.T) {
25+
func TestBuildStartLoginInput_noGuestProfileOmitsCredentials(t *testing.T) {
3226
cfg := &configpkg.Config{
3327
DeviceName: "dev",
34-
Profile: configpkg.Profile{
35-
GuestURL: "https://console.test/signin/guest?token=abc",
36-
GuestUserID: "usr_guest",
37-
APIKey: "hk_guest_key",
38-
},
28+
Profile: configpkg.Profile{},
3929
}
4030

41-
login_input := buildStartLoginInput(cfg, hookdeck.CLIAuthIntentLogin, "")
42-
require.Equal(t, hookdeck.CLIAuthIntentLogin, login_input.AuthIntent)
43-
require.Empty(t, login_input.GuestUserID)
44-
require.Empty(t, login_input.GuestAPIKey)
45-
46-
claim_input := buildStartLoginInput(cfg, hookdeck.CLIAuthIntentClaimGuest, "")
47-
require.Equal(t, "usr_guest", claim_input.GuestUserID)
48-
require.Equal(t, "hk_guest_key", claim_input.GuestAPIKey)
31+
input := buildStartLoginInput(cfg, "")
32+
require.Empty(t, input.GuestUserID)
33+
require.Empty(t, input.GuestAPIKey)
4934
}

test/acceptance/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ These tests run automatically in CI using API keys from `hookdeck ci`. They don'
1313

1414
**Login recovery (mock API, `basic` tag):** `login_auth_acceptance_test.go` runs the real CLI with `--api-base` pointing at a local server that returns **401** on `GET .../cli-auth/validate`, then completes a fake device-auth poll — this asserts `hookdeck login` continues into browser/device flow after a stale key (no human, no real Hookdeck key). The same file includes **`TestCIFailsFastWithInvalidAPIKeyAcceptance`**, which runs `hookdeck ci --api-key` with a bogus key against the real API and expects a quick failure with the friendly **Authentication failed** message, and asserts output does **not** contain browser/device-login phrases (`Press Enter to open the browser`, `To authenticate with Hookdeck`, etc.) so CI never enters the interactive `hookdeck login` flow.
1515

16-
**Guest login intents (mock API, `guest` tag):** `guest_login_acceptance_test.go` asserts `POST /cli-auth` receives `claim_guest` with guest credentials when a guest profile is present, and `login` without guest credentials after logout (empty profile).
16+
**Guest login (mock API, `guest` tag):** `guest_login_acceptance_test.go` asserts `POST /cli-auth` receives guest credentials when a guest profile is present, and omits them after logout (empty profile).
1717

1818
### 2. Manual Tests (Require Human Interaction)
1919
These tests require browser-based authentication via `hookdeck login` and must be run manually by developers.

test/acceptance/guest_login_acceptance_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,16 @@ func TestGuestLoginDefaultClaimGuestAcceptance(t *testing.T) {
130130
require.NoError(t, os.WriteFile(configPath, []byte(guestProfileConfig()), 0o600))
131131

132132
ts, serverURL := newGuestLoginMock(t, func(body map[string]interface{}) {
133-
require.Equal(t, "claim_guest", body["auth_intent"])
134133
require.Equal(t, "usr_guest_accept", body["guest_user_id"])
135134
require.Equal(t, "hk_test_stale_guest01", body["guest_api_key"])
135+
require.NotContains(t, body, "auth_intent")
136136
}, "https://example.test/signup?redirect=%2Fcli-auth%2Fkey")
137137

138138
runGuestLoginCLI(t, projectRoot, configPath, serverURL)
139139
_ = ts
140140
}
141141

142-
func TestGuestLoginAfterLogoutUsesLoginIntentAcceptance(t *testing.T) {
142+
func TestGuestLoginAfterLogoutOmitsGuestCredentialsAcceptance(t *testing.T) {
143143
if testing.Short() {
144144
t.Skip("Skipping acceptance test in short mode")
145145
}
@@ -151,7 +151,7 @@ func TestGuestLoginAfterLogoutUsesLoginIntentAcceptance(t *testing.T) {
151151
require.NoError(t, os.WriteFile(configPath, []byte(loggedOutProfileConfig()), 0o600))
152152

153153
ts, serverURL := newGuestLoginMock(t, func(body map[string]interface{}) {
154-
require.Equal(t, "login", body["auth_intent"])
154+
require.NotContains(t, body, "auth_intent")
155155
require.NotContains(t, body, "guest_user_id")
156156
require.NotContains(t, body, "guest_api_key")
157157
}, "https://example.test/signin?redirect=%2Fcli-auth%2Fkey")

0 commit comments

Comments
 (0)