Skip to content

Commit f06b05a

Browse files
authored
feat(auth): expose richer auth connection get/list output (#127)
## Summary - surface submission hints in `kernel auth connections get` by including discovered fields, MFA options, pending SSO buttons, and additional flow/error context in table output - make `kernel auth connections list -o json` print the raw API response payload when available so users can inspect exact API fields - add focused tests for `auth connections get/list` JSON and human-readable output behaviors ## Test plan - [x] `go test ./cmd -run AuthConnections` - [x] `go test ./cmd` Made with [Cursor](https://cursor.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because `auth connections list -o json` can now emit the raw paginated response shape (when `RawJSON()` is present), which may affect downstream JSON parsing; otherwise changes are output-only and covered by tests. > > **Overview** > **Richer `kernel auth connections get` table output** now includes submission hints and more flow/error context: `discovered_fields` (with type/required metadata), MFA options, pending SSO buttons, `external_action_message`, `website_error`, `flow_expires_at`, and `error_code`. > > **`kernel auth connections list -o json` output behavior changes** to print the raw API response payload when available (and prints `[]` on `nil` pages), falling back to the previous items-only JSON slice when raw JSON isn’t present. > > Adds focused tests around the new human-readable hints and the updated JSON output behaviors for `get`/`list`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8d8f3f8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 921d465 commit f06b05a

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

cmd/auth_connections.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,74 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e
219219
if auth.FlowStep != "" {
220220
tableData = append(tableData, []string{"Flow Step", string(auth.FlowStep)})
221221
}
222+
if len(auth.DiscoveredFields) > 0 {
223+
discoveredFields := make([]string, 0, len(auth.DiscoveredFields))
224+
for _, field := range auth.DiscoveredFields {
225+
fieldName := field.Name
226+
if fieldName == "" {
227+
fieldName = field.Label
228+
} else if field.Label != "" && field.Label != field.Name {
229+
fieldName = fmt.Sprintf("%s (%s)", field.Name, field.Label)
230+
}
231+
232+
fieldMeta := make([]string, 0, 2)
233+
if field.Type != "" {
234+
fieldMeta = append(fieldMeta, field.Type)
235+
}
236+
if field.Required {
237+
fieldMeta = append(fieldMeta, "required")
238+
}
239+
if len(fieldMeta) > 0 {
240+
fieldName = fmt.Sprintf("%s [%s]", fieldName, strings.Join(fieldMeta, ", "))
241+
}
242+
discoveredFields = append(discoveredFields, fieldName)
243+
}
244+
tableData = append(tableData, []string{"Discovered Fields", strings.Join(discoveredFields, "; ")})
245+
}
246+
if len(auth.MfaOptions) > 0 {
247+
mfaOptions := make([]string, 0, len(auth.MfaOptions))
248+
for _, option := range auth.MfaOptions {
249+
optionName := option.Label
250+
if optionName == "" {
251+
optionName = option.Type
252+
} else if option.Type != "" {
253+
optionName = fmt.Sprintf("%s (%s)", option.Label, option.Type)
254+
}
255+
mfaOptions = append(mfaOptions, optionName)
256+
}
257+
tableData = append(tableData, []string{"MFA Options", strings.Join(mfaOptions, "; ")})
258+
}
259+
if len(auth.PendingSSOButtons) > 0 {
260+
pendingSSOButtons := make([]string, 0, len(auth.PendingSSOButtons))
261+
for _, button := range auth.PendingSSOButtons {
262+
buttonLabel := button.Label
263+
if buttonLabel == "" {
264+
buttonLabel = button.Provider
265+
} else if button.Provider != "" {
266+
buttonLabel = fmt.Sprintf("%s (%s)", button.Label, button.Provider)
267+
}
268+
pendingSSOButtons = append(pendingSSOButtons, buttonLabel)
269+
}
270+
tableData = append(tableData, []string{"Pending SSO Buttons", strings.Join(pendingSSOButtons, "; ")})
271+
}
272+
if auth.ExternalActionMessage != "" {
273+
tableData = append(tableData, []string{"External Action", auth.ExternalActionMessage})
274+
}
222275
if auth.HostedURL != "" {
223276
tableData = append(tableData, []string{"Hosted URL", auth.HostedURL})
224277
}
225278
if auth.LiveViewURL != "" {
226279
tableData = append(tableData, []string{"Live View URL", auth.LiveViewURL})
227280
}
281+
if auth.WebsiteError != "" {
282+
tableData = append(tableData, []string{"Website Error", auth.WebsiteError})
283+
}
284+
if !auth.FlowExpiresAt.IsZero() {
285+
tableData = append(tableData, []string{"Flow Expires At", util.FormatLocal(auth.FlowExpiresAt)})
286+
}
287+
if auth.ErrorCode != "" {
288+
tableData = append(tableData, []string{"Error Code", auth.ErrorCode})
289+
}
228290
if auth.ErrorMessage != "" {
229291
tableData = append(tableData, []string{"Error Message", auth.ErrorMessage})
230292
}
@@ -272,6 +334,13 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput)
272334
}
273335

