Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions internal/client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ func (c *Client) ListOrgProjects(ctx context.Context, orgID string) ([]serverapi
return resp.Items, nil
}

func selectProjectName(projects []serverapi.Project) (string, bool) {
for _, p := range projects {
if p.Name != "" && p.DefaultProject != nil && *p.DefaultProject {
return p.Name, true
}
}
for _, p := range projects {
if p.Name != "" {
return p.Name, true
}
}
return "", false
}

// ResolveDefaultProject picks the project the CLI operates in. Projects are not
// exposed in the CLI UX, so we resolve one silently: the default project if the
// API marks one, otherwise the first.
Expand All @@ -94,23 +108,20 @@ func (c *Client) ResolveDefaultProject(ctx context.Context, orgID string) (strin
return "", err
}

// Server bug: GET /users/current/projects answers {"items":[],"total":0} on
// deployed servers even when the user has a default project, which made
// login fail outright. GET /organizations/{org_id}/projects is correct.
// Drop this fallback once the per-user endpoint is fixed.
if len(projects) == 0 && orgID != "" {
if name, ok := selectProjectName(projects); ok {
return name, nil
}

// GET /users/current/projects has returned empty and malformed project lists
// on deployed servers. GET /organizations/{org_id}/projects is the fallback.
if orgID != "" {
if projects, err = c.ListOrgProjects(ctx, orgID); err != nil {
return "", err
}
}

for _, p := range projects {
if p.DefaultProject != nil && *p.DefaultProject {
return p.Name, nil
if name, ok := selectProjectName(projects); ok {
return name, nil
}
}
if len(projects) > 0 {
return projects[0].Name, nil
}

return "", clierrors.New("No projects found for your account.")
}
64 changes: 64 additions & 0 deletions internal/client/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,70 @@ func TestResolveDefaultProjectFallsBackToOrgProjects(t *testing.T) {
}
}

func TestResolveDefaultProjectFallsBackWhenCurrentUserProjectsHaveNoName(t *testing.T) {
var paths []string

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/v1/users/current/projects":
fmt.Fprint(w, `{"items":[{"project":{"name":"default","default_project":true}}],"total":1}`)
case "/api/v1/organizations/org-1/projects":
fmt.Fprint(w, `{"items":[{"name":"other"},{"name":"the-default","default_project":true}],"total":2}`)
default:
t.Errorf("unexpected request to %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()

c := New(ts.URL, WithTokens("access", ""))

name, err := c.ResolveDefaultProject(context.Background(), "org-1")
if err != nil {
t.Fatalf("ResolveDefaultProject: %v", err)
}
if name != "the-default" {
t.Errorf("project = %q, want the-default", name)
}
if len(paths) != 2 || paths[1] != "/api/v1/organizations/org-1/projects" {
t.Errorf("requests = %v, want the org projects endpoint consulted second", paths)
}
}

func TestResolveDefaultProjectErrorsWhenNoProjectHasAName(t *testing.T) {
var paths []string

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/v1/users/current/projects":
fmt.Fprint(w, `{"items":[{"project":{"id":"project-1"}}],"total":1}`)
case "/api/v1/organizations/org-1/projects":
fmt.Fprint(w, `{"items":[{"id":"project-1"}],"total":1}`)
default:
t.Errorf("unexpected request to %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()

c := New(ts.URL, WithTokens("access", ""))

name, err := c.ResolveDefaultProject(context.Background(), "org-1")
if err == nil {
t.Fatal("ResolveDefaultProject returned a nil error for nameless projects")
}
if name != "" {
t.Errorf("project = %q, want empty on error", name)
}
if len(paths) != 2 || paths[1] != "/api/v1/organizations/org-1/projects" {
t.Errorf("requests = %v, want the org projects endpoint consulted second", paths)
}
}

// The per-user endpoint is still the primary source: when it answers, the org
// endpoint must not be touched.
func TestResolveDefaultProjectPrefersCurrentUserProjects(t *testing.T) {
Expand Down
Loading