From 6dee4ca329e6414b3910a22b58ebba49bc0c40a6 Mon Sep 17 00:00:00 2001 From: Levi Whalen Date: Wed, 2 Sep 2026 11:57:57 -0600 Subject: [PATCH 1/3] adds acls and app status to application list & current --- api/dashboard/client_test.go | 60 +++++++++++++++++++++ api/dashboard/types.go | 29 +++++++--- pkg/cmd/application/current/current.go | 4 ++ pkg/cmd/application/current/current_test.go | 3 ++ pkg/cmd/shared/apputil/output.go | 10 ++-- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/api/dashboard/client_test.go b/api/dashboard/client_test.go index b3315cfe..c731bb0a 100644 --- a/api/dashboard/client_test.go +++ b/api/dashboard/client_test.go @@ -221,6 +221,43 @@ func TestListApplications_Unauthorized(t *testing.T) { assert.Contains(t, err.Error(), "session expired") } +func TestListApplications_ParsesBlockedStatus(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/1/applications", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewEncoder(w).Encode(ApplicationsResponse{ + Data: []ApplicationResource{ + { + ID: "APP1", + Type: "application", + Attributes: ApplicationAttributes{ + ApplicationID: "APP1", + Name: "Active App", + IsBlocked: false, + }, + }, + { + ID: "APP2", + Type: "application", + Attributes: ApplicationAttributes{ + ApplicationID: "APP2", + Name: "Blocked App", + IsBlocked: true, + }, + }, + }, + })) + }) + + ts, client := newTestClient(mux) + defer ts.Close() + + apps, err := client.ListApplications("test-token") + require.NoError(t, err) + require.Len(t, apps, 2) + assert.Equal(t, ApplicationStatusActive, apps[0].Status) + assert.Equal(t, ApplicationStatusBlocked, apps[1].Status) +} + func TestGetApplication_Success(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/1/application/APP1", func(w http.ResponseWriter, r *http.Request) { @@ -263,6 +300,29 @@ func TestGetApplication_ParsesPlanLabel(t *testing.T) { assert.Equal(t, "Grow Plus", app.PlanLabel) } +func TestGetApplication_ParsesACL(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/1/application/APP1", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewEncoder(w).Encode(SingleApplicationResponse{ + Data: ApplicationResource{ + ID: "APP1", Type: "application", + Attributes: ApplicationAttributes{ + ApplicationID: "APP1", + Name: "My App", + Permissions: []string{"search", "analytics"}, + }, + }, + })) + }) + + ts, client := newTestClient(mux) + defer ts.Close() + + app, err := client.GetApplication("test-token", "APP1") + require.NoError(t, err) + assert.Equal(t, []string{"search", "analytics"}, app.ACL) +} + func TestCreateApplication_Success(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/1/applications", func(w http.ResponseWriter, r *http.Request) { diff --git a/api/dashboard/types.go b/api/dashboard/types.go index 3f4ae54c..5c68c572 100644 --- a/api/dashboard/types.go +++ b/api/dashboard/types.go @@ -34,6 +34,8 @@ type ApplicationAttributes struct { ApplicationID string `json:"application_id"` APIKey string `json:"api_key"` Plan ApplicationPlan `json:"plan"` + IsBlocked bool `json:"is_blocked"` + Permissions []string `json:"permissions"` } // ApplicationPlan is the plan applied to an application (attributes.plan). @@ -47,12 +49,21 @@ type ApplicationPlan struct { // Application is a flattened view of an Algolia application for CLI consumption. type Application struct { - ID string `json:"id"` - Name string `json:"name"` - APIKey string `json:"api_key,omitempty"` - APIKeyUUID string `json:"api_key_uuid,omitempty"` - PlanLabel string `json:"plan_label,omitempty"` // current plan label, e.g. "Grow Plus" -} + ID string `json:"id"` + Name string `json:"name"` + APIKey string `json:"api_key,omitempty"` + APIKeyUUID string `json:"api_key_uuid,omitempty"` + PlanLabel string `json:"plan_label,omitempty"` // current plan label, e.g. "Grow Plus" + Status string `json:"status,omitempty"` // ApplicationStatusActive or ApplicationStatusBlocked, from attributes.is_blocked + ACL []string `json:"acl,omitempty"` // authenticated user's permissions on this application +} + +// ApplicationStatusActive and ApplicationStatusBlocked are the values toApplication +// assigns to Application.Status, derived from ApplicationAttributes.IsBlocked. +const ( + ApplicationStatusActive = "active" + ApplicationStatusBlocked = "blocked" +) // PaginationMeta contains page-based pagination metadata. type PaginationMeta struct { @@ -218,11 +229,17 @@ func (r *APIKeyResource) toAPIKey() APIKey { // toApplication flattens a JSON:API resource into a simple Application. func (r *ApplicationResource) toApplication() Application { + status := ApplicationStatusActive + if r.Attributes.IsBlocked { + status = ApplicationStatusBlocked + } return Application{ ID: r.Attributes.ApplicationID, Name: r.Attributes.Name, APIKey: r.Attributes.APIKey, PlanLabel: r.Attributes.Plan.Label, + Status: status, + ACL: r.Attributes.Permissions, } } diff --git a/pkg/cmd/application/current/current.go b/pkg/cmd/application/current/current.go index f81799ff..f9fcb341 100644 --- a/pkg/cmd/application/current/current.go +++ b/pkg/cmd/application/current/current.go @@ -2,6 +2,7 @@ package current import ( "fmt" + "strings" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -101,6 +102,9 @@ func runCurrentCmd(opts *CurrentOptions) error { if current.Plan != "" { fmt.Fprintf(opts.IO.Out, " Plan: %s\n", current.Plan) } + if len(current.ACL) > 0 { + fmt.Fprintf(opts.IO.Out, " ACL: %s\n", strings.Join(current.ACL, ", ")) + } if current.Name == "" && current.Plan == "" { if signedOut { fmt.Fprintf( diff --git a/pkg/cmd/application/current/current_test.go b/pkg/cmd/application/current/current_test.go index 8aace58f..ae8c820a 100644 --- a/pkg/cmd/application/current/current_test.go +++ b/pkg/cmd/application/current/current_test.go @@ -46,6 +46,7 @@ func newServer(t *testing.T, status int) *httptest.Server { ApplicationID: "APP1", Name: "My App", Plan: dashboard.ApplicationPlan{Label: "Grow Plus"}, + Permissions: []string{"search", "settings_read"}, }, }, })) @@ -110,6 +111,7 @@ func Test_runCurrentCmd(t *testing.T) { assert.Contains(t, got, "my-alias") assert.Contains(t, got, "My App") assert.Contains(t, got, "Grow Plus") + assert.Contains(t, got, "search, settings_read") } func Test_runCurrentCmd_notConfigured(t *testing.T) { @@ -166,4 +168,5 @@ func Test_runCurrentCmd_outputJSON(t *testing.T) { assert.Contains(t, got, `"alias":"my-alias"`) assert.Contains(t, got, `"name":"My App"`) assert.Contains(t, got, `"plan":"Grow Plus"`) + assert.Contains(t, got, `"acl":["search","settings_read"]`) } diff --git a/pkg/cmd/shared/apputil/output.go b/pkg/cmd/shared/apputil/output.go index 2c021cbc..2b2d6772 100644 --- a/pkg/cmd/shared/apputil/output.go +++ b/pkg/cmd/shared/apputil/output.go @@ -9,10 +9,11 @@ import ( // every command that emits one (`application current`, `application select`, // `auth login`) type ApplicationOutput struct { - ID string `json:"id"` - Alias string `json:"alias"` - Name string `json:"name"` - Plan string `json:"plan"` + ID string `json:"id"` + Alias string `json:"alias"` + Name string `json:"name"` + Plan string `json:"plan"` + ACL []string `json:"acl,omitempty"` } // NewApplicationOutput builds the output view, reading the alias from the @@ -22,6 +23,7 @@ func NewApplicationOutput(cfg config.IConfig, app *dashboard.Application) Applic ID: app.ID, Name: app.Name, Plan: app.PlanLabel, + ACL: app.ACL, } if alias, ok := cfg.ApplicationAlias(app.ID); ok { out.Alias = alias From a91b7c5cfb3bb62b94da10f334f3926ee0a9e36d Mon Sep 17 00:00:00 2001 From: Levi Whalen Date: Wed, 2 Sep 2026 12:02:31 -0600 Subject: [PATCH 2/3] add status to current --- pkg/cmd/application/current/current.go | 3 +++ pkg/cmd/shared/apputil/output.go | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/application/current/current.go b/pkg/cmd/application/current/current.go index f9fcb341..d5f8b2a9 100644 --- a/pkg/cmd/application/current/current.go +++ b/pkg/cmd/application/current/current.go @@ -102,6 +102,9 @@ func runCurrentCmd(opts *CurrentOptions) error { if current.Plan != "" { fmt.Fprintf(opts.IO.Out, " Plan: %s\n", current.Plan) } + if current.Status != "" { + fmt.Fprintf(opts.IO.Out, " Status: %s\n", current.Status) + } if len(current.ACL) > 0 { fmt.Fprintf(opts.IO.Out, " ACL: %s\n", strings.Join(current.ACL, ", ")) } diff --git a/pkg/cmd/shared/apputil/output.go b/pkg/cmd/shared/apputil/output.go index 2b2d6772..19dee894 100644 --- a/pkg/cmd/shared/apputil/output.go +++ b/pkg/cmd/shared/apputil/output.go @@ -9,21 +9,23 @@ import ( // every command that emits one (`application current`, `application select`, // `auth login`) type ApplicationOutput struct { - ID string `json:"id"` - Alias string `json:"alias"` - Name string `json:"name"` - Plan string `json:"plan"` - ACL []string `json:"acl,omitempty"` + ID string `json:"id"` + Alias string `json:"alias"` + Name string `json:"name"` + Plan string `json:"plan"` + Status string `json:"status,omitempty"` + ACL []string `json:"acl,omitempty"` } // NewApplicationOutput builds the output view, reading the alias from the // config so it reflects what was actually persisted. func NewApplicationOutput(cfg config.IConfig, app *dashboard.Application) ApplicationOutput { out := ApplicationOutput{ - ID: app.ID, - Name: app.Name, - Plan: app.PlanLabel, - ACL: app.ACL, + ID: app.ID, + Name: app.Name, + Plan: app.PlanLabel, + Status: app.Status, + ACL: app.ACL, } if alias, ok := cfg.ApplicationAlias(app.ID); ok { out.Alias = alias From 31998380cedc183fb008ee08ea88d98c3154cf96 Mon Sep 17 00:00:00 2001 From: Levi Whalen Date: Wed, 2 Sep 2026 12:04:27 -0600 Subject: [PATCH 3/3] fix tests --- pkg/cmd/application/current/current_test.go | 2 ++ pkg/cmd/application/selectapp/select_test.go | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/application/current/current_test.go b/pkg/cmd/application/current/current_test.go index ae8c820a..2ed3595b 100644 --- a/pkg/cmd/application/current/current_test.go +++ b/pkg/cmd/application/current/current_test.go @@ -112,6 +112,7 @@ func Test_runCurrentCmd(t *testing.T) { assert.Contains(t, got, "My App") assert.Contains(t, got, "Grow Plus") assert.Contains(t, got, "search, settings_read") + assert.Contains(t, got, "active") } func Test_runCurrentCmd_notConfigured(t *testing.T) { @@ -168,5 +169,6 @@ func Test_runCurrentCmd_outputJSON(t *testing.T) { assert.Contains(t, got, `"alias":"my-alias"`) assert.Contains(t, got, `"name":"My App"`) assert.Contains(t, got, `"plan":"Grow Plus"`) + assert.Contains(t, got, `"status":"active"`) assert.Contains(t, got, `"acl":["search","settings_read"]`) } diff --git a/pkg/cmd/application/selectapp/select_test.go b/pkg/cmd/application/selectapp/select_test.go index c76b6286..16190f67 100644 --- a/pkg/cmd/application/selectapp/select_test.go +++ b/pkg/cmd/application/selectapp/select_test.go @@ -117,9 +117,10 @@ func Test_runSelectCmd_NonInteractiveWritesJSONOnlyToStdout(t *testing.T) { var got apputil.ApplicationOutput require.NoError(t, json.Unmarshal(stdout.Bytes(), &got), "stdout: %q", stdout.String()) assert.Equal(t, apputil.ApplicationOutput{ - ID: "APP1", - Alias: "my app", - Name: "My App", + ID: "APP1", + Alias: "my app", + Name: "My App", + Status: "active", }, got) assert.NotContains(t, stdout.String(), "API key") assert.NotContains(t, stdout.String(), "new-key")