274336
if in.Output == "json" {
337+
if page == nil {
338+
fmt.Println("[]")
339+
return nil
340+
}
341+
if page.RawJSON() != "" {
342+
return util.PrintPrettyJSON(page)
343+
}
275344
if len(auths) == 0 {
276345
fmt.Println("[]")
277346
return nil

cmd/auth_connections_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"io"
9+
"os"
10+
"testing"
11+
12+
"github.com/kernel/kernel-go-sdk"
13+
"github.com/kernel/kernel-go-sdk/option"
14+
"github.com/kernel/kernel-go-sdk/packages/pagination"
15+
"github.com/kernel/kernel-go-sdk/packages/ssestream"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
type FakeAuthConnectionService struct {
21+
NewFunc func(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error)
22+
GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error)
23+
ListFunc func(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error)
24+
DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error
25+
LoginFunc func(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error)
26+
SubmitFunc func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error)
27+
FollowStreamingFunc func(ctx context.Context, id string, opts ...option.RequestOption) *ssestream.Stream[kernel.AuthConnectionFollowResponseUnion]
28+
}
29+
30+
func (f *FakeAuthConnectionService) New(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
31+
if f.NewFunc != nil {
32+
return f.NewFunc(ctx, body, opts...)
33+
}
34+
return &kernel.ManagedAuth{}, nil
35+
}
36+
37+
func (f *FakeAuthConnectionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
38+
if f.GetFunc != nil {
39+
return f.GetFunc(ctx, id, opts...)
40+
}
41+
return nil, errors.New("not found")
42+
}
43+
44+
func (f *FakeAuthConnectionService) List(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error) {
45+
if f.ListFunc != nil {
46+
return f.ListFunc(ctx, query, opts...)
47+
}
48+
return &pagination.OffsetPagination[kernel.ManagedAuth]{Items: []kernel.ManagedAuth{}}, nil
49+
}
50+
51+
func (f *FakeAuthConnectionService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error {
52+
if f.DeleteFunc != nil {
53+
return f.DeleteFunc(ctx, id, opts...)
54+
}
55+
return nil
56+
}
57+
58+
func (f *FakeAuthConnectionService) Login(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error) {
59+
if f.LoginFunc != nil {
60+
return f.LoginFunc(ctx, id, body, opts...)
61+
}
62+
return &kernel.LoginResponse{}, nil
63+
}
64+
65+
func (f *FakeAuthConnectionService) Submit(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
66+
if f.SubmitFunc != nil {
67+
return f.SubmitFunc(ctx, id, body, opts...)
68+
}
69+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
70+
}
71+
72+
func (f *FakeAuthConnectionService) FollowStreaming(ctx context.Context, id string, opts ...option.RequestOption) *ssestream.Stream[kernel.AuthConnectionFollowResponseUnion] {
73+
if f.FollowStreamingFunc != nil {
74+
return f.FollowStreamingFunc(ctx, id, opts...)
75+
}
76+
return nil
77+
}
78+
79+
func TestAuthConnectionsGet_PrintsSubmissionHints(t *testing.T) {
80+
setupStdoutCapture(t)
81+
82+
fake := &FakeAuthConnectionService{
83+
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
84+
return &kernel.ManagedAuth{
85+
ID: id,
86+
Domain: "auth.leaseweb.com",
87+
ProfileName: "raf-leaseweb",
88+
Status: kernel.ManagedAuthStatusNeedsAuth,
89+
FlowStatus: kernel.ManagedAuthFlowStatusInProgress,
90+
FlowStep: kernel.ManagedAuthFlowStepAwaitingInput,
91+
DiscoveredFields: []kernel.ManagedAuthDiscoveredField{
92+
{Name: "username", Type: "text", Required: true},
93+
{Name: "password", Type: "password", Required: true},
94+
},
95+
MfaOptions: []kernel.ManagedAuthMfaOption{
96+
{Label: "Text message", Type: "sms"},
97+
},
98+
PendingSSOButtons: []kernel.ManagedAuthPendingSSOButton{
99+
{Label: "Continue with Google", Provider: "google"},
100+
},
101+
}, nil
102+
},
103+
}
104+
c := AuthConnectionCmd{svc: fake}
105+
106+
err := c.Get(context.Background(), AuthConnectionGetInput{ID: "e0x3vbw4z66kpwny3k5k46tj"})
107+
require.NoError(t, err)
108+
109+
out := outBuf.String()
110+
assert.Contains(t, out, "Discovered Fields")
111+
assert.Contains(t, out, "username")
112+
assert.Contains(t, out, "password")
113+
assert.Contains(t, out, "MFA Options")
114+
assert.Contains(t, out, "Text message")
115+
assert.Contains(t, out, "Pending SSO Buttons")
116+
assert.Contains(t, out, "Continue with Google")
117+
}
118+
119+
func TestAuthConnectionsGet_JSONOutputIncludesDiscoveredFields(t *testing.T) {
120+
setupStdoutCapture(t)
121+
oldStdout := os.Stdout
122+
r, w, _ := os.Pipe()
123+
os.Stdout = w
124+
t.Cleanup(func() {
125+
os.Stdout = oldStdout
126+
})
127+
128+
fake := &FakeAuthConnectionService{
129+
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
130+
jsonData := `{
131+
"id":"e0x3vbw4z66kpwny3k5k46tj",
132+
"domain":"auth.leaseweb.com",
133+
"profile_name":"raf-leaseweb",
134+
"save_credentials":true,
135+
"status":"NEEDS_AUTH",
136+
"flow_status":"IN_PROGRESS",
137+
"flow_step":"AWAITING_INPUT",
138+
"discovered_fields":[
139+
{"label":"Email","name":"email","selector":"#email","type":"email","required":true}
140+
]
141+
}`
142+
var auth kernel.ManagedAuth
143+
require.NoError(t, json.Unmarshal([]byte(jsonData), &auth))
144+
return &auth, nil
145+
},
146+
}
147+
c := AuthConnectionCmd{svc: fake}
148+
149+
err := c.Get(context.Background(), AuthConnectionGetInput{
150+
ID: "e0x3vbw4z66kpwny3k5k46tj",
151+
Output: "json",
152+
})
153+
require.NoError(t, err)
154+
155+
w.Close()
156+
var stdoutBuf bytes.Buffer
157+
_, _ = io.Copy(&stdoutBuf, r)
158+
out := stdoutBuf.String()
159+
assert.Contains(t, out, "\"discovered_fields\"")
160+
assert.Contains(t, out, "\"selector\"")
161+
assert.Contains(t, out, "\"email\"")
162+
}
163+
164+
func TestAuthConnectionsList_JSONOutput_PrintsRawResponse(t *testing.T) {
165+
setupStdoutCapture(t)
166+
oldStdout := os.Stdout
167+
r, w, _ := os.Pipe()
168+
os.Stdout = w
169+
t.Cleanup(func() {
170+
os.Stdout = oldStdout
171+
})
172+
173+
fake := &FakeAuthConnectionService{
174+
ListFunc: func(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error) {
175+
jsonData := `[{
176+
"id":"e0x3vbw4z66kpwny3k5k46tj",
177+
"domain":"auth.leaseweb.com",
178+
"profile_name":"raf-leaseweb",
179+
"save_credentials":true,
180+
"status":"NEEDS_AUTH"
181+
}]`
182+
var page pagination.OffsetPagination[kernel.ManagedAuth]
183+
require.NoError(t, json.Unmarshal([]byte(jsonData), &page))
184+
return &page, nil
185+
},
186+
}
187+
c := AuthConnectionCmd{svc: fake}
188+
189+
err := c.List(context.Background(), AuthConnectionListInput{Output: "json"})
190+
require.NoError(t, err)
191+
192+
w.Close()
193+
var stdoutBuf bytes.Buffer
194+
_, _ = io.Copy(&stdoutBuf, r)
195+
out := stdoutBuf.String()
196+
assert.Contains(t, out, "\"profile_name\"")
197+
assert.Contains(t, out, "\"raf-leaseweb\"")
198+
}

0 commit comments

Comments
 (0)