From e92966b3397fe1871a575b661fa850e66154760d Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 27 Feb 2026 15:23:15 -0500 Subject: [PATCH] feat(auth): expose richer auth connection responses Surface submission-related flow hints in `auth connections get` and make `auth connections list -o json` print the raw API payload so users can see the exact fields needed for submit operations. Made-with: Cursor --- cmd/auth_connections.go | 69 ++++++++++++ cmd/auth_connections_test.go | 198 +++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 cmd/auth_connections_test.go diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 5ff1e31..bc8f9a7 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -219,12 +219,74 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e if auth.FlowStep != "" { tableData = append(tableData, []string{"Flow Step", string(auth.FlowStep)}) } + if len(auth.DiscoveredFields) > 0 { + discoveredFields := make([]string, 0, len(auth.DiscoveredFields)) + for _, field := range auth.DiscoveredFields { + fieldName := field.Name + if fieldName == "" { + fieldName = field.Label + } else if field.Label != "" && field.Label != field.Name { + fieldName = fmt.Sprintf("%s (%s)", field.Name, field.Label) + } + + fieldMeta := make([]string, 0, 2) + if field.Type != "" { + fieldMeta = append(fieldMeta, field.Type) + } + if field.Required { + fieldMeta = append(fieldMeta, "required") + } + if len(fieldMeta) > 0 { + fieldName = fmt.Sprintf("%s [%s]", fieldName, strings.Join(fieldMeta, ", ")) + } + discoveredFields = append(discoveredFields, fieldName) + } + tableData = append(tableData, []string{"Discovered Fields", strings.Join(discoveredFields, "; ")}) + } + if len(auth.MfaOptions) > 0 { + mfaOptions := make([]string, 0, len(auth.MfaOptions)) + for _, option := range auth.MfaOptions { + optionName := option.Label + if optionName == "" { + optionName = option.Type + } else if option.Type != "" { + optionName = fmt.Sprintf("%s (%s)", option.Label, option.Type) + } + mfaOptions = append(mfaOptions, optionName) + } + tableData = append(tableData, []string{"MFA Options", strings.Join(mfaOptions, "; ")}) + } + if len(auth.PendingSSOButtons) > 0 { + pendingSSOButtons := make([]string, 0, len(auth.PendingSSOButtons)) + for _, button := range auth.PendingSSOButtons { + buttonLabel := button.Label + if buttonLabel == "" { + buttonLabel = button.Provider + } else if button.Provider != "" { + buttonLabel = fmt.Sprintf("%s (%s)", button.Label, button.Provider) + } + pendingSSOButtons = append(pendingSSOButtons, buttonLabel) + } + tableData = append(tableData, []string{"Pending SSO Buttons", strings.Join(pendingSSOButtons, "; ")}) + } + if auth.ExternalActionMessage != "" { + tableData = append(tableData, []string{"External Action", auth.ExternalActionMessage}) + } if auth.HostedURL != "" { tableData = append(tableData, []string{"Hosted URL", auth.HostedURL}) } if auth.LiveViewURL != "" { tableData = append(tableData, []string{"Live View URL", auth.LiveViewURL}) } + if auth.WebsiteError != "" { + tableData = append(tableData, []string{"Website Error", auth.WebsiteError}) + } + if !auth.FlowExpiresAt.IsZero() { + tableData = append(tableData, []string{"Flow Expires At", util.FormatLocal(auth.FlowExpiresAt)}) + } + if auth.ErrorCode != "" { + tableData = append(tableData, []string{"Error Code", auth.ErrorCode}) + } if auth.ErrorMessage != "" { tableData = append(tableData, []string{"Error Message", auth.ErrorMessage}) } @@ -272,6 +334,13 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput) } if in.Output == "json" { + if page == nil { + fmt.Println("[]") + return nil + } + if page.RawJSON() != "" { + return util.PrintPrettyJSON(page) + } if len(auths) == 0 { fmt.Println("[]") return nil diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go new file mode 100644 index 0000000..41bcb28 --- /dev/null +++ b/cmd/auth_connections_test.go @@ -0,0 +1,198 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "os" + "testing" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/kernel/kernel-go-sdk/packages/ssestream" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type FakeAuthConnectionService struct { + NewFunc func(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error) + GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) + ListFunc func(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error) + DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error + LoginFunc func(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error) + SubmitFunc func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) + FollowStreamingFunc func(ctx context.Context, id string, opts ...option.RequestOption) *ssestream.Stream[kernel.AuthConnectionFollowResponseUnion] +} + +func (f *FakeAuthConnectionService) New(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + if f.NewFunc != nil { + return f.NewFunc(ctx, body, opts...) + } + return &kernel.ManagedAuth{}, nil +} + +func (f *FakeAuthConnectionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, id, opts...) + } + return nil, errors.New("not found") +} + +func (f *FakeAuthConnectionService) List(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error) { + if f.ListFunc != nil { + return f.ListFunc(ctx, query, opts...) + } + return &pagination.OffsetPagination[kernel.ManagedAuth]{Items: []kernel.ManagedAuth{}}, nil +} + +func (f *FakeAuthConnectionService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error { + if f.DeleteFunc != nil { + return f.DeleteFunc(ctx, id, opts...) + } + return nil +} + +func (f *FakeAuthConnectionService) Login(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error) { + if f.LoginFunc != nil { + return f.LoginFunc(ctx, id, body, opts...) + } + return &kernel.LoginResponse{}, nil +} + +func (f *FakeAuthConnectionService) Submit(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + if f.SubmitFunc != nil { + return f.SubmitFunc(ctx, id, body, opts...) + } + return &kernel.SubmitFieldsResponse{Accepted: true}, nil +} + +func (f *FakeAuthConnectionService) FollowStreaming(ctx context.Context, id string, opts ...option.RequestOption) *ssestream.Stream[kernel.AuthConnectionFollowResponseUnion] { + if f.FollowStreamingFunc != nil { + return f.FollowStreamingFunc(ctx, id, opts...) + } + return nil +} + +func TestAuthConnectionsGet_PrintsSubmissionHints(t *testing.T) { + setupStdoutCapture(t) + + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ + ID: id, + Domain: "auth.leaseweb.com", + ProfileName: "raf-leaseweb", + Status: kernel.ManagedAuthStatusNeedsAuth, + FlowStatus: kernel.ManagedAuthFlowStatusInProgress, + FlowStep: kernel.ManagedAuthFlowStepAwaitingInput, + DiscoveredFields: []kernel.ManagedAuthDiscoveredField{ + {Name: "username", Type: "text", Required: true}, + {Name: "password", Type: "password", Required: true}, + }, + MfaOptions: []kernel.ManagedAuthMfaOption{ + {Label: "Text message", Type: "sms"}, + }, + PendingSSOButtons: []kernel.ManagedAuthPendingSSOButton{ + {Label: "Continue with Google", Provider: "google"}, + }, + }, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + err := c.Get(context.Background(), AuthConnectionGetInput{ID: "e0x3vbw4z66kpwny3k5k46tj"}) + require.NoError(t, err) + + out := outBuf.String() + assert.Contains(t, out, "Discovered Fields") + assert.Contains(t, out, "username") + assert.Contains(t, out, "password") + assert.Contains(t, out, "MFA Options") + assert.Contains(t, out, "Text message") + assert.Contains(t, out, "Pending SSO Buttons") + assert.Contains(t, out, "Continue with Google") +} + +func TestAuthConnectionsGet_JSONOutputIncludesDiscoveredFields(t *testing.T) { + setupStdoutCapture(t) + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + t.Cleanup(func() { + os.Stdout = oldStdout + }) + + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + jsonData := `{ + "id":"e0x3vbw4z66kpwny3k5k46tj", + "domain":"auth.leaseweb.com", + "profile_name":"raf-leaseweb", + "save_credentials":true, + "status":"NEEDS_AUTH", + "flow_status":"IN_PROGRESS", + "flow_step":"AWAITING_INPUT", + "discovered_fields":[ + {"label":"Email","name":"email","selector":"#email","type":"email","required":true} + ] + }` + var auth kernel.ManagedAuth + require.NoError(t, json.Unmarshal([]byte(jsonData), &auth)) + return &auth, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + err := c.Get(context.Background(), AuthConnectionGetInput{ + ID: "e0x3vbw4z66kpwny3k5k46tj", + Output: "json", + }) + require.NoError(t, err) + + w.Close() + var stdoutBuf bytes.Buffer + _, _ = io.Copy(&stdoutBuf, r) + out := stdoutBuf.String() + assert.Contains(t, out, "\"discovered_fields\"") + assert.Contains(t, out, "\"selector\"") + assert.Contains(t, out, "\"email\"") +} + +func TestAuthConnectionsList_JSONOutput_PrintsRawResponse(t *testing.T) { + setupStdoutCapture(t) + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + t.Cleanup(func() { + os.Stdout = oldStdout + }) + + fake := &FakeAuthConnectionService{ + ListFunc: func(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuth], error) { + jsonData := `[{ + "id":"e0x3vbw4z66kpwny3k5k46tj", + "domain":"auth.leaseweb.com", + "profile_name":"raf-leaseweb", + "save_credentials":true, + "status":"NEEDS_AUTH" + }]` + var page pagination.OffsetPagination[kernel.ManagedAuth] + require.NoError(t, json.Unmarshal([]byte(jsonData), &page)) + return &page, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + err := c.List(context.Background(), AuthConnectionListInput{Output: "json"}) + require.NoError(t, err) + + w.Close() + var stdoutBuf bytes.Buffer + _, _ = io.Copy(&stdoutBuf, r) + out := stdoutBuf.String() + assert.Contains(t, out, "\"profile_name\"") + assert.Contains(t, out, "\"raf-leaseweb\"") +}