diff --git a/pkg/cmd/whoami.go b/pkg/cmd/whoami.go index d76fe98..a069f44 100644 --- a/pkg/cmd/whoami.go +++ b/pkg/cmd/whoami.go @@ -6,6 +6,8 @@ import ( "github.com/hookdeck/hookdeck-cli/pkg/ansi" "github.com/hookdeck/hookdeck-cli/pkg/config" + "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" + "github.com/hookdeck/hookdeck-cli/pkg/project" "github.com/hookdeck/hookdeck-cli/pkg/validators" "github.com/spf13/cobra" ) @@ -42,20 +44,36 @@ func (lc *whoamiCmd) runWhoamiCmd(cmd *cobra.Command, args []string) error { return err } - fmt.Printf( - "Logged in as %s (%s) on project %s in organization %s\n", - color.Bold(response.UserName), - color.Bold(response.UserEmail), - color.Bold(response.ProjectName), - color.Bold(response.OrganizationName), - ) + projectName, orgName, projectMode, note := resolveActiveProject(response, Config.Profile.ProjectId, func() ([]hookdeck.Project, error) { + return Config.GetAPIClient().ListProjects() + }) + + if orgName != "" { + fmt.Printf( + "Logged in as %s (%s) on project %s in organization %s\n", + color.Bold(response.UserName), + color.Bold(response.UserEmail), + color.Bold(projectName), + color.Bold(orgName), + ) + } else { + fmt.Printf( + "Logged in as %s (%s) on project %s\n", + color.Bold(response.UserName), + color.Bold(response.UserEmail), + color.Bold(projectName), + ) + } + if note != "" { + fmt.Printf("%s\n", note) + } projectType := Config.Profile.ProjectType if projectType == "" && Config.Profile.ProjectMode != "" { projectType = config.ModeToProjectType(Config.Profile.ProjectMode) } - if projectType == "" && response.ProjectMode != "" { - projectType = config.ModeToProjectType(response.ProjectMode) + if projectType == "" && projectMode != "" { + projectType = config.ModeToProjectType(projectMode) } if projectType != "" { fmt.Printf("Project type: %s\n", projectType) @@ -63,3 +81,40 @@ func (lc *whoamiCmd) runWhoamiCmd(cmd *cobra.Command, args []string) error { return nil } + +// resolveActiveProject returns the project name, organization name, and project +// mode to display. /cli-auth/validate resolves the project from the API key's +// bound team and ignores the profile's active project_id, so when the two +// differ the active project is looked up via listProjects. A non-empty note is +// returned when the active project could not be resolved and the key-bound +// values are shown instead. +func resolveActiveProject(response *hookdeck.ValidateAPIKeyResponse, activeProjectID string, listProjects func() ([]hookdeck.Project, error)) (projectName, orgName, projectMode, note string) { + projectName = response.ProjectName + orgName = response.OrganizationName + projectMode = response.ProjectMode + + if activeProjectID == "" || activeProjectID == response.ProjectID { + return projectName, orgName, projectMode, "" + } + + projects, err := listProjects() + if err != nil { + note = fmt.Sprintf("Warning: could not look up the active project (%s); showing the project associated with your API key.", activeProjectID) + return projectName, orgName, projectMode, note + } + + for _, p := range projects { + if p.Id != activeProjectID { + continue + } + org, proj, parseErr := project.ParseProjectName(p.Name) + if parseErr != nil { + org = "" + proj = p.Name + } + return proj, org, p.Mode, "" + } + + note = fmt.Sprintf("Warning: the active project (%s) was not found; showing the project associated with your API key. Run 'hookdeck project use' to select a project.", activeProjectID) + return projectName, orgName, projectMode, note +} diff --git a/pkg/cmd/whoami_test.go b/pkg/cmd/whoami_test.go new file mode 100644 index 0000000..94810e6 --- /dev/null +++ b/pkg/cmd/whoami_test.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "errors" + "testing" + + "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" +) + +func TestResolveActiveProject(t *testing.T) { + validateResponse := &hookdeck.ValidateAPIKeyResponse{ + ProjectID: "tm_bound", + ProjectName: "Bound Project", + ProjectMode: "inbound", + OrganizationName: "Org A", + } + + projects := []hookdeck.Project{ + {Id: "tm_bound", Name: "[Org A] Bound Project", Mode: "inbound"}, + {Id: "tm_active", Name: "[Org B] Active Project", Mode: "outbound"}, + {Id: "tm_unparsable", Name: "No Org Format", Mode: "inbound"}, + } + + t.Run("no active project id uses validate response", func(t *testing.T) { + called := false + name, org, mode, note := resolveActiveProject(validateResponse, "", func() ([]hookdeck.Project, error) { + called = true + return projects, nil + }) + if called { + t.Error("listProjects should not be called when no active project id is set") + } + if name != "Bound Project" || org != "Org A" || mode != "inbound" || note != "" { + t.Errorf("got (%q, %q, %q, %q)", name, org, mode, note) + } + }) + + t.Run("active project matches key-bound project uses validate response", func(t *testing.T) { + called := false + name, org, mode, note := resolveActiveProject(validateResponse, "tm_bound", func() ([]hookdeck.Project, error) { + called = true + return projects, nil + }) + if called { + t.Error("listProjects should not be called when active project matches the key-bound project") + } + if name != "Bound Project" || org != "Org A" || mode != "inbound" || note != "" { + t.Errorf("got (%q, %q, %q, %q)", name, org, mode, note) + } + }) + + t.Run("active project differs and is resolved from project list", func(t *testing.T) { + name, org, mode, note := resolveActiveProject(validateResponse, "tm_active", func() ([]hookdeck.Project, error) { + return projects, nil + }) + if name != "Active Project" || org != "Org B" || mode != "outbound" || note != "" { + t.Errorf("got (%q, %q, %q, %q)", name, org, mode, note) + } + }) + + t.Run("unparsable project name falls back to full name without org", func(t *testing.T) { + name, org, mode, note := resolveActiveProject(validateResponse, "tm_unparsable", func() ([]hookdeck.Project, error) { + return projects, nil + }) + if name != "No Org Format" || org != "" || mode != "inbound" || note != "" { + t.Errorf("got (%q, %q, %q, %q)", name, org, mode, note) + } + }) + + t.Run("list error falls back to validate response with warning", func(t *testing.T) { + name, org, mode, note := resolveActiveProject(validateResponse, "tm_active", func() ([]hookdeck.Project, error) { + return nil, errors.New("boom") + }) + if name != "Bound Project" || org != "Org A" || mode != "inbound" { + t.Errorf("got (%q, %q, %q)", name, org, mode) + } + if note == "" { + t.Error("expected a warning note when the project list cannot be fetched") + } + }) + + t.Run("active project missing from list falls back with warning", func(t *testing.T) { + name, org, mode, note := resolveActiveProject(validateResponse, "tm_deleted", func() ([]hookdeck.Project, error) { + return projects, nil + }) + if name != "Bound Project" || org != "Org A" || mode != "inbound" { + t.Errorf("got (%q, %q, %q)", name, org, mode) + } + if note == "" { + t.Error("expected a warning note when the active project is not in the list") + } + }) +}